Translatable Input
The <x-wirekit:: 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
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.
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:
// 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'],
]);
}
}
{{-- 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.
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.
In a Plain Form
Without wire:model, name is the base of the fields' names, name[de], name[en], and value
fills them:
<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, with rows lines:
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 |
scope |
string|null |
null |
Scoped personalization key |
Accessibility
- The value is a
fieldsetnamed 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
langwith its code, so a screen reader reads the text in that language's pronunciation (WCAG 3.1.2). - 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).
- Where a chip opens its language, it carries
aria-expandedandaria-controls, and the focus moves to the field it opened. "Show all languages" carriesaria-expandedas 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 and Textarea, and the group is a field set, each with its own tokens.
Further Reading
- Field for the group's legend, hint and help
- WCAG 3.1.2: Language of Parts