Internationalisation
One JSON file per locale, t() to read it, and a switch that flips the whole site — direction included.
A site speaks several languages by keeping its text in one JSON file per locale and reading it with t("key"). Switching the locale re-renders every translated string, flips the document to right-to-left where the language reads that way, and changes how format and ago spell numbers and dates.
Setting up
In webfluent.app.json:
json
{
"i18n": {
"default_locale": "en",
"locales": ["en", "ar", "es"],
"dir": "src/translations"
}
}
One file per locale in that directory, flat keys with dots by convention:
json
{
"nav.home": "Home",
"nav.about": "About",
"welcome": "Welcome, {name}!",
"items.one": "{count} item",
"items.other": "{count} items",
"actions.save": "Save"
}
json
{
"nav.home": "الرئيسية",
"nav.about": "حول",
"welcome": "أهلاً، {name}!",
"items.zero": "لا عناصر",
"items.one": "عنصر واحد",
"items.two": "عنصران",
"items.few": "{count} عناصر",
"items.many": "{count} عنصراً",
"items.other": "{count} عنصر",
"actions.save": "حفظ"
}
A missing file is a build warning; a key missing from a locale falls back to the default locale, then to the key itself.
t("key")
wf
type User { name: String }
page Home(path: "/", title: "Home", description: "The home page.") {
state user = User(name: "Sam")
state items: [String] = ["a", "b", "c"]
Heading(t("welcome", { name: user.name })).h1
Text(t("items", { count: items.length }))
Button(t("actions.save")).primary
}
t("key")is the string for the current locale, reactive: every call re-evaluates when the locale changes.t("key", { name: value })fills{name}placeholders from the map.- A
countpicks the plural form:key.onewhen the locale's rules say "one",key.otherotherwise, andkey.zero,key.two,key.few,key.manywhen the file has them and the locale calls for them (Arabic has all six; English two). Falls back tokey.other, thenkey. - The static paint resolves
tfor the default locale at build time, so the pre-rendered HTML is already in the default language.
Switching the locale
wf
app {
state chosen = "en"
Navbar {
Navbar.Brand { Link("i18n", to: "/") }
Navbar.Links {
Link(t("nav.home"), to: "/")
Link(t("nav.about"), to: "/about")
}
Navbar.Actions {
Select(bind: chosen, label: t("nav.language")) {
on change { setLocale(chosen) }
Select.Option("English", value: "en")
Select.Option("العربية", value: "ar")
Select.Option("Español", value: "es")
}
}
}
Router
}
page Home(path: "/") {
Text("Locale: {locale}, direction: {dir}")
if locale == "ar" { Text("النسخة العربية") }
}
setLocale("ar") switches; locale is the current code and dir is "ltr" or "rtl". On a switch, <html lang> and <html dir> are set: ar, he, fa and ur are right-to-left, everything else left-to-right. The built-ins use logical CSS properties (margin-inline-start, not margin-left), so layouts mirror without work on your side; do the same in your own style blocks.
An app block may declare state like a page, as chosen above.
Which locale a page opens in
A page opens in default_locale, unless its address carries ?lang=<code> for one of the configured locales — /about?lang=ar opens the Arabic page. That is the address the build gives each language in the hreflang alternates it writes for search engines, so a reader who arrives from a search result in Arabic reads Arabic.
The runtime does not guess from the browser's language, and it does not remember a choice across visits on its own: both are a few lines of yours. To remember what the reader picked, keep it in a store with persist, and apply it from an effect in the app:
wf
store Prefs {
persist language = ""
action choose(code: String) {
language = code
setLocale(code)
}
}
app {
effect { if Prefs.language != "" { setLocale(Prefs.language) } }
Navbar {
Navbar.Brand { Link("i18n", to: "/") }
Navbar.Actions {
Button("English") { on click { Prefs.choose("en") } }
Button("العربية") { on click { Prefs.choose("ar") } }
}
}
Router
}
The stored value starts empty — "no choice yet" — so a first visit, and a search engine's crawler, still get the ?lang= locale or the default. To start from the browser's language, apply it when nothing is stored: setLocale(navigator.language.slice(0, 2)).
A static build pre-renders every page once, in the default locale; another locale is applied as the page loads. For search engines that is what the ?lang= alternates are for.
Numbers, dates and the locale
format and ago follow the current locale (chapter 14): format(1234.5, .currency, "EUR") is €1,234.50 in en, 1.234,50 € in de; ago(date) says "il y a 3 jours" in fr. The static paint uses a table of the common locales' separators and currency symbols; the live page uses Intl and corrects any difference.
Translating a Markdown page or a .md page
Text inside a .md page is not passed through t; keep one Markdown file per locale and route by a :locale parameter, or keep the long-form text in the translation file under a key and render Markdown(t("about.body")).
Checklist
- Every visible string goes through
t, includinglabel:,placeholder:andaria-label:values. - Never build a sentence from parts (
t("x") + name); use a placeholder so a translator can reorder it. - Give every count a plural key pair, even in English.
- Test with an RTL locale once: alignment, icons that imply direction (arrows), and any
left/rightin your own CSS.
On this page
Setting up t("key") Switching the locale Which locale a page opens in Numbers, dates and the locale Translating a Markdown page or a .md page ChecklistChecked by the test suite
Every code block in the guide is parsed, checked and type-checked on each release.