Skip to content

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.