Skip to content

Internationalization (i18n)

The console supports English (en) and Chinese (zh) via next-intl v4. URLs do not include a locale prefix (localePrefix: "never").


Configuration

Routing (i18n/routing.ts)

export const routing = defineRouting({
  locales: ["zh", "en"],
  defaultLocale: "zh",
  localePrefix: "never",
});

Locale resolved by middleware / proxy.ts from cookie or Accept-Language.

Request config (i18n/request.ts)

Loads messages at request time:

  1. Read messages/${locale}.json via readFileSync (avoids Turbopack dynamic import hang)
  2. Merge all messages/fragments/*.${locale}.json with deepMerge
  3. Return { locale, messages } to NextIntlClientProvider

Message file layout

frontend/messages/
├── en.json              # Shared / shell strings
├── zh.json
└── fragments/
    ├── qa.en.json       # namespace: "qa"
    ├── qa.zh.json
    ├── ingest.en.json
    ├── kb.en.json
    ├── health.en.json
    └── …

Fragment convention

Each fragment file is namespaced at the top level:

{
  "qa": {
    "composer": {
      "placeholder": "Ask anything…"
    },
    "error": {
      "query": "Query failed"
    }
  }
}

deepMerge recurses into existing keys — fragments extend base messages without clobbering siblings.

Adding a new page namespace

  1. Create messages/fragments/mypage.en.json + .zh.json
  2. No change to request.ts — auto-discovered by suffix scan
  3. Use useTranslations("mypage") in components

Usage in components

Client components

import { useTranslations } from "next-intl";

function Composer() {
  const t = useTranslations("qa");
  return <span>{t("composer.placeholder")}</span>;
}

Nested namespaces

const t = useTranslations("qa.sources");
const tRail = useTranslations("qa.rail");

Server components

import { getTranslations } from "next-intl/server";

const t = await getTranslations("kb");

Locale in App Router

app/[locale]/layout.tsx:

  • generateStaticParams() → both locales pre-rendered
  • setRequestLocale(locale) in layout for static optimization
  • Invalid locale → notFound()

<html lang={locale}> set for accessibility and SEO.


Fragment inventory (Q&A example)

messages/fragments/qa.en.json covers:

Namespace segment UI area
qa.composer Prompt, modes, attachments
qa.sources Evidence rail
qa.rail Structure / preview tabs
qa.history Session drawer
qa.scope Scope filter drawer
qa.error Toast messages

Parallel qa.zh.json must keep key parity — missing keys fall back to key path in dev.


Date / number formatting

Use next-intl formatters when displaying locale-sensitive values:

import { useFormatter } from "next-intl";
const format = useFormatter();
format.dateTime(new Date(session.updated_at));

History grouping (history-utils.ts) uses calendar math in UTC — UI labels come from qa.history.* keys.


API content language

Backend LLM answers follow model defaults (Chinese/English mixed possible). UI strings are fully localized; citation content comes from corpus language, not UI locale.


Testing locales

Switch locale via:

  • User preference store (prefsStore) if wired
  • Cookie set by middleware
  • Browser Accept-Language on first visit