Skip to content
Twilight React Playground
ثيم رائدaren

I18nProvider

providerAdvancedserverbrowserlive demo

The provider behind useTranslation, which TwilightProvider already mounts: it takes an i18next instance, merges theme translations into it and shares the language.

import { I18nProvider, I18nProviderProps } from '@salla.sa/twilight-theme-engine/i18n';

In plain words

A provider is a component that wraps other components and makes something available to all of them. I18nProvider is the one that makes useTranslation() work: it takes an i18next instance (the object that holds the messages and knows the language), adds your theme's translations to it, and shares it with every component inside.

TwilightProvider already mounts one around the whole page, so an ordinary theme never renders it. Render your own only to show part of a page in another language, or to test a component with messages you choose.

Signature

function I18nProvider(props: I18nProviderProps): React.ReactElement

interface I18nProviderProps {
  i18nInstance: i18n;                      // required: the provider never creates an instance
  themeTranslations?: TranslationMessages; // merged into 'theme' for i18nInstance.language
  children: ReactNode;
}

Try it live

A nested I18nProvider with a copy of the page's i18next instance renders one card in another language.Try this: pick fr: nothing is translated into French, so the card falls back to the page language while its direction turns ltr. Then switch the ar / en pill.
Storefront canvas · en · LTR
The page's I18nProvider
English · ltrHello from the theme's own translationsShopping Cart
A nested I18nProvider for "en"
English · ltrHello from the theme's own translationsShopping Cart
Controls
What a theme writes
import { useMemo, type ReactNode } from 'react';
import { useQuery } from '@tanstack/react-query';
import themeTranslations from 'virtual:twilight/theme-translations';
import { translations } from '@salla.sa/twilight-theme-engine/api/translations';
import { I18nProvider, useTranslation } from '@salla.sa/twilight-theme-engine/i18n';

export function InLanguage({ locale, children }: { locale: string; children: ReactNode }) {
  const { i18n } = useTranslation();
  const { data } = useQuery(translations.queries.byLocale());
  const instance = useMemo(() => {
    const copy = i18n.cloneInstance({ lng: locale, forkResourceStore: true });
    const salla = data?.[`${locale}.trans`];
    if (salla) copy.addResourceBundle(locale, 'app', salla, true, true);
    return copy;
  }, [i18n, locale, data]);

  return (
    <I18nProvider i18nInstance={instance} themeTranslations={themeTranslations}>
      {children}
    </I18nProvider>
  );
}

// Anywhere in a page:
<InLanguage locale={'en'}>
  <PromoCard />
</InLanguage>;

Example

app/components/InLanguage.tsx
import { useMemo, type ReactNode } from 'react';
import { useQuery } from '@tanstack/react-query';
import themeTranslations from 'virtual:twilight/theme-translations';
import { translations } from '@salla.sa/twilight-theme-engine/api/translations';
import { I18nProvider, useTranslation } from '@salla.sa/twilight-theme-engine/i18n';

/** Renders its children in another language, leaving the page's own instance alone. */
export function InLanguage({ locale, children }: { locale: string; children: ReactNode }) {
  const { i18n } = useTranslation();
  const { data } = useQuery(translations.queries.byLocale()); // Salla's messages, every language
  const instance = useMemo(() => {
    const copy = i18n.cloneInstance({ lng: locale, forkResourceStore: true });
    const salla = data?.[`${locale}.trans`];
    if (salla) copy.addResourceBundle(locale, 'app', salla, true, true);
    return copy;
  }, [i18n, locale, data]);

  return (
    <I18nProvider i18nInstance={instance} themeTranslations={themeTranslations}>
      {children}
    </I18nProvider>
  );
}

How it behaves

  • During render, memoised on i18nInstance and themeTranslations, it deep-merges themeTranslations['<language>.trans'] into the instance's theme namespace when that object has keys. It changes the instance you pass; it does not copy it.

  • It derives locale, direction, isRTL, isLTR and languageName from i18nInstance.language with getLanguageInfo, and renders the engine's language context around react-i18next's I18nextProvider.

  • In the browser an effect sets window.translations = { '<locale>.trans': <the instance's app namespace> }, the shape the Salla SDK reads its messages from. It always overwrites (with {} when app is empty) and never restores the old value, so a nested provider leaves its own language's messages there after it unmounts.

  • TwilightProvider mounts it with getTwilightContext().i18n, the instance rootBeforeLoad built from Salla's messages, and its own translations prop. It keeps that instance until the locale changes.

  • No factory is exported: createI18nInstance lives in src/providers/i18n-factory.ts and is private, and themes do not depend on i18next directly. Derive an instance from the page's with cloneInstance, as in the example.

  • The engine creates its instances with react: { useSuspense: false }, and a clone keeps those options, so no <Suspense> is needed around it.

Gotchas

  • i18n.cloneInstance({ lng }) without forkResourceStore: true shares the page's resource store: every bundle you add to the copy, and the theme translations the nested provider merges, land in the page's instance too. Fix: always pass forkResourceStore: true.

  • A key missing in the copy's language comes back in the page's language, not as the key: cloneInstance shares the original's services, including its fallback language, and the forked store still holds the page's messages. Fix: add Salla's bundle for the new language (from translations.queries.byLocale()) and keep a theme file for it, or only nest languages you have both for.

  • docs/11-internationalization.md says I18nProvider is not exported and shows createI18nInstance imported from /i18n. The opposite is true: I18nProvider is exported and createI18nInstance is not, so that import fails to resolve. Clone the page instance instead.

Related

Source and docs