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

Languages and direction

Beginner8 min

The locale in the URL, when translations load, how your theme strings merge with Salla’s, and how right-to-left reaches the page.

Salla stores sell in Arabic first, often in English too. That touches three things at once: the address (/ar/cart or /en/cart), the texts on the page, and the direction the page reads in (right-to-left for Arabic).

Step through what happens to the language between the address bar and a translated button.

Address/en/cartRoot beforeLoadlocale · dirSalla textstranslations.jsoni18n instanceapp + empty themeLocale wrapperis_multilingualLoaders · headSalla texts onlyFirst bytehtml lang · dirI18nProvidermerges theme textslocales/*.jsonvirtual moduleuseTranslation()t · locale · isRTLt(key)app, then theme

1. The language is part of the address

Store pages live under a language code: /ar/cart, /en/cart. A store that sells in one language has no code at all: /cart. Arabic (ar) is the fallback.

In engine terms

Every route is nested under an optional {-$locale} segment. The engine's route generator (buildVirtualRouteConfig, src/vite/adapters/tanstack.adapter.ts) wraps the routes in a $locale.tsx it writes into the engine package's own .twilight/ folder, so /{-$locale}/cart matches both /cart and /ar/cart. A theme's own route is declared the same way: createFileRoute('/{-$locale}/lookbook').

Where a text comes from

Switch the ar / en pill (top bar, or the ☰ menu on a phone) and try keys below: one only Salla has, one only this playground's locales files have.

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 · ar · RTL
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
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>;
}
app/components/LookbookTitle.tsx
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';

export function LookbookTitle() {
  const { t, isRTL } = useTranslation();
  return (
    // Logical spacing (ms-*, text-start) flips by itself in Arabic.
    <h1 className="ms-4 text-start">
      {t('lookbook.title', 'Lookbook')} {isRTL ? '←' : '→'}
    </h1>
  );
}

Why it matters

  • A raw key such as `lookbook.title` shows on the page: the key is missing, or it was translated in a loader or head function, where your texts are not merged yet. Always pass a default: t('lookbook.title', 'Lookbook').
  • Your file changes a Salla text and nothing happens: Salla's app namespace is searched first. Use a key of your own instead of reusing Salla's.
  • An Arabic page is mirrored wrong: physical CSS (margin-left, text-left, ml-4) does not flip. Use logical properties (margin-inline-start, text-start, ms-4).
  • Your own route, declared in app/routes.ts, sits inside the locale wrapper: its file must use createFileRoute('/{-$locale}/…').
  • A multilingual store reached without a code always lands on /ar/…, even if its main language is English. Keep the locale in links: the engine Link adds the current one for you.
Check yourself

Your locales/en.json sets common.titles.cart to "Basket", but the page still says "Shopping Cart". Why?