Internationalization
Internationalization#
Otok core stays free of i18n. Use @kamod-ch/otok-i18n for routing, SSR, islands, and metadata.
Plugin setup#
import { defineConfig } from "@kamod-ch/otok";
import i18n from "@kamod-ch/otok-i18n";
export default defineConfig({
plugins: [
i18n({
locales: ["de", "en", "fr"],
defaultLocale: "de",
routing: "prefix-except-default",
fallbackLocale: "en",
messages: {
de: () => import("./locales/de.json"),
en: () => import("./locales/en.json"),
fr: () => import("./locales/fr.json"),
},
}),
],
});
Add optional [[lang]] (or [[locale]]) route segments for localized URLs:
src/app/routes/[[lang]]/about.tsx → /about and /:lang/about
The Vite plugin treats [[lang]] / [[locale]] as first-class locale optional segments and sets localeParam on the matching route variant so i18n plugins can detect prefixed paths.
Locale resolution#
Priority: URL/domain → cookie → Accept-Language → defaultLocale.
routing |
URLs |
|---|---|
prefix-except-default |
/about, /en/about |
prefix |
/de/about, /en/about |
domain |
example.ch, example.fr |
none |
No locale prefix |
Loaders and islands#
import { defineLoader, serializeI18n } from "@kamod-ch/otok-i18n/loader";
import { i18nHead } from "@kamod-ch/otok-i18n";
export const loader = defineLoader(({ i18n, hono }) => ({
title: i18n.t("about.title"),
i18n: serializeI18n(hono),
}));
export const head = ({ data }) => i18nHead(data.i18n.locale, { title: data.title });
import { useI18n } from "@kamod-ch/otok-i18n/client";
export function Welcome() {
const { t, formatCurrency } = useI18n();
return (
<>
<h1>{t("dashboard.welcome")}</h1>
<p>{formatCurrency(29, "CHF")}</p>
</>
);
}
Pass data.i18n from the loader into <I18nProvider> so SSR and hydration share the same locale and messages.
Metadata and sitemap#
i18nHead() adds <html lang>, canonical URLs, and hreflang alternates when origin is set.
Use createLocalizedSitemapEntries + renderSitemapXml from @kamod-ch/otok-i18n/sitemap for multilingual sitemaps.
Example app#
examples/i18n-trilingual — German, English, and French with islands, locale switcher, and sitemap.
Package docs#
See the package README for routing helpers, pluralization, and the legacy flat-catalog API.
Recommendations#
- Keep message catalogs at the app layer (JSON or lazy
import()). - Prefer URL locale for shareable links; cookies persist user choice.
- Use
serializeI18n(hono)— only the active locale ships to the client. - Register
i18n()before plugins that depend on locale in middleware order.