Skip to content

Multi-Language Input

Edit translations in one field, with a language picker and a status for each translation.

import { MultiLanguageInput } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use it for content you publish in more than one language: a product name, a page title, a description, an email subject. One field holds every language, so a form is as long as it would be in one. The field also shows which translations are missing, out of date or waiting for review.

It scales from two locales to forty. Pinned locales are one-press keys; every other locale is a search away, by its name, its own name, its code or its region. Wrap several fields in a MultiLanguageProvider and they share one switcher, one status model and a side-by-side view for translators.

import { useState } from 'react'
import { Field, FieldLabel, MultiLanguageInput } from '@youj/react'

export function ProductName() {
  const [name, setName] = useState<Record<string, string>>({ en: 'Linen overshirt' })
  return (
    <Field required>
      <FieldLabel>Product name</FieldLabel>
      <MultiLanguageInput locales={['en', 'fr', 'de']} value={name} onValueChange={setName} name="name" />
    </Field>
  )
}
  • The value is a record of locale to text: { en: 'Linen overshirt', fr: 'Surchemise en lin' }.
  • Locales are BCP 47 tags ('en', 'pt-BR', 'zh-Hans'). Their names come from Intl.DisplayNames in the interface's language, and you can pass { code, label } to name one yourself.
  • Direction and script: every control carries its locale's lang and dir. Arabic and Hebrew run right to left, spellcheck and speech follow the language, and Japanese gets Japanese glyphs rather than Chinese ones. Chinese, Japanese, Korean, Devanagari and Thai are set a step larger.
  • The source locale (sourceLocale, the first by default) is the one translations are made from. A required Field requires it, or every locale in requiredLocales.
  • Native forms: with a name, the form submits one hidden input per locale: name[en], name[fr], and so on.

Reference

Propslocales, sourceLocale, requiredLocales… (51)

Props

MultiLanguageInput

MultiLanguageInput props
PropType
localesLocaleInput[]

The locales (BCP 47 tags or { code, label?, flag?, group? }). Required on its own; inside a MultiLanguageProvider, the provider's apply.

–
sourceLocalestring

On its own: the locale translations are made from. Default: the first locale.

–
requiredLocalesstring[]

Locales a required field must be filled in. Default: the provider's, or the source locale.

–
localestring

On its own: the locale being edited (controlled).

–
onLocaleChange(locale: string) => void–
viewMultiLanguageView
'single''side-by-side'

On its own: 'side-by-side' shows the source beside the translation.

–
pinnedstring[]

On its own: locales kept as one-press keys in the switcher.

–
valueTranslationValue–
defaultValueTranslationValue–
onValueChange(value: TranslationValue) => void–
reviewTranslationReviews

Per locale: the source text it was translated against and whether it's reviewed (controlled). Store it beside the value to keep "outdated" across sessions.

–
defaultReviewTranslationReviews–
onReviewChange(review: TranslationReviews) => void–
suggestionsRecord<string, string>

Machine suggestions per locale, offered in empty locales and filled as "needs review".

–
multilineboolean

A textarea instead of a single line.

–
placeholderstring–
disabledboolean–
requiredboolean

Requires the required locales. Default: the surrounding Field's required.

–
namestring

Submits one hidden input per locale, name[code].

–
maxLengthnumber

A hard cap: the browser stops typing past it.

–
limitnumber | Record<string, number>

A soft limit in characters, for every locale or per locale: a count, and an error past it.

–
validate(text: string, locale: string) => string | null | undefined | false

A message for invalid text in a locale, or nothing.

–
errorsRecord<string, string | undefined>

Errors per locale from your app or server.

–
refRef<HTMLInputElement & HTMLTextAreaElement>

The text control, e.g. react-hook-form's field.ref.

