Skip to content

React

Comvi i18n integrates with React via the @comvi/react package. This guide covers installation, setup, hooks, and safe HTML interpolation with the <T> component.

Terminal window
npm install @comvi/react
  1. Create an i18n instance:

    src/i18n.ts
    import { createI18n } from "@comvi/react";
    export const i18n = createI18n({
    locale: "en",
    fallbackLocale: "en",
    });
    // Register translations
    i18n.registerLoader({
    en: () => import("./locales/en.json"),
    de: () => import("./locales/de.json"),
    "en:admin": () => import("./locales/admin/en.json"),
    });
  2. Wrap your app in I18nProvider:

    src/main.tsx
    import { createRoot } from "react-dom/client";
    import { I18nProvider } from "@comvi/react";
    import { i18n } from "./i18n";
    import App from "./App";
    createRoot(document.getElementById("root")!).render(
    <I18nProvider i18n={i18n}>
    <App />
    </I18nProvider>
    );

    The provider auto-initializes the i18n instance on mount and uses useSyncExternalStore for tear-free state subscriptions. Optional props:

    • autoInit?: boolean (default true) — skip if already initialized
    • onError?: (err) => void — error handler
    • ssrInitialLocale?: string — hydration locale (SSR)
    • ssrInitialIsLoading?: boolean — hydration state
    • ssrInitialIsInitializing?: boolean — hydration state
  3. Use in components:

    src/App.tsx
    import { useI18n } from "@comvi/react";
    export default function App() {
    const { t, locale, setLocale } = useI18n();
    return (
    <div>
    <h1>{t("home.title")}</h1>
    <p>Current locale: {locale}</p>
    <button onClick={() => setLocale("de")}>Deutsch</button>
    </div>
    );
    }

useI18n() returns translation methods and reactive state. Commonly used values:

interface UseI18nReturnPreview {
t(key: string, params?: object): string; // flattened rich text as string
locale: string; // current locale (read-only)
setLocale(locale: string): Promise<void>;
isLoading: boolean;
isInitializing: boolean;
onMissingKey(
cb: (key: string, locale: string, namespace: string) => string | void,
): () => void;
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
formatDate(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
formatCurrency(value: number, currency: string, options?: Intl.NumberFormatOptions): string;
dir: "ltr" | "rtl";
}

See the React API reference for the full return type.

Access a specific namespace:

import { useI18n } from "@comvi/react";
function DashboardPage() {
const { t } = useI18n("admin");
return <h1>{t("dashboard.title")}</h1>;
}

t() always returns a string. If a translation contains rich text tags, t() flattens them to text. Use the <T> component to render rich text with React elements.

import { useI18n } from "@comvi/react";
function Example() {
const { t } = useI18n();
return (
<>
{/* t() returns flattened string */}
<h1>{t("home.title")}</h1>
<p>{t("home.greeting", { name: "Alice" })}</p>
{/* Parameters, fallback, specific locale */}
<p>{t("admin.section", { fallback: "Not found" })}</p>
<p>{t("home.title", { locale: "de" })}</p>
<p>{t("dashboard.title", { ns: "admin" })}</p>
</>
);
}

The <T> component renders rich text (HTML tags, custom components) safely:

import { T } from "@comvi/react";
function LegalNotice() {
return (
<>
{/* Custom component function */}
<T
i18nKey="legal.tos"
components={{
link: ({ children }) => (
<a href="/terms" className="text-blue-600">
{children}
</a>
),
}}
/>
{/* Element component */}
<T
i18nKey="legal.tos"
components={{
link: <a href="/terms" className="text-blue-600" />,
}}
/>
{/* With parameters */}
<T
i18nKey="welcome.message"
params={{ name: "Alice", count: 5 }}
ns="admin"
/>
{/* Children as fallback */}
<T i18nKey="optional.key">Fallback text</T>
</>
);
}
interface TProps {
i18nKey: string; // Translation key (required)
params?: Record<string, unknown>; // Parameters for interpolation
ns?: string; // Namespace override
locale?: string; // Locale override
fallback?: string; // Fallback if key missing
children?: React.ReactNode; // Used as fallback if key missing
components?: {
[tag: string]: string | React.ReactElement | (({ children }: { children: React.ReactNode }) => React.ReactElement);
};
// Any other props are merged into params
}

Access the provider’s context directly (rarely needed):

import { useI18nContext } from "@comvi/react";
function MyComponent() {
const { i18n, locale, isLoading, isInitializing, translationCache } = useI18nContext();
// i18n is the underlying I18n instance
}

Use setLocale() to await translation loading:

import { useI18n } from "@comvi/react";
function LanguageSwitcher() {
const { locale, setLocale } = useI18n();
const handleChange = async (newLocale) => {
await setLocale(newLocale);
// Translations are guaranteed loaded here
document.documentElement.lang = newLocale;
document.documentElement.dir = newLocale === "ar" ? "rtl" : "ltr";
};
return (
<select value={locale} onChange={(e) => handleChange(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="ar">العربية</option>
</select>
);
}

Comvi uses useSyncExternalStore internally, so it’s compatible with React 18+ concurrent features (Suspense, Transitions, useTransition) without tearing:

import { useI18n } from "@comvi/react";
import { useTransition } from "react";
function LocaleSwitcher() {
const { locale, setLocale } = useI18n();
const [isPending, startTransition] = useTransition();
const handleSwitch = (newLocale) => {
startTransition(() => {
void setLocale(newLocale);
});
};
return (
<>
<button onClick={() => handleSwitch("de")} disabled={isPending}>
{isPending ? "Loading..." : "Switch to German"}
</button>
</>
);
}
import { useI18n } from "@comvi/react";
function App() {
const { t, isLoading, isInitializing } = useI18n();
if (isInitializing) return <div>Initializing...</div>;
if (isLoading) return <div>{t("common.loading")}</div>;
return <div>Content loaded</div>;
}

Register the callback inside useEffect so it runs once and is cleaned up on unmount:

import { useEffect } from "react";
import { useI18n } from "@comvi/react";
function MissingKeyLogger() {
const { onMissingKey } = useI18n();
useEffect(() => {
return onMissingKey((key, locale, namespace) => {
console.warn(`Missing: ${namespace}:${key} (${locale})`);
return `[${key}]`;
});
}, [onMissingKey]);
return null;
}
import { createI18n } from "@comvi/react";
import { FetchLoader } from "@comvi/plugin-fetch-loader";
import { LocaleDetector } from "@comvi/plugin-locale-detector";
export const i18n = createI18n({
locale: "en",
fallbackLocale: "en",
})
.use(
FetchLoader({
cdnUrl: "https://cdn.example.com/translations/",
})
)
.use(
LocaleDetector({
supportedLocales: ["en", "de", "fr"],
order: ["localStorage", "navigator"],
caches: ["localStorage"],
})
);
i18n.registerLoader({
en: () => import("./locales/en.json"),
de: () => import("./locales/de.json"),
});

For framework SSR, prefer the Next.js guide. For custom React SSR, create an i18n instance per request on the server and pass matching ssrInitialLocale, ssrInitialIsLoading, and ssrInitialIsInitializing values to <I18nProvider> during hydration.