.dev
Storefront templates

Localization

Theme translations — locales/*.json in the kit, the store_template_locales storage, and the t() pipeline with fallback rules.

Theme copy — "Buy it now", "Your cart is empty", checkout headings — ships with the theme as one JSON file per language. The build embeds them into the template, publish moves them to a dedicated backend table, and at runtime every custom component reads them through one function: t().

This is separate from the platform's own chrome strings (native order form labels, validation messages), which Lithium translates internally. Keep theme copy in the theme: that is what makes a published template work on any store in any supported language.

The pipeline

locales/{ar,en,fr}.json           # you author these in the kit
        │  feeef template build

data.i18n = { defaultLocale, locales, messages }   # embedded for local dev
        │  feeef template publish

store_template_locales            # one backend row per language
        │  SSR fetch on the storefront

t("checkout.title")               # in custom JSX, resolved per visitor

Authoring locale files

my-template/
  locales/
    ar.json
    en.json
    fr.json
  feeef.template.json    # optional "defaultLocale": "en"
  pages/...

Nested objects, dotted lookup, simple named parameters:

{
  "checkout": {
    "title": "Checkout",
    "empty": "Your cart is empty",
    "itemsInCart": "{count} items in cart"
  },
  "thanks": {
    "title": "Thank you!",
    "subtitle": "Your order was received"
  },
  "common": {
    "addToCart": "Add to cart",
    "buyNow": "Buy it now"
  }
}

Interpolation is a plain {param} replacement — t("checkout.itemsInCart", { count: 3 }) — not full ICU (no plural rules or select clauses).

Compiled and stored shapes

feeef template build embeds the bundle into the TemplateData and also writes dist/locales/*.json for upload:

{
  "props": { "theme": { "mode": "light" } },
  "i18n": {
    "defaultLocale": "en",
    "locales": ["ar", "en", "fr"],
    "messages": {
      "ar": { "checkout": { "title": "إتمام الشراء" } },
      "en": { "checkout": { "title": "Checkout" } }
    }
  },
  "pages": {}
}

Canonical storage after publishing is the store_template_locales table — one row per language on the listing, not a blob on the template. Publish strips data.i18n and replaces the rows from dist/locales/*.json.

MethodPathPurpose
GET/store_templates/:id/localesPublic, cached bundle { defaultLocale, locales, messages }
PUT/store_templates/:id/localesReplace the full set — what the CLI publish calls
POST/store_templates/:id/localesAdd one locale row
PUT/store_templates/:id/locales/:localeUpdate one locale's messages / default flag
DELETE/store_templates/:id/locales/:localeRemove a locale

Exactly one row is the default; if none is flagged on a full replace, the first locale (alphabetically) becomes it. Mutations require template-owner auth; the GET is public.

On the storefront, when a store has a templateId, the locales bundle is fetched server-side (revalidated every 60 seconds) into the effective data.i18n. Themes still carrying embedded data.i18n (legacy installs, local kit builds, draft previews) fall back to it seamlessly.

Which language does the visitor see?

The active locale is derived from the store's language setting, not the browser:

store.configs.defaultLanguage    # "ar", "en", "fr", "ar-DZ", "arabic", ...
  → normalized to a supported locale: direct match → region prefix ("ar-DZ" → "ar")
    → aliases ("arabic", "français", ...) → fallback "ar"

The same mapping drives the HTML lang/dir and the platform chrome, so theme strings and native strings always agree. On the client, a ?hl= query parameter (persisted as feeef_hl in localStorage) can override the store default — useful for previewing a locale.

Two "defaults" coexist and mean different things:

ConceptFieldRole
Store active languagestore.configs.defaultLanguageWhich locale visitors see
Theme fallback localei18n.defaultLocaleWhere t() looks when a key is missing

Fallback rules

t(key) resolves in strict order and never throws:

  1. messages[activeLocale] — the visitor's locale
  2. messages[defaultLocale] — the theme's fallback locale
  3. The key string itself ("checkout.title") — so a missing translation is visible, not a crash

Example: theme defaultLocale: "en", store language art() reads Arabic first and falls back to English per key.

Using it in custom components

t, useFeeefT and useFeeefLocale are injected into the custom component scope — no imports:

function App() {
  const title = t("checkout.title");
  const meta = t("checkout.itemsInCart", { count: 3 });

  const translate = useFeeefT();      // hook form, same resolver
  const locale = useFeeefLocale();    // "ar" | "en" | "fr"
  const dir = locale === "ar" ? "rtl" : "ltr";

  return (
    <section dir={dir}>
      <h1>{title}</h1>
      <p>{meta}</p>
    </section>
  );
}
SymbolMeaning
t(key, params?)Theme locale lookup with {param} interpolation
useFeeefT()Same resolver as a hook (memoized per template + locale)
useFeeefLocale()Active locale code

Never hardcode bilingual fallbacks in JSX (isArabic ? "اشتر الآن" : "Buy now") — add the key to every locale file instead.

Marketplace components

template_components rows carry code and schemas only — no language packs. When a merchant places a public component from another author, t() inside it resolves against the host store's theme bundle. Missing keys fall back per the rules above, so prefix your keys (myTheme.gallery.zoom) to avoid collisions, and document any keys a reusable component expects.

RTL and LTR

Arabic stores render right-to-left; a portable theme handles both directions:

  • Derive direction from the mapped locale (ar is RTL) and set dir on each chrome root — header, footer, product shell, checkout shell.
  • Use logical CSS properties: margin-inline-start, padding-inline, inset-inline-start, text-align: start — never left/right pairs.
  • Phone number inputs keep their digits LTR (dir="ltr", text-align: left on the input) while the label, placeholder and errors follow the theme direction.
  • Never hardcode dir="ltr" or direction: ltr on containers in a multilang theme.

Roadmap

Pitfalls

BadGood
Hardcoded copy or bilingual ternaries in JSXt("...") + a key in every locale file
Theme strings added to the platform's message fileslocales/*.json in the theme — keeps it portable
Translations nested under props.i18n ad hocTop-level data.i18n via the kit build
Assuming English is the fallbackThe theme's i18n.defaultLocale decides; platform default is Arabic
Full ICU plural syntax in messagesSimple {param} placeholders
Left/right CSS in RTL-capable themesLogical properties + dir from the locale

Related: template kit for the build and publish flow, custom components for the injected scope, and standard pages for translating checkout and thanks copy.

On this page