Translations and right-to-left
Show text in the page language with t(), and build layouts that flip correctly between Arabic and English.
A Salla store usually sells in Arabic and English, and often more languages. Two things change with the language: the words, and the direction the page reads in. Arabic runs right to left (RTL), English left to right (LTR).
This page is en and ltr right now. The ar / en pill (top bar, or the ☰ menu on a phone) switches it, and every live demo on the page follows.
Words: t()
Never write "Cart" in a component. Write a key, and t from useTranslation() returns the text for that key in the page language. The texts come from two places: Salla's shared messages, which every store has, and your theme's own locales/*.json files.
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';
export function CartTitle({ count }: { count: number }) {
const { t, locale, direction } = useTranslation();
return (
<h1 lang={locale} dir={direction}>
{t('common.titles.cart', 'Cart')} ({count})
</h1>
);
}
t() from useTranslation(), reading Salla's messages and the playground's own locales/*.json for this page's language.Try this: pick common.titles.cart, then set the namespace to theme: only the theme files are searched and the key comes back. Then switch the ar / en pill (top bar, or the ☰ menu on a phone).Hello from the theme's own translations
- locale
- en
- direction
- ltr (isRTL: false)
- languageName
- English
- ready
- true
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';
export function Label() {
const { t } = useTranslation();
return <span>{t('playground.greeting')}</span>;
}
// locales/en.json
{
"lookbook": {
"title": "This season's lookbook"
}
}
// locales/ar.json
{
"lookbook": {
"title": "إطلالات الموسم"
}
}
// vite.config.ts: twilightReact({ localesDir: './locales' })
// In a component: t('lookbook.title')
import { formatSallaPlural } from '@salla.sa/twilight-theme-engine/utils';
// Salla's messages use :name placeholders, which t() does not fill in:
t('common.elements.search_about').replace(':word', query); // "Search for (:word)" → "Search for (sofa)"
// …and Salla-style plurals, which t() returns whole:
formatSallaPlural(t('lookbook.items'), 3); // "{0} none|{1} one item|[2,*] :count items" → "3 items"
The language lives in the address
Every page exists once per language: /ar/cart and /en/cart. The first part of the path is the locale, and it decides the language of the whole request, on the server as well as in the browser.
- An address without a locale gets one: on a store with several languages
/cartbecomes/ar/cart, and a first segment that is not a supported code is prefixed the same way. - A store with a single language has no locale segment:
/en/cartbecomes/cart. - Links from the engine's
Linkcomponent take a path without the locale,<Link to="/cart">, and add the current one.
Direction: think start and end, not left and right
The engine sets dir="rtl" or dir="ltr" on the page, and the browser flips text, flex rows and grids for you. What it cannot flip is CSS that names a side: margin-left stays on the left in Arabic too.
Below is one cart row. Switch dir to see it in both directions, and switch CSS between physical properties (left, right) and logical ones (start, end).
.cart-row { border-left: 4px solid; }
.cart-text { margin-left: 12px; text-align: left; }
.badge { margin-left: 8px; }
.cart-price { margin-left: auto; }
With physical properties the Arabic row keeps its spacing on the wrong side: the text touches the image, the badge touches the name, the price leaves the far end, and the coloured border sits at the end instead of the start. With logical properties the one stylesheet is right in both directions: inline-start means left in English and right in Arabic.
In engine terms
useTranslation(ns?)from@salla.sa/twilight-theme-engine/i18nreturns react-i18next'st,i18nandready, pluslocale,direction,isRTL,isLTRandlanguageName(src/providers/I18nProvider.tsx).- One i18next instance per request, with two namespaces:
app(Salla's messages,rootBeforeLoadloads them) searched first, thentheme(yourlocales/*.json, fromvirtual:twilight/theme-translations, merged byTwilightProvider'stranslationsprop). Only the page language is loaded. SUPPORTED_LOCALESlists 39 codes andisLocale(value)checks one;RTL_LOCALESisar,fa,heandur. The generated{-$locale}route redirects an unknown segment to/ar/…and removes the segment on a store that is not multilingual.- On localhost and the preview host the store's username comes first (
/<username>/ar/cart); the router removes it before matching and adds it back to the links it builds, which is why a path is never built fromwindow.location. app/routes/__root.tsxrenders<html lang={ctx.locale} dir={ctx.dir}>fromgetTwilightContext(), so the direction is already right in the server's HTML.- Tailwind's
rtl:andltr:variants work too; the engine's own layout uses them (rtl:lg:ml-8 ltr:lg:mr-8). - Reference: useTranslation, theme translations, SUPPORTED_LOCALES and isLocale, formatSallaPlural.