Supply translated component labels, merge service catalogs, and keep locale behavior consistent.
3 min read
A screen has two kinds of text. Your application supplies its own titles, field labels, and actions through props or children. Shared components translate the copy they own, such as a dialog's close control or an input's password toggle. Neither kind is translated automatically from an English string.
An empty Button has no useful name. Pass visible text, or name an icon-only control explicitly.
Initialize the catalogs
The first-control example shows a complete minimal setup. A service with its own YAML catalogs uses this configuration, with paths relative to that service's i18n module:
import { initServiceI18n } from '@tale/ui/i18n/init-service';
import { uiMessages } from '@tale/ui/i18n/messages';
import enMessages from '../../messages/en.yml';
import deMessages from '../../messages/de.yml';
import frMessages from '../../messages/fr.yml';
import globalMessages from '../../messages/global.yml';
export const i18n = initServiceI18n({
bundles: { en: enMessages, de: deMessages, fr: frMessages },
regional: import.meta.glob('../../messages/*-*.yml', {
eager: true,
import: 'default',
}),
global: globalMessages,
packages: [uiMessages],
});Register the YAML Vite plugin from Installation. The glob must remain a literal at the call site. Add marketingUiMessages from @tale/marketing-ui/i18n/messages to packages when rendering that package.
| Input | Purpose |
|---|---|
bundles | The service's complete base-locale namespace trees. |
packages | Shared catalogs, merged before the service's catalogs. Later package entries override earlier ones per key. |
regional | Sparse regional overrides, such as de-CH, discovered by the glob. |
global | Locale-neutral keys shared across locales, such as product names. |
The merge is deep: overriding one service key preserves sibling keys from the package. A service override wins over the corresponding package key. Use that ability deliberately; changing a shared action label can affect multiple controls.
Read a translated label
import { Button } from '@tale/ui/button';
import { useT } from '@tale/ui/i18n/client';
export function SaveButton() {
const { t } = useT('common');
return <Button type="submit">{t('actions.save')}</Button>;
}useT wraps react-i18next for a namespace. Services can expose a typed wrapper narrowed to their own namespace catalog. Keep interpolation variables separate from translated prose; never assemble a sentence from separately translated fragments.
ICU handles counts and interpolation:
members:
count: "{count, plural, one {# member} other {# members}}"Read it with t('count', { count }) in the members namespace. Supply equivalent native plural forms in each locale and test zero, one, and multiple items.
Choose one locale source
For preference-driven apps, use AppShell with locale={{ mode: 'client' }}. Its locale provider detects the saved preference and browser language; the bridge synchronizes i18n.
For URL-driven sites, omit that option and mount LocaleSync from the root route with the route's locale. Do not run two competing locale sources. This component documentation site deliberately pins LocaleSync to English: its English pages and examples do not have translated routes, even though its chrome catalogs contain EN, DE, and FR keys.
Add or change copy
- Find the component that owns the text. Put reusable control copy in the package catalog; put screen-specific copy in the service catalog.
- Update
en,de, andfrtogether. Usede-CHonly where a regional override is needed, rather than copying the entire German catalog. - Write each locale from the intended meaning. Tale uses
duin German andtuin French. Preserve values, interpolation keys, and conditions while making the sentence natural. - Check the actual state that renders the key: loading, disabled, success, error, or empty. Inspect long labels on a narrow screen.
Check catalog wiring
Services use defineI18nTests from @tale/ui/i18n/tests. Its packageCatalogs option lists the package message directories merged at runtime. These directories inform missing-key checks; package-owned tests remain responsible for orphan keys inside the package.
If a component shows a dotted key, first check its namespace and spelling, then the runtime packages list and service override. If tests alone report a shared key missing, compare packageCatalogs with the runtime configuration. Run the service's tests after a catalog change; key parity cannot judge native grammar or whether the label fits its control.