---
title: Translatable Input
description: "A value in several languages: the primary language's field, a chip per translation with its state, and one language at a time when there are many"
visibility: guest
draft: false
---

# Translatable Input

The `<x-wirekit::translatable-input>` component edits one value in several languages: a product name,
a category, a shipping method's description. The primary language's field comes first. Under it,
every further language has a chip that says whether it is translated, and a field of its own. With a
few languages every field is on screen; with many, a chip opens its language, one at a time.

## Usage

:::preview{title="Three languages"}
<x-wirekit::translatable-input
    style="max-width: 28rem;"
    label="Product name"
    :locales="['de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français']"
    :value="['de' => 'Kugelschreiber', 'en' => 'Ballpoint pen', 'fr' => '']"
/>
:::

:::source{language="blade"}
<x-wirekit::translatable-input
    label="Product name"
    :locales="['de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français']"
    wire:model="name"
/>
:::

The first language of `locales` is the primary one unless `primary` names another. A translation that
is still missing shows the primary text as its placeholder, so the reader sees what to translate, and
its chip says "missing" until something is typed.

## Many Languages

With more further languages than `inline` (2 by default), the fields of the further languages close.
A chip opens its language and closes the one open before, and "Show all languages" opens every one of
them, in two columns from the `sm` breakpoint on.

:::preview{title="Nine languages, one at a time"}
<x-wirekit::translatable-input
    style="max-width: 36rem;"
    label="Category"
    :locales="['de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', 'it' => 'Italiano', 'es' => 'Español', 'nl' => 'Nederlands', 'pl' => 'Polski', 'sv' => 'Svenska', 'da' => 'Dansk']"
    :value="['de' => 'Schreibwaren', 'en' => 'Stationery', 'fr' => 'Papeterie', 'nl' => 'Kantoorartikelen']"
/>
:::

A chip shows its language's code, a check for translated, a dash for missing and a warning sign for a
field the server rejected. The state is in the chip's accessible name as well, "Italiano: missing",
so it never rests on its color.

## With Livewire

`wire:model` on the component binds each language to its key of one array, `name.de`, `name.en`,
with the modifiers you give it:

```php
// app/Livewire/EditCategory.php
use Livewire\Component;

class EditCategory extends Component
{
    // 1. One array per value, keyed by the language code
    public array $name = ['de' => '', 'en' => '', 'fr' => ''];

    public function save(): void
    {
        // 2. The primary language is required, a translation is not
        $this->validate([
            'name.de' => ['required', 'string', 'max:120'],
            'name.*' => ['nullable', 'string', 'max:120'],
        ]);
    }
}
```

```blade
{{-- resources/views/livewire/edit-category.blade.php --}}
<x-wirekit::translatable-input
    label="Category"
    :locales="['de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français']"
    wire:model.blur="name"
    required
/>
```

An error the server files under one language, `name.fr`, stands under that language's field, its chip
shows the warning sign, and a closed field opens. An error filed under the value itself, `name`,
stands under the legend, and every language's field says it is invalid and points at it. `required`
marks the primary language only: a translation stays optional.

:::preview{title="Error state"}
<x-wirekit::translatable-input
    style="max-width: 28rem;"
    label="Product name"
    :locales="['de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français']"
    :value="['de' => '', 'en' => '', 'fr' => '']"
    error="Give the name in at least one language."
    required
/>
:::

