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

Add translation keys

Beginner6 min

Put your theme's words in locales/ar.json and locales/en.json and read them with t(), with a default for missing keys.

Goal: every sentence your theme adds reads correctly in Arabic and in English, and you can change the wording without touching components.

Mechanism: one JSON file per language in a locales folder, holding keys (names such as trust_badges.title) and their text. The build bundles the folder into one module, the provider hands it to the engine, and a component asks for a key with t(). t looks for the key in Salla's own messages first (the words every store already has, such as "Cart"), then in your files, and falls back to the default text you pass.

1. Try it

Where a key is found, for this page's language. Try a key only Salla has, then one of the playground's own.

Where a key is found: Salla's 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.
Storefront canvas · en · LTR
app: Salla's messages(not here)
theme: locales/en.jsonHello from the theme's own translations
t() answersHello from the theme's own translations
virtual:twilight/theme-translations: {…} 2 keys
ar.trans: {…} 1 keys
playground: {…} 2 keys
greeting: "مرحبًا من ترجمات القالب نفسه"
items: "{0} لا عناصر|{1} عنصر واحد|[2,*] :count عناصر"
en.trans: {…} 1 keys
playground: {…} 2 keys
greeting: "Hello from the theme's own translations"
items: "{0} no items|{1} one item|[2,*] :count items"
Controls
What a theme writes
// 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>;
}

Free delivery over 200 SAR

This playground's locale files have no trust_badges.free_delivery key, so you see the default text, with the amount filled in. In your theme, the file for the page's language answers instead.

2. Add the keys to every language file

locales/en.json
{
  "trust_badges.title": "Why shop with us",
  "trust_badges.free_delivery": "Free delivery over {{amount}} SAR"
}
locales/ar.json
{
  "trust_badges.title": "لماذا تتسوق معنا",
  "trust_badges.free_delivery": "توصيل مجاني للطلبات فوق {{amount}} ريال"
}

Flat keys with dots, as the reference theme writes them, and nested objects ({ "trust_badges": { "title": … } }) both work. {{amount}} is a placeholder t() fills in.

3. Read them with t()

app/components/home/FreeDeliveryNote.tsx
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';

/** A sentence from the theme's own locales/*.json, in the page's language. */
export function FreeDeliveryNote({ amount }: { amount: number }) {
  const { t } = useTranslation();

  return (
    <p className="text-sm">
      {/* key, the text to show if no file has the key, then the values to fill in */}
      {t('trust_badges.free_delivery', 'Free delivery over {{amount}} SAR', { amount })}
    </p>
  );
}

4. Check the wiring (a new theme already has it)

vite.config.ts (excerpt)
// vite.config.ts: the folder the build reads
...(await twilightReact({ localesDir: './locales' })),
app/routes/__root.tsx (excerpt)
// app/routes/__root.tsx: the module the build makes from that folder
import themeTranslations from 'virtual:twilight/theme-translations';

<TwilightProvider translations={themeTranslations}>
In engine terms

themeTranslationsPlugin(localesDir) (src/vite/plugins/theme-translations.plugin.ts) reads every *.json directly inside the folder and exports { '<file name>.trans': contents } as virtual:twilight/theme-translations. I18nProvider (src/providers/I18nProvider.tsx) merges themeTranslations['<locale>.trans'] into the i18next theme namespace with addResourceBundle(locale, 'theme', …, true, true). The instance (src/providers/i18n-factory.ts) has defaultNS: 'app' (Salla's messages, loaded by rootBeforeLoad) and fallbackNS: 'theme', keySeparator: '.'; i18next also finds a flat key that contains dots. useTranslation('theme') or t(key, { ns: 'theme' }) reads your files alone.

Traps