Localization

Ship a paywall in multiple languages by adding one file per locale — no registration, no wiring.

Ship a paywall in multiple languages by adding one file per locale. The filename is the locale, and the device picks which one renders — no registration, no wiring.

Add locales

Message catalogs live in messages/ directories, at the same two levels as components and assets:

superwall/
├── messages/                 shared by every paywall
│   ├── en.ts
│   └── de.ts
└── paywalls/pro/
    └── messages/             this paywall's own
        ├── en.ts
        └── fr.ts

Each file default-exports a nested object:

// paywalls/pro/messages/fr.ts
export default {
  paywall: {
    title: "Passez à Pro",
    cta: "S'abonner · {price}",
    perMonth: "{price} par mois, facturé annuellement",
  },
} as const;

A paywall's own catalog layers over the shared one — it overrides the keys it names and inherits the rest. A locale can exist in either layer or both.

If your fallback language isn't English, set it in config.ts:

localization: { defaultLocale: "en" },

Use the strings — useTranslation()

const { t, locale, setLocale, locales } = useTranslation();

<h1>{t("paywall.title")}</h1>
<button>{price ? t("paywall.cta", { price }) : t("paywall.ctaBare")}</button>
<button aria-label={t("paywall.close")}>×</button>
  • t(key, values?) — the translated string for the active locale. Interpolation is {name} in the catalog with t(key, { name: value }) at the call site.
  • locale — the active locale, resolved from the device. Resolution is specific-to-general: pt-BR matches a pt-BR catalog first, then pt, then the default locale.
  • setLocale(locale) — override the device; setLocale(undefined) returns to auto-detection. This is for previews and tests — on device, the system setting is the truth.
  • locales — every locale that has a catalog.

How fallbacks behave

  • A key missing from the active locale falls back to the default locale per key — a partial translation stays usable while it's being finished.
  • An unknown key renders as itself, so t() never breaks. The flip side: key typos are invisible at runtime — nothing throws, the key just shows up on screen. Check your copy in the studio with its locale switcher.
  • Guard interpolations on the value existing, with a bare-key fallback — as in the CTA above. Never render "Subscribe · undefined".

The rules

  • Never put a price in a catalog. Prices are localized by the store — the SDK delivers the right currency and format for the user's region. Interpolate them: "Subscribe · {price}". See Products.
  • No language picker on device. The locale is the person's system setting; preview other locales with the studio's locale switcher.
  • Copy expands. German runs long — size nothing to fit English.
  • Product period and periodly variables ("yearly" → "jährlich") localize automatically in 44 languages, independent of your catalogs.
  • A single-locale paywall needs none of this — plain strings in JSX are fine until the second locale arrives.

There is no plural engine — no ICU, no _one/_other suffixes. Write around plurals, or fork on the count yourself.

The localization example shows four locales, both catalog layers, and guarded interpolation.

How is this guide?

On this page