Add a DOM-free LocalizedText component for MessageDescriptor values and export it from the runtime entry point.\n\nAlso expose provider detection so packages with a standalone fallback provider can avoid invoking Lingui hooks outside a provider. Cover localized rendering without an extra wrapper node.
@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:
- merges configured package catalogs and the application catalog per locale;
- creates one deterministic shared schema from semantic message IDs;
- replaces those IDs in production JavaScript with 10-character SHA-256 Base64URL keys;
- emits one cacheable
assets/i18n/<locale>-<content-hash>.jsonfile 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, updatesi18n.config.json, and asks Lingui to extract its catalog.extractextracts application messages into its catalogs.compilecompiles application catalogs to TypeScript modules.uistarts 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.