Languages and direction
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.
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.
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/en.json | Hello from the theme's own translations |
| t() answers | Hello from the theme's own translations |
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>;
}
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
appnamespace 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 usecreateFileRoute('/{-$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 engineLinkadds the current one for you.
Go deeper: useTranslation, theme translations, SUPPORTED_LOCALES, FALLBACK_LOCALE, useDocumentClass and twilightReact.