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:
Maofeng
2026-07-29 21:23:00 +08:00
parent 011e7d8115
commit 9e3f1d6b59
39 changed files with 2897 additions and 0 deletions
@@ -0,0 +1,77 @@
// @vitest-environment jsdom
import * as React from "react"
import { render, screen } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { I18nProvider } from "./i18n-provider"
import { useLocale, useLocales, useMessage, useTranslate } from "./hooks"
import { Translate } from "./translate"
const catalogs = {
en: {
greeting: "Hello",
},
"zh-Hans": {
greeting: "你好",
},
}
function HookProbe() {
const locale = useLocale()
const locales = useLocales()
const message = useMessage("greeting")
const translate = useTranslate()
return (
<output>
{locale}|{locales.map((item) => item.locale).join(",")}|{message}|
{translate("greeting")}
</output>
)
}
describe("i18n runtime", () => {
it("provides locale information and reactive translation helpers", () => {
render(
<I18nProvider
catalogs={catalogs}
locale="zh-Hans"
locales={[
{ label: "English", locale: "en" },
{ label: "简体中文", locale: "zh-Hans" },
]}
>
<HookProbe />
</I18nProvider>
)
expect(screen.getByText("zh-Hans|en,zh-Hans|你好|你好")).toBeTruthy()
})
it("wraps Lingui Trans without adding a DOM element", () => {
const { container } = render(
<I18nProvider catalogs={catalogs} locale="en">
<p>
<Translate id="greeting" message="Fallback" />
</p>
</I18nProvider>
)
expect(container.innerHTML).toBe("<p>Hello</p>")
})
it("infers right-to-left locale direction", () => {
function DirectionProbe() {
return <span>{useLocales()[0]?.direction}</span>
}
render(
<I18nProvider locale="ar" locales={["ar"]}>
<DirectionProbe />
</I18nProvider>
)
expect(screen.getByText("rtl")).toBeTruthy()
})
})