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
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# @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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceLocale": "en",
|
||||
"locales": ["en", "zh-Hans"],
|
||||
"catalogPath": "src/locales/{locale}/messages",
|
||||
"include": ["src"],
|
||||
"exclude": ["**/*.test.{ts,tsx}"]
|
||||
}
|
||||
```
|
||||
|
||||
Then expose it as a Lingui configuration:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
<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`:
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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.
|
||||
Reference in New Issue
Block a user