Like every field, it takes a `labelAfter` slot for a mark after the legend, a badge that says the
value is not decided yet, for example, as described for
[Input](/components/input#a-mark-after-the-label).

## In a Plain Form

Without `wire:model`, `name` is the base of the fields' names, `name[de]`, `name[en]`, and `value`
fills them:

```blade
<form method="POST" action="/categories/12">
    @csrf
    @method('PUT')

    <x-wirekit::translatable-input
        label="Category"
        name="name"
        :locales="['de' => 'Deutsch', 'en' => 'English']"
        :value="old('name', $category->name)"
    />
</form>
```

`$category->name` is the array of texts, as a JSON column cast to `array` stores it. Laravel reads
the request back as the same array, and an error under `name.en` reaches the English field.
`old('name', …)` brings back what the reader typed when the request fails validation; with
`$category->name` alone, every language would show the stored text again and the edits would be
gone.

## Several Lines

`multiline` gives every language a [Textarea](/components/textarea), with `rows` lines:

:::preview{title="A description in three languages"}
<x-wirekit::translatable-input
    style="max-width: 32rem;"
    label="Description"
    multiline
    :rows="3"
    :locales="['de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français']"
    :value="['de' => 'Mit dokumentenechter Mine, nachfüllbar.', 'en' => 'With a document-proof refill.', 'fr' => '']"
    hint="Shown on the product page."
/>
:::

## Naming the Languages

Give `locales` as a map of code to name, in the words your readers know. A list of codes works too:
each language is then named in the reader's language through PHP's `intl` extension, `en` as
"Englisch" for a German reader, and by its code where `intl` is not installed. A code is letters,
digits, `-` and `_` (`pt-BR`, `zh_Hant`); anything else is left out, with a warning in the log while
`APP_DEBUG` is on.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string\|null` | `null` | The value's name, the legend of the group |
| `locales` | `array` | `[]` | The languages, a map of code to name or a list of codes |
| `primary` | `string\|null` | `null` | The primary language's code. Without it, the first of `locales` |
| `value` | `array\|null` | `null` | The text per language, for a form without `wire:model` |
| `name` | `string\|null` | `null` | The base of the fields' names, `name[de]` |
| `multiline` | `bool` | `false` | A textarea per language instead of an input |
| `rows` | `int\|string` | `3` | The lines of each textarea, or `auto` to grow with its text |
| `inline` | `int` | `2` | How many further languages stand as fields without asking |
| `hint` | `string\|null` | `null` | A sentence under the legend |
| `help` | `string\|null` | `null` | An explanation in a tooltip beside the legend |
| `error` | `string\|null` | `null` | A message about the value as a whole. Without it, the error bag's message for `name` or the model |
| `required` | `bool` | `false` | Marks and requires the primary language |
| `placeholder` | `string\|null` | `null` | The primary field's placeholder |
| `disabled` | `bool` | `false` | Disables every language |
| `readonly` | `bool` | `false` | Makes every language read-only |
| `size` | `string` | `'md'` | `sm`, `md` or `lg`, as on [Input](/components/input) |
| `scope` | `string\|null` | `null` | Scoped personalization key |

## Accessibility

- The value is a `fieldset` named by its legend, and each field is named by its language in full, so
  a screen reader announces "Product name, group" and then "English, edit text".
- Each field carries `lang` with its code, so a screen reader reads the text in that language's
  pronunciation ([WCAG 3.1.2](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html)).
- A chip is a button. Its name is the language and its state, "English: translated"; the code and the
  glyph on it are hidden from screen readers, and the colors follow the state without carrying it
  alone ([WCAG 1.4.1](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html)).
- Where a chip opens its language, it carries `aria-expanded` and `aria-controls`, and the focus moves
  to the field it opened. "Show all languages" carries `aria-expanded` as well.
- A field the server rejected opens and stays open, so its message is in reach.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Tab` | Moves through the primary field, the chips, "Show all languages" and the open fields |
| `Enter` / `Space` | On a chip: opens its language and moves to its field, or closes it; with every field on screen, moves to its field |

## Design Tokens

| Token | Used for |
| --- | --- |
| `--color-wk-bg-muted` | The code beside each field |
| `--color-wk-text-muted` | A further language's code, and a chip before it is translated |
| `--color-wk-text` | The primary language's code |
| `--color-wk-border-strong` | A chip's edge |
| `--color-wk-bg-hover` | A chip under the pointer |
| `--color-wk-border-success` | The edge of a translated chip |
| `--color-wk-success-text` | The text of a translated chip |
| `--color-wk-border-error` | The edge of a chip whose field has an error |
| `--color-wk-danger-text` | The text of a chip whose field has an error |
| `--color-wk-bg-active` | The chip of the open language |
| `--gap-wk-sm` | The space between a code and its field, and between two fields |
| `--gap-wk-xs` | The space between the chips |

The fields are [Input](/components/input) and [Textarea](/components/textarea), and the group is a
[field set](/components/field), each with its own tokens.

## Further Reading

- [Field](/components/field) for the group's legend, hint and help
- [WCAG 3.1.2: Language of Parts](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html)
