# Translate text with t()

Call t() with a key, scope it to a namespace, set default parameters for the whole instance, and decide what renders when a key is missing.

Canonical: https://comvi.io/docs/i18n/translation-function

Call `t()` with a key and get the text for the current locale. All it takes is the i18n
instance from the [Quick start](/docs/i18n/quick-start) and a binding, or bare `@comvi/core`.

`src/Greeting.tsx`

```tsx

export function Greeting() {
const { t } = useI18n();
return <h1>{t('welcome', { name: 'Alice' })}</h1>;
}
```

`useI18n()` returns the `t()` bound to the provider's instance. Outside a component, call
`i18n.t()` after `await i18n.init()`. `t()` always returns a string; a translation with tags
renders through `<T>` on [Insert values and markup](/docs/i18n/tag-interpolation).

## Keys

`src/locales/en.json`

```json
{
"user": {
    "profile": { "title": "My Profile" }
},
"errors.validation.required": "This field is required"
}
```

`src/Profile.tsx`

```tsx

export function Profile() {
const { t } = useI18n();
return (
    <>
      <h1>{t('user.profile.title')}</h1>
      <p>{t('errors.validation.required')}</p>
    </>
);
}
```

Keys use dot notation. A nested catalog and a flat one are the same catalog: the instance
flattens nested JSON when it ingests it, so both keys resolve the same way.

To read a key from another namespace, pass `ns` in the params, never as a prefix in the key
string. `t('dashboard:welcome')` looks up the literal key `dashboard:welcome`.

`src/Dashboard.tsx`

```tsx

export function Dashboard() {
const { t } = useI18n('dashboard');
return (
    <>
      <h1>{t('page.title')}</h1>
      <button>{t('buttons.save', { ns: 'common' })}</button>
    </>
);
}
```

`useI18n('dashboard')` scopes every `t()` in the component to that namespace, and an explicit
`ns` still wins for one call. Scoping does not load the namespace: load it at init or with
`addActiveNamespace()`, on [Split translations into namespaces](/docs/i18n/namespaces).

## Call options

`src/Options.tsx`

```tsx

export function Options() {
const { t } = useI18n();
const isDefault = t('welcome', { name: 'Alice', markers: false }) === 'Welcome, Alice!';
return (
    <p data-default={isDefault}>
      {t('welcome', {
        name: 'Alice',
        ns: 'dashboard',
        locale: 'de',
        fallback: 'Welcome, {name}!',
      })}
    </p>
);
}
```

The params object mixes interpolation values with four call options:

| Option | Type | Effect |
| --- | --- | --- |
| `ns` | `string` | Namespace for this call. Defaults to `defaultNs`. |
| `locale` | `string` | Renders this locale instead of the current one. |
| `fallback` | `string` | Returned, with interpolation, when the key is missing or empty in every locale of the chain. |
| `markers` | `boolean` | `false` turns the in-context editor's invisible markers off for this call. |

Use `locale` for mixed-language content such as a language name shown in its own language.
Use `markers: false` when the editor's invisible markers cause a problem: a comparison or
sort that sees extra characters, a width-sensitive layout, a canvas or a library that counts
characters. Nothing else reads the option, and that one string is then not editable in context.

## Default parameters

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'de',
compiler: icuCompiler,
defaultParams: { formality: 'formal' },
messages: {
    de: { review: '{formality, select, formal {Ihre Bewertung} other {Deine Bewertung}}' },
},
});
```

`src/review.ts`

```ts

await i18n.init();

i18n.t('review'); // formality: "formal"
i18n.t('review', { formality: 'informal' }); // per-call override

i18n.setDefaultParams({ formality: 'informal' });
```

`defaultParams` applies to every `t()` call on the instance, and a per-call value wins key by
key. `setDefaultParams()` replaces the whole object rather than merging, and a key supplied at
construction must stay present with a compatible value. Defaults hold interpolation data only:
`ns`, `locale`, `fallback`, `markers` and `tagInterpolation` are rejected there, and so are `null`
and `undefined`. The `select` in the catalog is why the instance names `compiler: icuCompiler`,
on [Pluralize with ICU](/docs/i18n/pluralization).

## Missing keys

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
onMissingKey: ({ key, locale, namespace }) => {
    console.warn(`Missing translation: ${key} [${locale}/${namespace}]`);
    // Return a string to render it, or nothing to render the key.
},
});
```