–
onBlur(event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void–
aria-labelstring

Names the field outside a Field (the locale is added: "Title, German").

–
Other props pass through: extends ComponentProps<'div'>.

MultiLanguageProvider

MultiLanguageProvider props
PropType
localesRequiredLocaleInput[]

The locales, as BCP 47 tags ('en', 'pt-BR') or { code, label?, flag?, group? }. Their order is the order everywhere.

–
sourceLocalestring

The locale translations are made from. Default: the first locale.

–
requiredLocalesstring[]

Locales a required field must be filled in. Default: the source locale.

–
localestring

The locale being edited (controlled).

–
defaultLocalestring

The locale edited at first (uncontrolled). Default: the source locale.

–
onLocaleChange(locale: string) => void–
pinnedstring[]

Locales kept as one-press keys in the switcher; the rest are a search away. Default: every locale when there are 5 or fewer, else the source and required locales.

–
viewMultiLanguageView
'single''side-by-side'

'side-by-side' shows the source beside each translation.

–
defaultViewMultiLanguageView
'single''side-by-side'
–
onViewChange(view: MultiLanguageView) => void–
missingOnlyboolean

Lists only the locales that need work in the picker.

–
defaultMissingOnlyboolean–
onMissingOnlyChange(missingOnly: boolean) => void–
groupBy'region' | 'script' | 'none'

How the picker groups the locales.

default 'region'

'region'
displayLocalestring

The language locales are named in. Default: the YoujProvider's locale.

–
onBulkAction(result: BulkResult) => void

After a bulk action: offer an Undo with result.undo.

–
childrenRequiredReactNode–

MultiLanguageSwitcher, MultiLanguageProgress and MultiLanguageActions

MultiLanguageSwitcher props
PropType
classNamestring–
stepperboolean

Up and down buttons to the previous and next locale that needs work. Default: with more than three locales.

–
progressboolean

The progress ring and count.

default true

true
MultiLanguageProgress props
PropType
variant'ring' | 'bar'

'ring' is compact, for a toolbar; 'bar' also shows what's outdated or waiting for review.

default 'ring'

'ring'
localestring

One locale across the fields. Default: every locale.

–
showCountboolean

Shows the count: "12/16" beside a ring, a caption with a legend over a bar.

default variant === 'ring'

variant === 'ring'
Other props pass through: extends ComponentProps<'div'>.
MultiLanguageActions props
PropType
classNamestring–
localestring

The locale the actions apply to. Default: the one being edited.

–

Helpers

ExportWhat it does
useMultiLanguage()Returns locale, setLocale, view, setView, missingOnly, setMissingOnly, summaries (per locale: worst status, and field counts per status), next() / previous(), and the bulk actions.
missingTranslations(value, locales)The locales among locales with no text.
translationStatus(value, locale, source, review?)One translation's status.
describeLocale(code, displayLocale)A locale's name, native name, script, region and direction.
countCharacters(text, locale)Characters as a reader counts them (grapheme clusters).
TranslationStatusIconThe status glyph on its own, for your own overviews.
MotionLocale switch, keys and picker highlight, status glyph… · reduced motion

Motion

Motion tokens
MomentTokens
Locale switchthe new text fades up and drifts 6px in from the side it came from (a Web Animation; the next switch cancels it)
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Keys and picker highlightglide to the selected key and the highlighted row
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Status glypha changed status pops in
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Locale completethe disc lands and its check draws
spring.settle

0.45s, bounce 0.3

0.45s, bounce 0.3
Progress ringthe arc follows the count, and turns to success at 100%
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Progress bar and meterssegments grow and move along the track
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Copy flashthe surface tints at once, then fades back
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Source panefades in with a 4px drift as side by side opens
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)

Reduced motion

The text changes in place, keys and highlights jump, statuses appear drawn, meters jump to their value and nothing flashes. Announcements and focus are unchanged.

Reduced motion

The text swaps in place, the keys and the picker highlight jump, statuses appear already drawn, and meters show their value at once. Copying doesn't flash. Focus, the caret and every announcement are unchanged.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • Names: every control is named by its Field label and its locale: "Title, German".
  • Descriptions: it's described by the Field's description and error, the source text in side-by-side view, the note for the locale being edited, and the character count. aria-invalid follows the locale being edited.
  • Keys: the keys are a toggle group named "Language". Each key reads its language and, when there's work, its status ("French, Missing").
  • Picker: its search is a combobox that drives a grouped listbox through aria-activedescendant, so focus stays in the search. An empty result is announced.
  • Announcements: locale switches from the keyboard shortcut or the picker, copies, suggestions, "mark reviewed" and bulk actions are announced politely.
  • Progress: the progress ring and bar are progress bars, and their value text reads the whole breakdown ("12 of 16 languages complete, 2 outdated, 2 missing").
KeyAction
← →Move between the keys.
Any letter on the picker keyOpens the picker with that letter searched.
↑ ↓, Enter, Escape (picker)Move through the locales, pick one, close.
⌥⌘↓ / ⌥⌘↑ (Ctrl+Alt elsewhere)Next or previous locale that needs work, from anywhere in the field or form.