Locale Routing
The active locale lives in the URL path. Routing is built into the core @opentf/afw router, so a single locale-agnostic route table serves every language.
prefix_except_default
The default locale is served at the bare path; every other locale is prefixed:
| Route | en (default) | fr | ja |
|---|---|---|---|
/ | / | /fr | /ja |
/pricing | /pricing | /fr/pricing | /ja/pricing |
/blog/hello | /blog/hello | /fr/blog/hello | /ja/blog/hello |
Define the locales in afw.config.js:
export default { i18n: { locales: ["en", "fr", "ja"], defaultLocale: "en", } };
Pass that configuration to mountApp in your browser entry. The compiler plugin does not read it automatically:
import { mountApp } from "@opentf/afw"; import { pages, guard } from "@afw/routes"; import config from "./afw.config.js"; mountApp({ pages, guard, i18n: config.i18n, target: document.getElementById("app") });
Use the same locales in createI18n and pass i18n: config.i18n to your prerender helper or request renderer.
There is no in-place locale toggle — to change language you navigate to the localized URL. Because each URL is exactly one locale, every page is independently cacheable, SEO-friendly, and free of hydration locale mismatches.
Reading the locale
router.locale is reactive — read it in any binding and it updates on navigation:
import { router } from "@opentf/afw"; <span class="badge">{router.locale}</span>
Localized links
<Link> automatically keeps navigation in the active locale — no per-link work:
import { Link } from "@opentf/afw"; // On /fr/… this renders href="/fr/pricing"; on the default locale, "/pricing". <Link href="/pricing">Pricing</Link>
Need to build a localized path yourself (e.g. a language switcher)? Use localizePath — the default locale stays bare, others get a prefix, and any existing prefix is replaced:
import { localizePath } from "@opentf/afw"; localizePath("/pricing", "fr"); // "/fr/pricing" localizePath("/pricing", "en"); // "/pricing" (default → bare) localizePath("/fr/pricing", "ja"); // "/ja/pricing" (re-pointed)
A language switcher, then, is just links to the current path under each locale:
<Link href={localizePath(router.pathname, "fr")}>Français</Link> <Link href={localizePath(router.pathname, "ja")}>日本語</Link>
Static generation (SSG)
The prerender helper, supplied with the i18n configuration, renders every route once per locale — the default bare, others under their prefix — each with the right <html lang> and hreflang alternates for SEO:
dist/ ├── index.html → en ├── pricing/index.html → en ├── fr/index.html → fr ├── fr/pricing/index.html → fr └── ja/… → ja
Server rendering (SSR) + detection
An app-provided request server can derive the locale from the URL prefix and use Accept-Language or an afw_locale cookie to redirect a bare path to a non-default locale. This requires server wiring; the native compiler plugin does not install locale-detection middleware automatically.
Programmatic control
In a routed app the locale comes from the URL. For previews, tests, or a custom flow, setLocale sets router.locale directly without navigating:
import { setLocale } from "@opentf/afw"; setLocale("fr"); // router.locale → "fr"; t()/fmt re-render