The `onMissingKey` option runs after the fallback chain has been tried. Return a string to
render it, or nothing to render the key itself. A `fallback` passed to the call outranks the
option, so the option never sees a call that carries `params.fallback`; a report that counts
misses belongs in a runtime callback instead. Runtime registration is part of the plugin
capability: `.use(plugins())` on the instance, or `useI18nPlugins()` in a component.

`src/MissingKeyReporter.tsx`

```tsx

declare function reportToSentry(info: Record<string, string>): void;

export function MissingKeyReporter() {
const { onMissingKey } = useI18nPlugins();
useEffect(
    () => onMissingKey((key, locale, ns) => reportToSentry({ key, locale, ns })),
    [onMissingKey],
);
return null;
}
```

The runtime callback takes three positional arguments and returns an unsubscribe function.
Several can be registered, and they all run on every miss; the first non-`undefined` result
wins. Keep the callbacks for diagnostics and `params.fallback` for the text a user sees.

## How it works

`t()` picks a catalog before it reads anything. The locale is `params.locale` or the current
one; the namespace is `params.ns` or `defaultNs`. Each `(locale, namespace)` pair is one flat
object in the cache, flattened once where the catalog entered the instance, so a lookup is a
single property read and key depth costs nothing at render. That flat form is also what
`getMessages(locale, namespace)` returns.

When the key is absent from that catalog, or its value is an empty string, the lookup walks
the fallback chain in the same namespace, in array order, and the first non-empty value wins. A message found in a fallback locale is
compiled with that locale's plural rules, not the current one's, so an English fallback for a
Ukrainian key pluralizes as English. When no locale in the chain has text for the key, the missing-key
path runs, in this order:

1. With `strict: 'dev'`, log `[@comvi/core] Translation not found: "key"` in a development
   build; a production build logs nothing here.
2. Emit the `missingKey` event.
3. Run every runtime callback registered with `onMissingKey()`, keeping the first
   non-`undefined` result. These run even when `params.fallback` is set.
4. If `params.fallback` is set, return it interpolated. A runtime result is discarded.
5. Otherwise, if no runtime callback returned a value, run the `onMissingKey` option.
6. Return the first value produced, or the key itself.

Interpolation is a template step on whichever message won. A message is parsed once into
tokens and cached by its text, so the first call pays for the parse and every later call pays
for a lookup and a concatenation; a message with no `{`, `<`, `&` or `'` in it is returned as
is. `{name}` takes the value under that key. Strings, numbers and booleans are concatenated; a
node from `@comvi/core/rich-text` or a framework element renders as an element, and anything
else goes through `String()`. `fallback` is a template too: `{name}` in it is filled
from the same params, and a broken ICU argument in it fails at the call in development, as one
in the catalog does at ingestion. What a missing parameter renders is on
[Insert values and markup](/docs/i18n/tag-interpolation#missing-parameters).

All of that happens synchronously inside the call. `t()` reads the catalog that is in the cache
at that moment and never waits for a load, so a key whose namespace has not arrived yet goes
down the missing-key path and renders as itself. That is why a provider `fallback` or an
`isLoading` check belongs around the first paint, where a key flashing as its own name would be
visible.

What makes the text update later is the binding, not `t()`. A component that calls `useI18n()`
subscribes to the instance's revision events: `localeChanged`, `namespaceLoaded`,
`loadingStateChanged`, `initialized`, `messagesCleared`, `defaultNamespaceChanged` and
`configChanged`. Each of them re-renders that component and no other, so a locale switch or a
catalog that lands later refreshes every `t()` on screen without a store of your own, and a
component that never read the instance is untouched. On a bare `@comvi/core` instance you
subscribe yourself with `i18n.on()`.

## Read next

- [Insert values and markup](/docs/i18n/tag-interpolation): `{param}`, missing parameters and `<T>`.
- [Split translations into namespaces](/docs/i18n/namespaces): define, load and scope namespaces.
- [Type-check translation keys](/docs/i18n/type-safety): generated keys make `t()` autocomplete and check its params.
