Files
simple-react-app-kit/packages/i18n
Maofeng d36028d67b build: migrate workspace tooling to Oxc
Replace Turbo task orchestration with Bun workspace scripts and remove the Prettier configuration. Add Oxlint and Oxfmt project configuration, including Tailwind class sorting, generated-file exclusions, and unified check/fix commands. Apply the new formatter and resolve Hook dependency errors surfaced by Oxlint.
2026-07-30 15:50:51 +08:00
..

@workspace/i18n

Reusable Lingui runtime, Devtool, Vite plugin, and CLI. Every consuming application owns its i18n.config.json and application catalogs. Reusable packages may additionally ship built-in catalogs, which the application can compose and override.

Consumer setup

Install the package in an application and add scripts that invoke its CLI:

{
  "dependencies": {
    "@lingui/core": "^6",
    "@workspace/i18n": "workspace:*"
  },
  "scripts": {
    "i18n": "workspace-i18n",
    "i18n:compile": "workspace-i18n compile",
    "i18n:extract": "workspace-i18n extract",
    "i18n:ui": "workspace-i18n ui"
  }
}

Create i18n.config.json in that application:

{
  "sourceLocale": "en",
  "locales": ["en", "zh-Hans"],
  "catalogPath": "src/locales/{locale}/messages",
  "catalogSources": [
    "@workspace/ui/locales/{locale}",
    "@workspace/blocks/navigation/locales/{locale}"
  ],
  "include": [
    "src",
    "../../packages/ui/src",
    "../../packages/blocks/src/blocks/navigation"
  ],
  "exclude": ["**/*.test.{ts,tsx}"]
}

i18n.config.json is the application's only persistent i18n configuration. The package converts it to Lingui's normalized configuration in memory. CLI commands create a temporary Lingui module only for the lifetime of the child process, so consuming applications do not need a lingui.config.ts.

catalogSources lists reusable catalogs in merge order. The application catalog from catalogPath is always merged last, so translations customized in the application override package defaults. Keep dependency source folders in include when the application should be able to edit those overrides in Message Studio.

Mount the API plugin in the consuming application's Vite configuration:

import { i18n } from "@workspace/i18n/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [i18n()],
})

Runtime

The Vite plugin exposes the merged catalog through @workspace/i18n/catalogs. Development loads source modules directly. A production build emits one content-hashed JSON asset per locale and embeds the matching compact catalog in the SSR bundle.

import { use } from "react"
import { loadMessageCatalog } from "@workspace/i18n/catalogs"
import {
  I18nProvider,
  Translate,
  useLocale,
  useLocales,
  useMessage,
  useTranslate,
} from "@workspace/i18n"
import { I18nDevtool } from "@workspace/i18n/devtool"

function Root({
  children,
  locale,
}: React.PropsWithChildren<{ locale: string }>) {
  const messages = use(loadMessageCatalog(locale))

  return (
    <I18nProvider
      locale={locale}
      locales={[
        { locale: "en", label: "English" },
        { locale: "zh-Hans", label: "简体中文" },
      ]}
      catalogs={{ [locale]: messages }}
    >
      {children}
      {import.meta.env.DEV && <I18nDevtool dark={"ui\\:dark"} />}
    </I18nProvider>
  )
}

function Greeting() {
  const locale = useLocale()
  const locales = useLocales()
  const title = useMessage("dashboard.title")
  const translate = useTranslate()

  return (
    <>
      <Translate
        id="welcome"
        message="Welcome, {name}"
        values={{ name: "Ada" }}
      />
      <span>{title}</span>
    </>
  )
}

@workspace/ui and every @workspace/blocks block provide de, en, es, fr, ja, ko, zh-Hans, and zh-Hant as independent locale subpaths. Import only the blocks and languages enabled by the application:

import {
  calendarLocale,
  messages as uiJapanese,
} from "@workspace/ui/locales/ja"
import { messages as navigationJapanese } from "@workspace/blocks/navigation/locales/ja"

The UI root locales entry and each block-specific locales entry export metadata and types only. They do not statically import catalogs. For example, using @workspace/blocks/navigation and @workspace/blocks/navigation/locales/ja does not import appearance, chats, layout, notifications, or their translations. The UI locale entry also exposes its matching react-day-picker calendarLocale.

Catalog sources are merged from left to right, so application catalogs should be last: they can override a package translation without forking that package. An application therefore only needs to translate its own messages unless it intentionally customizes package copy.

Production catalogs

During vite build, the plugin:

  1. merges configured package catalogs and the application catalog per locale;
  2. creates one deterministic shared schema from semantic message IDs;
  3. replaces those IDs in production JavaScript with 10-character SHA-256 Base64URL keys;
  4. emits one cacheable assets/i18n/<locale>-<content-hash>.json file for each locale.

The private semantic-to-compact schema is written to .i18n/schema.json for diagnostics and is ignored by Git. It is not shipped to the browser. Because every locale uses the same compact key, changing languages only loads the selected locale asset; package catalogs are not duplicated in the main application bundle.

Translate is a thin wrapper around Lingui's runtime Trans component. It does not add a DOM wrapper and retains values, components, formats, component, and render.

The generated in-memory Lingui configuration points runtimeConfigModule.Trans at Translate, so explicit <Translate id="…" message="…" /> usages are found by lingui extract.

Devtool

I18nDevtool must be mounted inside I18nProvider. It reads the active locale and available locales from the provider, while the Vite i18n() plugin serves catalog reads, updates, extraction, and compilation.

<I18nProvider locale="zh-Hans" locales={["en", "zh-Hans"]}>
  <App />
  {import.meta.env.DEV && <I18nDevtool dark={"ui\\:dark"} />}
</I18nProvider>

The dark value can be a class name such as ui\:dark, or a CSS selector such as [data-theme="dark"]. The Devtool observes document theme changes and renders through a portal, so application overflow and stacking contexts do not clip it. Its settings panel can override the theme with auto, light, or dark, and place the floating panel at the left, center, or right of the viewport. These preferences and the draggable trigger position persist across page reloads.

The Devtool control-panel language is independent from the message locale. Pass a BCP 47 language tag with locale to select it explicitly:

<I18nDevtool locale="zh-Hans" dark={"ui\\:dark"} />

When locale is omitted, the Devtool selects the first supported entry from navigator.languages and falls back to English. Built-in control-panel locales are English, German, Spanish, French, Japanese, Korean, Simplified Chinese, and Traditional Chinese.

Lower-level MessagePanel, MessageRepositoryProvider, and repository types remain available from @workspace/i18n/devtool.

The source locale remains available in the target-locale selector. Application messages are read-only there because their source text belongs in source code; messages extracted from dependency packages remain editable, and their source locale translations are stored as application-level overrides.

CLI

Run inside the consuming application so the CLI discovers that application's i18n.config.json:

bun run i18n new ja
bun run i18n extract
bun run i18n compile
bun run i18n ui

Commands:

  • new <locale> validates and canonicalizes a BCP-47 locale, updates i18n.config.json, and asks Lingui to extract its catalog.
  • extract extracts application messages into its catalogs.
  • compile compiles application catalogs to TypeScript modules.
  • ui starts Message Studio. It reads and writes PO catalogs through Lingui's catalog API, and provides Extract and Compile actions.

All commands accept --project <path> when invoked outside the application.

Catalog workflow

cd apps/web
bun run i18n:extract
bun run i18n:compile

The application's i18n.config.json is the single machine-editable source of truth. Runtime catalog access, Devtool actions, extraction, and compilation all derive their Lingui configuration from it.