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,60 @@
// @vitest-environment jsdom
import * as React from "react"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { describe, expect, it, vi } from "vitest"
import { MessagePanel } from "./message-panel"
import type { DevtoolMessage, MessageRepository } from "./types"
const message: DevtoolMessage = {
catalog: "messages",
comments: ["Navigation title"],
id: "navigation.home",
missing: true,
obsolete: false,
origins: [{ file: "src/navigation.tsx", line: 12 }],
source: "Home",
translation: "",
}
describe("MessagePanel", () => {
it("loads, filters, and saves messages through the repository", async () => {
const updateMessage = vi.fn(async (input) => ({
...message,
missing: false,
translation: input.translation,
}))
const repository: MessageRepository = {
getMessages: vi.fn(async () => [message]),
updateMessage,
}
render(<MessagePanel locale="zh-Hans" repository={repository} />)
expect(await screen.findByText("navigation.home")).toBeTruthy()
const textarea = screen.getByLabelText(
"navigation.home translation for zh-Hans"
)
fireEvent.change(textarea, { target: { value: "首页" } })
fireEvent.click(screen.getByRole("button", { name: "Save" }))
await waitFor(() => {
expect(updateMessage).toHaveBeenCalledWith({
catalog: "messages",
id: "navigation.home",
locale: "zh-Hans",
translation: "首页",
})
})
expect(await screen.findByText("Saved")).toBeTruthy()
fireEvent.change(screen.getByPlaceholderText("Search messages"), {
target: { value: "does not exist" },
})
expect(
screen.getByText("No messages match the current filters.")
).toBeTruthy()
})
})