@comvi/react
The React binding: a provider, hooks over the i18n instance it carries, and a <T> component
for rich text. Subscriptions go through useSyncExternalStore.
Exports
Section titled “Exports”- Components:
I18nProvider,T. - Hooks:
useI18n,useI18nInstance,useLocale,useIsLoading,useI18nLoader,useI18nPlugins,useSetLocaleTransition,useFormatters. - Re-exports:
createI18n,loader,attachLoader,plugins,definePlugin,icuCompiler,devtools. - Types:
I18nProviderProps,MessagesMap(the@comvi/coretype, re-exported),UseI18nReturn,UseI18nLoaderReturn,UseI18nPluginsReturn,UseSetLocaleTransitionReturn,UseFormattersReturn,TProps,ComponentMap,DevtoolsOptions,I18n, every@comvi/coretype.
I18n is a type only. @comvi/core/tags, the free format* functions, getTextDirection,
MessagesCache and flattenCatalog are not re-exported as values; import them from @comvi/core.
import { createI18n, loader, plugins } from '@comvi/react';
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en' }) .use(loader({ en: () => import('./locales/en.json') })) .use(plugins());<I18nProvider>
Section titled “<I18nProvider>”function I18nProvider(props: I18nProviderProps): JSX.Element| Prop | Type | Default | Meaning |
|---|---|---|---|
i18n |
AnyI18nInstance |
required | The i18n instance; any default-params type |
children |
React.ReactNode |
required | The subtree |
autoInit |
boolean |
true |
Call init() on mount unless already initialized or initializing |
fallback |
React.ReactNode |
none | Rendered instead of children until isInitialized; false or null gates blank, undefined does not gate |
ssrLocale |
string |
none | Server-rendered locale, so the first client render matches |
targetLocale |
string |
none | The locale a switch that arrives with its own markup (a route parameter) renders in: from the render it changes in until the instance’s locale next changes, the tree renders in it, so the text commits with that markup. A switch that never sets the locale, a failed or cancelled load, does not end it: the tree stays in targetLocale until the instance’s locale moves. Rendering only: you still set the instance’s locale, and the target’s catalog must be cached or in messages |
messages |
MessagesMap |
none | Catalogs that arrive with the render, keyed locale or locale:namespace as addMessages takes them: a server handover or a route’s own catalog. The render that brings them translates from them at once, through a read view; the provider adds them to the instance when that render commits, so an abandoned render and a server render never write the instance. The reference must be stable (module scope, useMemo, the server payload): the same object is added once, while an inline literal is a new object on every parent render, and each one merges the catalog again and re-renders every useI18n() consumer. Keys that name one catalog, en and en:default, merge key by key in input order, the last write winning, as addMessages merges them |
ssrInitialIsLoading |
boolean |
false |
Server snapshot of isLoading |
ssrInitialIsInitializing |
boolean |
false |
Server snapshot of isInitializing; true makes fallback gate the server render too |
onError |
(error: Error) => void |
none | Receives an init() rejection; otherwise it goes to console.error |
Every hook throws outside the provider.
import { I18nProvider } from '@comvi/react';import { i18n } from './i18n';
export function App() { return ( <I18nProvider i18n={i18n} fallback={<p>Loading</p>}> <h1>Hello</h1> </I18nProvider> );}useI18n(ns?)
Section titled “useI18n(ns?)”function useI18n<D extends DefaultTranslationParams = {}, NS extends string | undefined = undefined>(ns?: NS): UseI18nReturn<D, NS>| Returned | Type | Meaning |
|---|---|---|
t |
NamespaceBoundTranslateFn<NS, D, string> |
Translate to a string; bound to ns and the render locale |
tRaw |
NamespaceBoundTranslateFn<NS, D, TranslationResult> |
Rich-text half |
locale |
string |
Current locale |
setLocale |
(locale: string) => Promise<void> |
setLocale; resolves once loaded |
messagesCache |
ReadonlyMap<string, FlattenedMessages> |
Internal map, read-only |
cacheRevision |
number |
Changes on every cache mutation: addMessages, a load, clearMessages |
isLoading, isInitializing, isInitialized |
boolean |
Loading flags |
addMessages |
(t: Record<string, Record<string, TranslationValue>>) => void |
Merge catalogs |
setFallbackLocale |
(locales: string | string[]) => void |
Fallback chain |
defaultParams |
DefaultParamsSnapshot<D> |
Shallow snapshot |
setDefaultParams |
WrapperI18nInstance<D>['setDefaultParams'] |
Replace defaults |
clearMessages |
(locale?, namespace?) => void |
Clear content |
hasLocale, hasTranslation |
as on the instance | Non-reactive checks; hasTranslation defaults to the render locale, hasLocale takes its locale explicitly |
getLoadedLocales, getActiveNamespaces, getDefaultNamespace |
() => string[], () => string |
Snapshots |
formatNumber, formatDate, formatCurrency, formatRelativeTime |
bound, trailing locale? |
Default to the render locale |
dir |
'ltr' | 'rtl' |
Text direction |
on |
(event, cb) => () => void |
Subscribe |
reportError |
(error, context?) => void |
Route to onError |
The return object is new on every render, so destructure members before putting them in a dependency array.
import { useI18n } from '@comvi/react';
export function Greeting() { const { t, locale, setLocale } = useI18n(); return ( <button onClick={() => void setLocale(locale === 'en' ? 'de' : 'en')}> {t('greeting', { name: 'Alice' })} </button> );}useI18nLoader()
Section titled “useI18nLoader()”function useI18nLoader(): UseI18nLoaderReturn // AcquiredLoaderApiThe loader members of the instance in context: addActiveNamespace, addActiveNamespaces,
removeActiveNamespace, removeActiveNamespaces, reloadMessages, onLoadError. It
throws, in development and production, when the instance has no loader capability. The result
is cached per instance, so the members are stable effect dependencies.
import { useEffect } from 'react';import { useI18n, useI18nLoader } from '@comvi/react';
export function Dashboard() { const { t } = useI18n('dashboard'); const { addActiveNamespace } = useI18nLoader();
useEffect(() => { void addActiveNamespace('dashboard'); }, [addActiveNamespace]);
return <h1>{t('page.title')}</h1>;}useI18nPlugins()
Section titled “useI18nPlugins()”function useI18nPlugins(): UseI18nPluginsReturn // AcquiredPluginsApiThe plugin members of the instance in context: onMissingKey(cb), returning an unsubscribe.
The callback may return a full TranslationResult. It throws when the instance has no plugin
capability.
Selector hooks
Section titled “Selector hooks”function useLocale(): stringfunction useIsLoading(): { isLoading: boolean; isInitializing: boolean }function useI18nInstance(): WrapperI18nInstance| Hook | Meaning |
|---|---|
useLocale() |
Subscribes to locale changes only |
useIsLoading() |
Subscribes to loading state only |
useI18nInstance() |
The i18n instance itself, not reactive; for imperative calls |
useSetLocaleTransition()
Section titled “useSetLocaleTransition()”function useSetLocaleTransition(): { isPending: boolean; setLocale: (locale: string) => void } // returns void, unlike the instance's setLocale()Wraps the locale change in useTransition. isPending stays true until the fetch and the
transition settle; this setLocale returns void.
useFormatters()
Section titled “useFormatters()”function useFormatters(): { formatNumber(value: number, options?: Intl.NumberFormatOptions): string; formatDate(value: Date | number, options?: Intl.DateTimeFormatOptions): string; formatCurrency(value: number, currency: string, options?: Intl.NumberFormatOptions): string; formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;}The four formatters bound to the current locale, memoized on the locale.
const T: React.NamedExoticComponent<TProps>
type ComponentMap = Record<string, ComponentHandler>type ComponentHandler = | string | React.ReactElement | ((params: { children: React.ReactNode }) => React.ReactElement) | ({ props?: Record<string, any> } & ( | { tag: ComponentConfigTarget; component?: ComponentConfigTarget } | { component: ComponentConfigTarget; tag?: ComponentConfigTarget } ))type ComponentConfigTarget = string | React.ReactElement | ((props: any) => React.ReactElement)| Prop | Type | Meaning |
|---|---|---|
i18nKey |
keyof TranslationKeys | PermissiveKey |
Required; typed params become required with it |
params |
TranslationParams |
Interpolation params; any other prop is merged in and wins a collision |
ns, locale, fallback |
string |
Overrides |
markers |
boolean |
false turns the in-context editor’s markers off for this element when they cause a problem; it is then not editable in context |
components |
ComponentMap |
Tag handlers: a tag name, an element, a component, or a { tag | component, props } config; tag wins over component |
children |
React.ReactNode |
Rendered when the key is missing, no fallback was given and onMissingKey returned nothing |
Elements are built structurally, so translated text is never parsed as HTML.
import { T } from '@comvi/react';
export function Legal() { return ( <> <T i18nKey="legal.tos" components={{ link: <a href="/terms" /> }} /> <T i18nKey="maybe.missing">Default fallback text</T> </> );}When a tag handler throws
Section titled “When a tag handler throws”A handler is rendered as a component, never called, so the only guard is the error boundary
around each tag’s render. A throw there is reported as
reportError(error, { source: 'translation', tagName }) and the tag renders its own children.
The boundary is client-only and stays failed until the surrounding tree remounts.
Read next
Section titled “Read next”- React: setup, loading, plurals, tags and typing for a React app.
@comvi/next: the server helpers, routing and plugin scoping.@comvi/core: the i18n instance, the capabilities and every type.