Skip to main content

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:

ts
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.

InputPurpose
bundlesThe service's complete base-locale namespace trees.
packagesShared catalogs, merged before the service's catalogs. Later package entries override earlier ones per key.
regionalSparse regional overrides, such as de-CH, discovered by the glob.
globalLocale-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

tsx
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:

yaml
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

  1. Find the component that owns the text. Put reusable control copy in the package catalog; put screen-specific copy in the service catalog.
  2. Update en, de, and fr together. Use de-CH only where a regional override is needed, rather than copying the entire German catalog.
  3. Write each locale from the intended meaning. Tale uses du in German and tu in French. Preserve values, interpolation keys, and conditions while making the sentence natural.
  4. 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.