i18n Routing
@domphy/app handles locale-prefix URL routing via middleware. Translations and locale state use @domphy/i18n.
Setup
import { createApp, createI18nMiddleware, getLocale } from "@domphy/app"
import { createI18n } from "@domphy/i18n"
import en from "./locales/en.json"
import vi from "./locales/vi.json"
export const i18nOpts = {
locales: ["en", "vi"] as const,
defaultLocale: "en" as const,
} satisfies { locales: readonly string[]; defaultLocale: string }
export const i18n = createI18n({
globalKey: "__myapp_i18n__",
namespace: "app",
locales: { en, vi },
defaultLocale: "en",
})
const app = createApp(routes, {
middleware: [createI18nMiddleware(i18nOpts)],
})How It Works
createI18nMiddleware intercepts every navigation before routing:
| URL | Action | Route rendered |
|---|---|---|
/vi/about | rewrite to /about | about route with locale "vi" |
/en/about | rewrite to /about | about route with locale "en" |
/about | pass through | about route with defaultLocale ("en") |
Your page components and loaders don't need to know about the locale prefix — they always receive the bare pathname.
Reading the Active Locale
Use getLocale(context, opts) in any loader or page to read the locale from the original URL:
{
path: "about",
loader: async (ctx) => {
const locale = getLocale(ctx, i18nOpts) // "en" | "vi"
await i18n.initI18n(locale)
return locale
},
page: (ctx) => ({
h1: (l) => i18n.t(l, "about.title"),
}),
}getLocale reads the first URL segment from context.url (the original URL before the middleware rewrite) and matches it against options.locales. Falls back to defaultLocale when no prefix is present.
Prefix Default Locale
By default, the default locale has no URL prefix (/about → "en"). To prefix every locale (Next.js locales with defaultLocale):
createI18nMiddleware({ ...i18nOpts, prefixDefault: true })With prefixDefault: true, a bare /about redirects to /en/about.
Options
createI18nMiddleware(options)
| Option | Type | Default | Description |
|---|---|---|---|
locales | readonly TLocale[] | required | All supported locale codes |
defaultLocale | TLocale | required | Locale to use when no prefix is detected |
prefixDefault | boolean | false | Whether to prefix the default locale in the URL |
Returns a Middleware to pass in createApp(routes, { middleware: [...] }).
getLocale(context, options)
| Param | Type | Description |
|---|---|---|
context | { url: string } | Any loader or route context |
options.locales | readonly TLocale[] | Must match the middleware options |
options.defaultLocale | TLocale | Fallback when no prefix detected |
Returns TLocale.
Switching Locale
Switching locale is a navigation to the locale-prefixed URL:
function LocaleSwitcher() {
const router = getRouter()
return {
div: [
{
button: "English",
onClick: () => router.push(`/en${router.state.get("pathname")}`),
},
{
button: "Tiếng Việt",
onClick: () => router.push(`/vi${router.state.get("pathname")}`),
},
],
}
}