LangENKO

Internationalisation (i18n)

Add a `[lang]` segment, translation modules, and a locale switcher — the same pattern this docs site uses across 23 languages.

since v0.22
On this page

Internationalisation

TL;DR — Move pages under app/[lang]/, register locales and defaultLocale in a translation module, return them all from generateStaticParams, and read params.lang per page. Mandu prerenders one HTML per locale.

Why

i18n is the place agents drop bugs — hardcoded strings, missing locale on a sub-route, broken RTL. The Mandu pattern keeps locale in the URL and translation strings in typed modules, so an agent can search for t.nav.docs and find every place a copy lives.

How

1. Translation modules

src/shared/utils/client/i18n/
├── types.ts            # Translation interface (typed schema)
├── translations/
│   ├── en.ts           # default locale
│   ├── ko.ts
│   ├── ja.ts
│   └── ...
└── index.ts            # locales[], defaultLocale, getTranslation(), isValidLocale(), isRtl()
// types.ts
export interface Translation {
  nav: { docs: string; getStarted: string };
  hero: { title: string; subtitle: string };
}

// translations/en.ts
import type { Translation } from "../types";
export const en: Translation = {
  nav: { docs: "Docs", getStarted: "Get Started" },
  hero: { title: "Build with agents", subtitle: "..." },
};

2. Route under [lang]

// app/[lang]/page.tsx
import { getTranslation, isValidLocale, defaultLocale, locales } from "@/shared/utils/client/i18n";

export function generateStaticParams() {
  return locales.map((lang) => ({ lang }));
}

export default function HomePage({ params }: { params: { lang: string } }) {
  if (!isValidLocale(params.lang)) return <NotFound />;
  const t = getTranslation(params.lang);
  return <h1>{t.hero.title}</h1>;
}

3. Locale-aware layout (RTL + fonts)

// app/[lang]/layout.tsx
import { isValidLocale, defaultLocale, isRtl } from "@/shared/utils/client/i18n";

export default function LangLayout({ children, params }) {
  const lang = isValidLocale(params.lang) ? params.lang : defaultLocale;
  return <div dir={isRtl(lang) ? "rtl" : "ltr"}>{children}</div>;
}

🤖 Agent Prompt

🤖 Agent Prompt — Add i18n
Add i18n to my Mandu app:

1. Create `src/shared/utils/client/i18n/types.ts` with a `Translation`
   interface, then `translations/<locale>.ts` per locale exporting a
   typed Translation. Add an `index.ts` that re-exports `locales`
   (string[]), `defaultLocale`, `getTranslation(lang)`,
   `isValidLocale(lang)`, `isRtl(lang)` (true for ar/he/ur/fa).

2. Move pages that need localisation under `app/[lang]/...`. In each
   page: validate via `isValidLocale(params.lang)`; if invalid, render
   a 404 fallback. Read copy as `getTranslation(params.lang)`.

3. Add `export function generateStaticParams() { return locales.map(
   (lang) => ({ lang })); }` so the static build emits every locale.

4. In `app/[lang]/layout.tsx`, set `dir={isRtl(lang) ? 'rtl' : 'ltr'}`
   on the root wrapper.

Required invariants:
- params.lang is the only source of truth for locale.
- Never import a locale module directly in a page; always go through
  getTranslation(lang).
- generateStaticParams must enumerate every entry of `locales`.
- dir='rtl' must be applied for ar, he, ur, fa.

After writing the files, run `bun run guard`, `bun run check`, and
verify a couple of locale URLs in the dev server.

Pitfalls

  • Don't hardcode strings. Every visible text goes through getTranslation(lang). Mandu Guard flags string literals in JSX under app/[lang]/.
  • Validate params.lang first. Otherwise a malformed URL like /zz/about throws inside getTranslation.
  • generateStaticParams is not optional. Without it, only the default locale prerenders.
  • RTL is more than dir. Some Tailwind classes (e.g. ml-2) are direction-bound; prefer start-2 / end-2 for RTL-safe spacing.

For Agents

AI hint

Wrap localised pages under `app/[lang]/` and pass `params.lang` to a `getTranslation(lang)` helper that returns a typed Translation object. Validate `lang` against `locales` and fall back to `defaultLocale`. Set `dir="rtl"` for ar/he/ur. Use named export `generateStaticParams` to prerender every locale.

Invariants
  • Localised pages live under `app/[lang]/...` — `params.lang` is the only source of truth for locale
  • Validate `params.lang` with `isValidLocale(params.lang)` and fall back to `defaultLocale` for unknown values
  • Translation modules export a typed `Translation` object; never import locale strings inline
  • `dir="rtl"` must be set on the layout's root element when `isRtl(lang)` is true (ar, he, ur, fa)
  • `generateStaticParams` must return every locale so the static build emits one HTML per language
Guard scope
i18n-route