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.tsEach 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 witht(key, { name: value })at the call site.locale, the active locale, resolved from the device. Resolution is specific-to-general:pt-BRmatches apt-BRcatalog first, thenpt, 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, and 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
periodandperiodlyvariables ("yearly" → "jährlich") localize automatically in 44 locales, independent of your catalogs. - Start with a catalog even in one language. Every user-facing string lives in
messages/en.tsand reaches the page throught()from the first commit,aria-labels and button copy included. A second locale is then one new file and no code change, copy is reviewable in one place, and nothing has to be hunted out of JSX later. Thesuperwall createscaffold is set up this way.
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?