Skip to content
Comvi

@comvi/react

View .md

The React binding: a provider, hooks over the i18n instance it carries, and a <T> component for rich text. Subscriptions go through useSyncExternalStore.

  • 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/core type, re-exported), UseI18nReturn, UseI18nLoaderReturn, UseI18nPluginsReturn, UseSetLocaleTransitionReturn, UseFormattersReturn, TProps, ComponentMap, DevtoolsOptions, I18n, every @comvi/core type.

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.

src/i18n.ts
import { createI18n, loader, plugins } from '@comvi/react';
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en' })
.use(loader({ en: () => import('./locales/en.json') }))
.use(plugins());
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.

src/App.tsx
import { I18nProvider } from '@comvi/react';
import { i18n } from './i18n';
export function App() {
return (
<I18nProvider i18n={i18n} fallback={<p>Loading</p>}>
<h1>Hello</h1>
</I18nProvider>
);
}
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.

src/Greeting.tsx
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>
);
}
function useI18nLoader(): UseI18nLoaderReturn // AcquiredLoaderApi

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

src/Dashboard.tsx
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>;
}
function useI18nPlugins(): UseI18nPluginsReturn // AcquiredPluginsApi

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

function useLocale(): string
function 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
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.

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.

src/Legal.tsx
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>
</>
);
}

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.

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