Theme translations
How a theme's locales/*.json files reach t(): the Vite plugin bundles them, TwilightProvider receives them, and Salla's messages are checked first.
import { TranslationMessages } from '@salla.sa/twilight-theme-engine/i18n';In plain words
Salla already translates the common storefront words, such as "Cart" and "Add to cart". For the words only your theme uses, put one JSON file per language in a locales folder: locales/ar.json, locales/en.json.
Three lines connect those files to t():
vite.config.tsnames the folder:twilightReact({ localesDir: './locales' }).- The build turns the folder into a module you import as
virtual:twilight/theme-translations. "Virtual" means it is generated while the theme builds and never exists as a file. app/routes/__root.tsxhands that module to the provider:<TwilightProvider translations={themeTranslations}>.
After that, t('your.key') finds your text whenever Salla has none for the same key. TranslationMessages is the TypeScript type of the module: one object per language, under keys such as 'ar.trans'.
Signature
type TranslationMessages = Record<string, Record<string, string>>;
// virtual:twilight/theme-translations, generated from locales/*.json:
// { 'ar.trans': { lookbook: { title: '…' } }, 'en.trans': { lookbook: { title: 'Lookbook' } } }
// vite.config.ts: no default, so without it the module is {}
twilightReact({ localesDir?: string })
// app/routes/__root.tsx
<TwilightProvider translations={themeTranslations}>Find a translation key
1,396 keys. Start with a word you would show a shopper, or open a group:
Try it live
app namespace first, then your theme files. Below, the module the build made from packages/playground/locales.Try this: type common.titles.cart (only Salla has it), then playground.items, then just playground.| app: Salla's messages | (not here) |
|---|---|
| theme: locales/ar.json | مرحبًا من ترجمات القالب نفسه |
| t() answers | مرحبًا من ترجمات القالب نفسه |
virtual:twilight/theme-translations: {…} 2 keys
ar.trans: {…} 1 keys
playground: {…} 2 keys
en.trans: {…} 1 keys
playground: {…} 2 keys
// locales/ar.json and locales/en.json: { "playground": { "greeting": "…" } }
// vite.config.ts: twilightReact({ localesDir: './locales' })
// app/routes/__root.tsx: <TwilightProvider translations={themeTranslations}>
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';
export function Label() {
const { t } = useTranslation();
return <span>{t('playground.greeting')}</span>;
}
Example
// locales/en.json { "lookbook": { "title": "Lookbook" } }
// locales/ar.json { "lookbook": { "title": "دليل الإطلالات" } }
// vite.config.ts twilightReact({ localesDir: './locales' })
// __root.tsx import themeTranslations from 'virtual:twilight/theme-translations';
// <TwilightProvider translations={themeTranslations}>
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';
export function LookbookTitle() {
const { t } = useTranslation();
return <h1>{t('lookbook.title', 'Lookbook')}</h1>;
}
How it behaves
The plugin (
twilight:translations) reads every*.jsondirectly insidelocalesDir, not in subfolders. The file name is the language:en.jsonbecomes'en.trans'. The module is generated in memory; nothing is written to disk.TwilightProviderpasses the object toI18nProvider, which merges only the page language's part (themeTranslations['<locale>.trans']) into the i18nextthemenamespace, deep and overwriting, and only when that part has keys.themeis the fallback namespace:t(key)looks in Salla's messages (app) first.t(key, { ns: 'theme' })reads your file alone.Nested objects and flat dotted keys both work. The playground's own
locales/*.jsonare nested; docs/getting-started/08-add-translations.md asks for flat keys, which is a style choice, not a requirement.The module is declared as
Record<string, Record<string, string>>in the engine'sdist/ambient/virtual-modules.d.ts, which a theme lists underfilesintsconfig.json. Nested files do not match that type, and work at runtime all the same.In the dev server the plugin watches the folder and invalidates the module when a
*.jsonfile in it changes or is added.
Gotchas
One invalid file empties the module for every language. The plugin wraps the whole read in
try/catchand returnsexport default {}without logging anything (theme-translations.plugin.ts), so a trailing comma inen.jsonmakes every theme-only key render its default or its key, in Arabic too. Fix: when theme texts vanish, check each file parses, e.g.node -e "require('./locales/en.json')", which names the broken position.Forgetting either end fails silently. Without
localesDirthe module is{}; without thetranslationspropI18nProviderhas nothing to merge. Nothing warns, andtreturns defaults or keys. Fix: keep both, aspackages/theme-custom/vite.config.tsandapp/routes/__root.tsxdo.A key Salla also has never shows your text, because
appis searched beforetheme. Fix: give your keys a prefix Salla does not use (the playground usesplayground.*), or read your file explicitly witht(key, { ns: 'theme' }).Only the instance inside the provider has your texts.
rootBeforeLoadbuilds a new instance withcreateI18nInstance(locale, translations), which receives Salla's messages only, and loaders andheadfunctions run before the provider renders; each client-side navigation builds another. SogetTwilightContext().i18n.t('lookbook.title')in a loader returns the key, and so doesuseTwilight().i18n.t('lookbook.title')in a component after the first client-side navigation. Fix: useuseTranslation()in components; in loaders andhead, pass the text as the default (i18n.t('lookbook.title', 'Lookbook')) or first merge your file the way the provider does:i18n.addResourceBundle(locale, 'theme', themeTranslations[locale + '.trans'] ?? {}, true, true).
Related
Gives a component the translate function t, the page language, its text direction and the language name.
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.
I18nProviderThe provider behind useTranslation, which TwilightProvider already mounts: it takes an i18next instance, merges theme translations into it and shares the language.