feat(i18n): expand catalog runtime and developer tooling

- add Intl-backed locale definitions, formatters, catalog loaders, and runtime hooks\n- compile hashed production catalogs from package-scoped catalog sources\n- drive Lingui extraction and compilation directly from project JSON config\n- redesign the devtool with localized controls, draggable persistence, settings, navigation, copy actions, ANSI output, and fullscreen support\n- extend repository APIs, CLI coverage, documentation, and runtime tests
This commit is contained in:
Maofeng
2026-07-30 14:47:42 +08:00
parent 0d0aad1d47
commit 0768e8fa29
55 changed files with 7553 additions and 495 deletions
+103 -23
View File
@@ -1,8 +1,9 @@
# @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.
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
@@ -30,19 +31,29 @@ Create `i18n.config.json` in that application:
"sourceLocale": "en",
"locales": ["en", "zh-Hans"],
"catalogPath": "src/locales/{locale}/messages",
"include": ["src"],
"catalogSources": [
"@workspace/ui/locales/{locale}",
"@workspace/blocks/navigation/locales/{locale}"
],
"include": [
"src",
"../../packages/ui/src",
"../../packages/blocks/src/blocks/navigation"
],
"exclude": ["**/*.test.{ts,tsx}"]
}
```
Then expose it as a Lingui configuration:
`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`.
```ts
import project from "./i18n.config.json" with { type: "json" }
import { createLinguiConfig } from "@workspace/i18n/lingui-config"
export default createLinguiConfig(project)
```
`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:
@@ -57,12 +68,14 @@ export default defineConfig({
## Runtime
Compiled Lingui catalogs are passed to the provider synchronously, so the
first client render and SSR render use the same locale.
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.
```tsx
import { messages as en } from "@/locales/en/messages"
import { messages as zhHans } from "@/locales/zh-Hans/messages"
import { use } from "react"
import { loadMessageCatalog } from "@workspace/i18n/catalogs"
import {
I18nProvider,
Translate,
@@ -73,7 +86,12 @@ import {
} from "@workspace/i18n"
import { I18nDevtool } from "@workspace/i18n/devtool"
function Root({ children, locale }: React.PropsWithChildren<{ locale: string }>) {
function Root({
children,
locale,
}: React.PropsWithChildren<{ locale: string }>) {
const messages = use(loadMessageCatalog(locale))
return (
<I18nProvider
locale={locale}
@@ -81,7 +99,7 @@ function Root({ children, locale }: React.PropsWithChildren<{ locale: string }>)
{ locale: "en", label: "English" },
{ locale: "zh-Hans", label: "简体中文" },
]}
catalogs={{ en, "zh-Hans": zhHans }}
catalogs={{ [locale]: messages }}
>
{children}
{import.meta.env.DEV && <I18nDevtool dark={"ui\\:dark"} />}
@@ -108,13 +126,54 @@ function Greeting() {
}
```
`@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:
```ts
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 Lingui configuration points `runtimeConfigModule.Trans` at
`Translate`, so explicit `<Translate id="…" message="…" />` usages are found
by `lingui extract`.
The generated in-memory Lingui configuration points
`runtimeConfigModule.Trans` at `Translate`, so explicit
`<Translate id="…" message="…" />` usages are found by `lingui extract`.
## Devtool
@@ -132,11 +191,31 @@ catalog reads, updates, extraction, and compilation.
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.
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:
```tsx
<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
@@ -168,5 +247,6 @@ 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.
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.