Skip to content

Localizing built-in copy

Texaryn’s built-in widgets invent six pieces of copy of their own: the words on an array’s add, remove and move-up controls, together with each control’s accessible name, the marker that shows a field is required, and the error summary’s heading and per-item detail. Everything else on screen comes from the schema or from UI hints.

Those six are one contract, FormMessages in @texaryn/core, and every binding renders from it. English is the default. Supplying another set replaces every built-in word in every widget, including a layered widget set such as @texaryn/react-mui, which reads the same configuration through @texaryn/react.

import type { FormMessages } from '@texaryn/core'
const french: FormMessages = {
addItem: ({ itemTemplateTitle, containerTitle }) => ({
label: 'Ajouter',
accessibleName: containerTitle
? `Ajouter un élément à ${containerTitle}`
: `Ajouter ${itemTemplateTitle ?? 'un élément'}`,
}),
removeItem: ({ position, itemTitle, containerTitle }) => ({
label: 'Retirer',
accessibleName: `Retirer ${itemTitle ?? 'élément'} ${position}${containerTitle ? ` de ${containerTitle}` : ''}`,
}),
moveItemUp: ({ position, itemTitle, containerTitle }) => ({
label: 'Monter',
accessibleName: `Monter ${itemTitle ?? 'élément'} ${position}${containerTitle ? ` dans ${containerTitle}` : ''}`,
}),
requiredIndicator: () => ({ text: '(obligatoire)', placement: 'before' }),
errorSummaryHeading: ({ count }) => (count === 1 ? 'Il y a un problème' : `Il y a ${count} problèmes`),
errorSummaryDetail: ({ messages }) => ` : ${messages.join(', ')}`,
}

Each message is a function rather than a template, so a language decides its own word order, whether a clause appears at all, and how a position is written. position is 1-based and meant for display.

An action returns both surfaces of its control. The accessible name has to contain the visible label: a speech-input user says the word on the button, and the control has to answer to it. The conformance suite asserts that relation on the rendered DOM.

The required indicator returns its text and which side of the label it sits on. The binding keeps it out of the accessible name; aria-required already carries the state.

The error summary’s heading takes the number of items, and its detail returns the whole text that follows an item’s link, punctuation included, so a locale decides its own colon and its own list separator. The link text itself is the field’s label, a fact the binding supplies.

A locale implements the whole interface. When Texaryn adds a message, a translated application fails to compile rather than rendering one English control. To reword a single message while keeping the rest, use mergeMessages(englishMessages, { requiredIndicator: () => ({ text: '*', placement: 'after' }) }).

React, on the provider:

<FormProvider value={form.runtime} messages={french}>
<FormRoot registry={registry} />
</FormProvider>

Vue, beside the runtime. A ref or getter lets a language switch reach a mounted form:

const messages = ref(french)
provideFormRuntime(form.runtime, { messages })

Web Components, as a property, before or after the element is connected:

import type { TexarynFormElement } from '@texaryn/web-components'
const form = document.createElement('texaryn-form') as TexarynFormElement
form.messages = french

or through mountForm(container, runtime, { registry, idPrefix, messages: french }), whose returned Mount has setMessages for a later switch.

In every binding a new set takes effect on the mounted form without remounting it, so focus and any partly typed value survive a language change.

Texaryn defines no message identifiers. Each function calls your own translation function with keys you choose, so the catalogue stays yours:

import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type { FormMessages } from '@texaryn/core'
function useTexarynMessages(): FormMessages {
const { t } = useTranslation()
return useMemo(
() => ({
addItem: (c) => ({ label: t('form.add'), accessibleName: t('form.addName', c) }),
removeItem: (c) => ({ label: t('form.remove'), accessibleName: t('form.removeName', c) }),
moveItemUp: (c) => ({ label: t('form.up'), accessibleName: t('form.upName', c) }),
requiredIndicator: () => ({ text: t('form.required'), placement: 'after' }),
errorSummaryHeading: (c) => t('form.summaryHeading', c),
errorSummaryDetail: (c) => t('form.summaryDetail', c),
}),
[t],
)
}

The useMemo matters: the set is passed by identity, so a new object on every render would re-render every widget.

A custom widget reads the configured set the same way the built-in ones do: useFormMessages() in React, useFormMessages() returning a computed in Vue, and ctx.messages on the render context in Web Components, read inside update on every render rather than captured when the widget is created, or a later setMessages never reaches it. There is no exported English helper to call directly, because a helper that ignored the configuration would leave one widget in English while the form around it translates.

Validation messages come from the schema evaluator, not from Texaryn, and are not part of this contract. The error summary’s focus behaviour is ADR-005’s, not this contract’s. Texaryn sets no text direction; that is the host document’s.