Translate text with t()
Call t() with a key and get the text for the current locale. All it takes is the i18n
instance from the Quick start and a binding, or bare @comvi/core.
import { useI18n } from '@comvi/react';
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.
{ "user": { "profile": { "title": "My Profile" } }, "errors.validation.required": "This field is required"}import { useI18n } from '@comvi/react';
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.
import { useI18n } from '@comvi/react';
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.
Call options
Section titled “Call options”import { useI18n } from '@comvi/react';
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
Section titled “Default parameters”import { createI18n } from '@comvi/core';import { icuCompiler } from '@comvi/core/icu';
export const i18n = createI18n({ locale: 'de', compiler: icuCompiler, defaultParams: { formality: 'formal' }, messages: { de: { review: '{formality, select, formal {Ihre Bewertung} other {Deine Bewertung}}' }, },});import { i18n } from './i18n';
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.
Missing keys
Section titled “Missing keys”import { createI18n } from '@comvi/core';
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.
import { useEffect } from 'react';import { useI18nPlugins } from '@comvi/react';
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
Section titled “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:
- With
strict: 'dev', log[@comvi/core] Translation not found: "key"in a development build; a production build logs nothing here. - Emit the
missingKeyevent. - Run every runtime callback registered with
onMissingKey(), keeping the first non-undefinedresult. These run even whenparams.fallbackis set. - If
params.fallbackis set, return it interpolated. A runtime result is discarded. - Otherwise, if no runtime callback returned a value, run the
onMissingKeyoption. - 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.
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
Section titled “Read next”- Insert values and markup:
{param}, missing parameters and<T>. - Split translations into namespaces: define, load and scope namespaces.
- Type-check translation keys: generated keys make
t()autocomplete and check its params.