Languages and RTL

Translate your theme with useTranslation, locale files, and direction-safe layout.

Read this when you add text to your theme, or support both Arabic and English.

Salla stores are multilingual, and Arabic is right-to-left (RTL). The engine handles language and direction for you. Your job is to put text in translation files and write layout that works both ways.

The language comes from the URL

Every route starts with the optional {-$locale} segment (see Routes and pages). For /en/cart, the locale is en. On a store with several languages, a URL without the segment redirects to /ar/.... From the locale it also picks the direction: ar, fa, he and ur are RTL, everything else is LTR.

The root route writes both onto the page, so the first server render already has the right direction:

app/routes/__root.tsx
import { getTwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';

function RootComponent() {
  const ctx = getTwilightContext();

  return (
    <html lang={ctx.locale} dir={ctx.dir} suppressHydrationWarning>
      {/* … */}
    </html>
  );
}

Translation files

Your theme's strings live in locales/ar.json and locales/en.json. Keys are flat strings with dots, not nested objects:

locales/en.json
{
  "pages.cart.total": "Total",
  "pages.cart.empty_cart": "Your cart is empty",
  "pages.products.add_to_cart": "Add to Cart",
  "common.actions.save": "Save"
}

vite.config.ts points the plugin at the folder (twilightReact({ localesDir: './locales' })), and the root route hands the result to the provider:

app/routes/__root.tsx
import themeTranslations from 'virtual:twilight/theme-translations';

<TwilightProvider translations={themeTranslations} layout={ThemeLayout}>
  <Outlet />
</TwilightProvider>

Without that translations prop none of your files load. Add every key to every language file. The translation keys recipe covers the steps.

The store's own translations from Salla are looked up first, and your theme files are the fallback. If a key you added seems ignored, the same key probably exists on the Salla side.

Use a translation

useTranslation returns t plus the language facts:

import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';

function AddToCart() {
  const { t, locale, isRTL } = useTranslation();
  return <button>{t('pages.products.add_to_cart', 'Add to Cart')}</button>;
}

The second argument is the fallback text, shown when the key is missing, so customers never see a raw key. Besides t, you get locale, direction, isRTL, isLTR and languageName. t behaves as in react-i18next, and works during server rendering in the language of the request. Outside the provider the language fields fall back to Arabic, right to left. Every field and the full signature are on the useTranslation page.

Placeholders

Put values in the string with {{name}} and pass them as options. Use defaultValue for the fallback in this form:

t('store.closed_notification', { defaultValue: 'Closed until {{nextOpen}}', nextOpen })

Plurals

Some Salla strings use Laravel-style plural segments such as {1} once|[3,10] :count times. Plain t() cannot read those, so run the result through formatSallaPlural:

app/components/product/ProductDetails.tsx
import { formatSallaPlural } from '@salla.sa/twilight-theme-engine/utils';

<span>{formatSallaPlural(t('pages.products.sold_times'), product.sold_quantity ?? 0)}</span>

It picks the matching segment, fills in :count, and strips inline tags.

Write layout that works both ways

Use logical classes so one design serves both directions:

UseInstead of
ms-4, me-4ml-4, mr-4
ps-4, pe-4pl-4, pr-4
text-start, text-endtext-left, text-right
rtl:rotate-180 on an arrow icontwo icons behind an if

Arrows and chevrons are the usual RTL bug. Open your page at /ar and /en side by side before you call it done.

Next: Settings, to let merchants configure your theme.

On this page