I18nProvider
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
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.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
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
i18nInstanceandthemeTranslations, it deep-mergesthemeTranslations['<language>.trans']into the instance'sthemenamespace when that object has keys. It changes the instance you pass; it does not copy it.It derives
locale,direction,isRTL,isLTRandlanguageNamefromi18nInstance.languagewithgetLanguageInfo, and renders the engine's language context around react-i18next'sI18nextProvider.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{}whenappis empty) and never restores the old value, so a nested provider leaves its own language's messages there after it unmounts.TwilightProvidermounts it withgetTwilightContext().i18n, the instancerootBeforeLoadbuilt from Salla's messages, and its owntranslationsprop. It keeps that instance until the locale changes.No factory is exported:
createI18nInstancelives in src/providers/i18n-factory.ts and is private, and themes do not depend oni18nextdirectly. Derive an instance from the page's withcloneInstance, 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 })withoutforkResourceStore: trueshares 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 passforkResourceStore: true.A key missing in the copy's language comes back in the page's language, not as the key:
cloneInstanceshares 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 (fromtranslations.queries.byLocale()) and keep a theme file for it, or only nest languages you have both for.docs/11-internationalization.md says
I18nProvideris not exported and showscreateI18nInstanceimported from/i18n. The opposite is true:I18nProvideris exported andcreateI18nInstanceis not, so that import fails to resolve. Clone the page instance instead.
Related
Gives a component the translate function t, the page language, its text direction and the language name.
Theme translationsHow a theme's locales/*.json files reach t(): the Vite plugin bundles them, TwilightProvider receives them, and Salla's messages are checked first.
TwilightProviderThe component a theme mounts once around its pages; it shares the store, theme, language and navigation with everything inside it.
translationsDownloads Salla's shared translations file, every storefront message in Arabic and English, which the engine's translate function reads.