Files
Maofeng 9e3f1d6b59 feat(i18n): add Lingui runtime and development tooling
- add SSR-safe I18nProvider, Translate, locale hooks, catalog activation, and typed runtime APIs

- add consumer-owned Lingui configuration helpers and a CLI for locale creation, extraction, compilation, and project discovery

- add catalog read/write services plus a Vite development API plugin scoped to each consuming application

- add the portal-based I18nDevtool, message editor, dark-theme tracking, and standalone Message Studio

- document the consumer workflow and cover runtime, configuration, catalog, CLI, and Devtool behavior with tests
2026-07-29 21:23:00 +08:00

4.4 KiB

@workspace/i18n

Reusable Lingui runtime, Devtool, Vite plugin, and CLI. This package does not own application locales or catalogs: every consuming application keeps its own i18n.config.json, lingui.config.ts, and generated message files.

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",
  "include": ["src"],
  "exclude": ["**/*.test.{ts,tsx}"]
}

Then expose it as a Lingui configuration:

import project from "./i18n.config.json" with { type: "json" }
import { createLinguiConfig } from "@workspace/i18n/lingui-config"

export default createLinguiConfig(project)

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

Compiled Lingui catalogs are passed to the provider synchronously, so the first client render and SSR render use the same locale.

import { messages as en } from "@/locales/en/messages"
import { messages as zhHans } from "@/locales/zh-Hans/messages"
import {
  I18nProvider,
  Translate,
  useLocale,
  useLocales,
  useMessage,
  useTranslate,
} from "@workspace/i18n"
import { I18nDevtool } from "@workspace/i18n/devtool"

function Root({ children, locale }: React.PropsWithChildren<{ locale: string }>) {
  return (
    <I18nProvider
      locale={locale}
      locales={[
        { locale: "en", label: "English" },
        { locale: "zh-Hans", label: "简体中文" },
      ]}
      catalogs={{ en, "zh-Hans": zhHans }}
    >
      {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>
    </>
  )
}

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 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.

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

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 machine-editable source of truth. Its lingui.config.ts maps that manifest to Lingui's configuration format.