feat(i18n): standardize bilingual catalogs and tooling

This commit is contained in:
Maofeng
2026-09-20 15:52:31 +08:00
parent ea00a71112
commit 6cd981d6a2
92 changed files with 685 additions and 1986 deletions
+35 -29
View File
@@ -26,33 +26,39 @@ async function createProject() {
catalogPath: "app/{locale}/messages",
catalogSources: ["./dependency/{locale}.js"],
include: ["src"],
locales: ["en", "zh-Hans"],
sourceLocale: "en",
locales: ["en-US", "zh-Hans"],
sourceLocale: "en-US",
}
temporaryDirectories.push(projectRoot)
for (const locale of project.locales) {
const dependencyDirectory = join(projectRoot, "dependency")
const applicationDirectory = join(projectRoot, "app", locale)
await Promise.all(
project.locales.map(async (locale) => {
const dependencyDirectory = join(projectRoot, "dependency")
const applicationDirectory = join(projectRoot, "app", locale)
await mkdir(dependencyDirectory, { recursive: true })
await mkdir(applicationDirectory, { recursive: true })
await writeFile(
join(dependencyDirectory, `${locale}.js`),
`export const messages = ${JSON.stringify({
dependency: `${locale}: dependency`,
overridden: `${locale}: dependency`,
})}\n`
)
await writeFile(
join(applicationDirectory, "messages.js"),
`export const messages = ${JSON.stringify({
application: `${locale}: application`,
overridden: `${locale}: application`,
})}\n`
)
}
await Promise.all([
mkdir(dependencyDirectory, { recursive: true }),
mkdir(applicationDirectory, { recursive: true }),
])
await Promise.all([
writeFile(
join(dependencyDirectory, `${locale}.js`),
`export const messages = ${JSON.stringify({
dependency: `${locale}: dependency`,
overridden: `${locale}: dependency`,
})}\n`
),
writeFile(
join(applicationDirectory, "messages.js"),
`export const messages = ${JSON.stringify({
application: `${locale}: application`,
overridden: `${locale}: application`,
})}\n`
),
])
})
)
return { project, projectRoot }
}
@@ -63,12 +69,12 @@ describe("production catalog sources", () => {
const filenames = await resolveCatalogSourceFilenames(
project,
projectRoot,
"en"
"en-US"
)
expect(filenames).toEqual([
join(projectRoot, "dependency", "en.js"),
join(projectRoot, "app", "en", "messages.js"),
join(projectRoot, "dependency", "en-US.js"),
join(projectRoot, "app", "en-US", "messages.js"),
])
})
@@ -76,10 +82,10 @@ describe("production catalog sources", () => {
const { project, projectRoot } = await createProject()
const { catalogs } = await loadProjectCatalogs(project, projectRoot)
expect(catalogs.en).toEqual({
application: "en: application",
dependency: "en: dependency",
overridden: "en: application",
expect(catalogs["en-US"]).toEqual({
application: "en-US: application",
dependency: "en-US: dependency",
overridden: "en-US: application",
})
expect(catalogs["zh-Hans"]).toEqual({
application: "zh-Hans: application",
+3 -5
View File
@@ -30,12 +30,10 @@ export async function resolveCompiledCatalogFilename(
? pathname
: resolve(projectRoot, pathname)
const candidates = [baseFilename, `${baseFilename}.ts`, `${baseFilename}.js`]
const availability = await Promise.all(candidates.map(fileExists))
const candidate = candidates.find((_, index) => availability[index])
for (const candidate of candidates) {
if (await fileExists(candidate)) {
return candidate
}
}
if (candidate) return candidate
throw new Error(
`Compiled i18n catalog not found for ${locale}: ${baseFilename}.{ts,js}. Run the i18n compile command before building.`
@@ -23,7 +23,8 @@ export function createMessageIdSchema(
): MessageIdSchema {
const ids = Array.from(
new Set(Object.values(catalogs).flatMap((catalog) => Object.keys(catalog)))
).sort()
)
ids.sort()
const messages: Record<string, string> = {}
const idsByHash = new Map<string, string>()
+89 -90
View File
@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http"
import type { ViteDevServer } from "vite"
import type { Connect, ViteDevServer } from "vite"
import type { UpdateMessageInput } from "../devtool/types"
import { CatalogService } from "./catalog-service.ts"
@@ -43,94 +43,93 @@ export function configureI18nApi(
server: ViteDevServer,
projectFilename: string
) {
server.middlewares.use(async (request, response, next) => {
if (!request.url) {
next()
return
}
const url = new URL(request.url, "http://localhost")
if (!url.pathname.startsWith(API_PREFIX)) {
next()
return
}
try {
if (
request.method === "GET" &&
url.pathname === `${API_PREFIX}/project`
) {
const project = await readProject(projectFilename)
sendJson(response, 200, project)
return
}
if (
request.method === "GET" &&
url.pathname === `${API_PREFIX}/messages`
) {
const locale = url.searchParams.get("locale")
if (!locale) {
sendJson(response, 400, { error: "locale is required." })
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getMessages(locale))
return
}
if (
request.method === "GET" &&
url.pathname === `${API_PREFIX}/packages`
) {
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getPackages())
return
}
if (
request.method === "PUT" &&
url.pathname === `${API_PREFIX}/messages`
) {
const input = await readJsonBody(request)
if (!isUpdateMessageInput(input)) {
sendJson(response, 400, {
error: "The message update payload is invalid.",
})
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.updateMessage(input))
return
}
if (
request.method === "POST" &&
url.pathname.startsWith(`${API_PREFIX}/actions/`)
) {
const action = url.pathname.slice(`${API_PREFIX}/actions/`.length)
if (action !== "extract" && action !== "compile") {
sendJson(response, 404, { error: "Unknown action." })
return
}
const result = await runProjectLinguiCommand(projectFilename, action)
sendJson(response, 200, result)
return
}
sendJson(response, 404, { error: "Not found." })
} catch (error) {
sendJson(response, 500, {
error:
error instanceof Error ? error.message : "An unknown error occurred.",
})
}
server.middlewares.use((request, response, next) => {
void handleI18nApiRequest(request, response, next, projectFilename).catch(
next
)
})
}
async function handleI18nApiRequest(
request: IncomingMessage,
response: ServerResponse,
next: Connect.NextFunction,
projectFilename: string
) {
if (!request.url) {
next()
return
}
const url = new URL(request.url, "http://localhost")
if (!url.pathname.startsWith(API_PREFIX)) {
next()
return
}
try {
if (request.method === "GET" && url.pathname === `${API_PREFIX}/project`) {
const project = await readProject(projectFilename)
sendJson(response, 200, project)
return
}
if (request.method === "GET" && url.pathname === `${API_PREFIX}/messages`) {
const locale = url.searchParams.get("locale")
if (!locale) {
sendJson(response, 400, { error: "locale is required." })
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getMessages(locale))
return
}
if (request.method === "GET" && url.pathname === `${API_PREFIX}/packages`) {
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.getPackages())
return
}
if (request.method === "PUT" && url.pathname === `${API_PREFIX}/messages`) {
const input = await readJsonBody(request)
if (!isUpdateMessageInput(input)) {
sendJson(response, 400, {
error: "The message update payload is invalid.",
})
return
}
const service = new CatalogService(projectFilename)
sendJson(response, 200, await service.updateMessage(input))
return
}
if (
request.method === "POST" &&
url.pathname.startsWith(`${API_PREFIX}/actions/`)
) {
const action = url.pathname.slice(`${API_PREFIX}/actions/`.length)
if (action !== "extract" && action !== "compile") {
sendJson(response, 404, { error: "Unknown action." })
return
}
const result = await runProjectLinguiCommand(projectFilename, action)
sendJson(response, 200, result)
return
}
sendJson(response, 404, { error: "Not found." })
} catch (error) {
sendJson(response, 500, {
error:
error instanceof Error ? error.message : "An unknown error occurred.",
})
}
}
@@ -39,8 +39,8 @@ describe("CatalogService", () => {
{
catalogPath: "locales/{locale}/messages",
include: ["src"],
locales: ["en", "zh-Hans"],
sourceLocale: "en",
locales: ["en-US", "zh-Hans"],
sourceLocale: "en-US",
},
null,
2
@@ -54,7 +54,7 @@ describe("CatalogService", () => {
throw new Error("Expected a Lingui catalog.")
}
await catalog.write("en", {
await catalog.write("en-US", {
"dashboard.title": {
comments: ["Dashboard heading"],
message: "Dashboard",
@@ -113,7 +113,7 @@ describe("CatalogService", () => {
'msgstr "数据看板"'
)
const sourceMessages = await service.getMessages("en")
const sourceMessages = await service.getMessages("en-US")
const applicationMessage = sourceMessages.find(
(item) => item.id === "dashboard.title"
)
@@ -134,7 +134,7 @@ describe("CatalogService", () => {
service.updateMessage({
catalog: applicationMessage?.catalog ?? "messages",
id: "dashboard.title",
locale: "en",
locale: "en-US",
translation: "Customized dashboard",
})
).rejects.toThrow("must be changed in source code")
@@ -142,7 +142,7 @@ describe("CatalogService", () => {
const customizedDependency = await service.updateMessage({
catalog: dependencyMessage?.catalog ?? "messages",
id: "ui.common.close",
locale: "en",
locale: "en-US",
translation: "Dismiss",
})
@@ -151,7 +151,7 @@ describe("CatalogService", () => {
source: "Close",
translation: "Dismiss",
})
expect(await readFile(catalog.getFilename("en"), "utf8")).toContain(
expect(await readFile(catalog.getFilename("en-US"), "utf8")).toContain(
'msgstr "Dismiss"'
)
})
+28 -26
View File
@@ -37,15 +37,16 @@ class PackageNameResolver {
}
async resolve(origins: MessageType["origin"] | undefined) {
for (const [filename] of origins ?? []) {
const packageName = await this.#findNearestPackageName(
dirname(resolve(this.#projectRoot, filename))
const packageNames = await Promise.all(
(origins ?? []).map(([filename]) =>
this.#findNearestPackageName(
dirname(resolve(this.#projectRoot, filename))
)
)
)
const packageName = packageNames.find(Boolean)
if (packageName) {
return packageName
}
}
if (packageName) return packageName
return this.#projectPackageName
}
@@ -187,27 +188,28 @@ export class CatalogService {
...Object.keys(targetCatalog),
])
return Promise.all(
Array.from(ids)
.sort((left, right) => left.localeCompare(right))
.map(async (id) => {
const sourceEntry = sourceCatalog[id]
const targetEntry = targetCatalog[id]
const packageName = await packageNameResolver.resolve(
sourceEntry?.origin ?? targetEntry?.origin
)
const sortedIds = Array.from(ids)
sortedIds.sort((left, right) => left.localeCompare(right))
return toDevtoolMessage({
applicationPackageName,
catalog: key,
id,
locale,
packageName,
sourceEntry,
sourceLocale: config.sourceLocale,
targetEntry,
})
return Promise.all(
sortedIds.map(async (id) => {
const sourceEntry = sourceCatalog[id]
const targetEntry = targetCatalog[id]
const packageName = await packageNameResolver.resolve(
sourceEntry?.origin ?? targetEntry?.origin
)
return toDevtoolMessage({
applicationPackageName,
catalog: key,
id,
locale,
packageName,
sourceEntry,
sourceLocale: config.sourceLocale,
targetEntry,
})
})
)
})
)
+4 -4
View File
@@ -30,8 +30,8 @@ describe("runProjectLinguiCommand", () => {
{
catalogPath: "locales/{locale}/messages",
include: ["src"],
locales: ["en"],
sourceLocale: "en",
locales: ["en-US"],
sourceLocale: "en-US",
},
null,
2
@@ -45,7 +45,7 @@ describe("runProjectLinguiCommand", () => {
throw new Error("Expected a Lingui catalog.")
}
await catalog.write("en", {
await catalog.write("en-US", {
greeting: {
message: "Hello",
translation: "Hello",
@@ -53,7 +53,7 @@ describe("runProjectLinguiCommand", () => {
})
const result = await runProjectLinguiCommand(projectFilename, "compile")
const compiledFilename = join(directory, "locales/en/messages.ts")
const compiledFilename = join(directory, "locales/en-US/messages.ts")
const firstModifiedTime = (await stat(compiledFilename)).mtimeMs
expect(result.output).toContain("Compiling message catalogs")
+14 -12
View File
@@ -83,24 +83,26 @@ async function readFileIfPresent(filename: string) {
async function publishCompiledCatalogs(
catalogs: readonly StagedCompiledCatalog[]
) {
for (const catalog of catalogs) {
const nextContent = await readFile(catalog.stagedOutputFilename, "utf8")
const currentContent = await readFileIfPresent(catalog.outputFilename)
await Promise.all(
catalogs.map(async (catalog) => {
const [nextContent, currentContent] = await Promise.all([
readFile(catalog.stagedOutputFilename, "utf8"),
readFileIfPresent(catalog.outputFilename),
])
if (currentContent === nextContent) {
continue
}
if (currentContent === nextContent) return
await mkdir(dirname(catalog.outputFilename), { recursive: true })
await writeFile(catalog.outputFilename, nextContent, "utf8")
}
await mkdir(dirname(catalog.outputFilename), { recursive: true })
await writeFile(catalog.outputFilename, nextContent, "utf8")
})
)
}
async function runLinguiCommand(
args: readonly string[],
cwd: string
): Promise<LinguiCommandResult> {
return new Promise((resolve, reject) => {
return new Promise((resolvePromise, reject) => {
const child = spawn("node", [LINGUI_BIN, ...args], {
cwd,
env: process.env,
@@ -119,7 +121,7 @@ async function runLinguiCommand(
child.on("error", reject)
child.on("close", (code) => {
if (code === 0) {
resolve({ output })
resolvePromise({ output })
return
}
@@ -170,7 +172,7 @@ export async function runProjectLinguiCommand(
catalogs: projectConfig.catalogs?.map((catalog) =>
typeof catalog === "string"
? catalog
: { ...catalog, path: stagedCatalogPath }
: Object.assign({}, catalog, { path: stagedCatalogPath })
),
}
: undefined),
+2 -2
View File
@@ -62,8 +62,8 @@ describe("readProject", () => {
"@workspace/blocks/navigation/locales/{locale}",
],
include: ["src"],
locales: ["en", "zh-Hans"],
sourceLocale: "en",
locales: ["en-US", "zh-Hans"],
sourceLocale: "en-US",
})}\n`
)
+8 -16
View File
@@ -60,24 +60,16 @@ function validateProjectConfig(
}
}
async function findInParents(startDirectory: string) {
let directory = resolve(startDirectory)
async function findInParents(
startDirectory: string
): Promise<string | undefined> {
const directory = resolve(startDirectory)
const candidate = join(directory, PROJECT_FILENAME)
while (true) {
const candidate = join(directory, PROJECT_FILENAME)
if (await fileExists(candidate)) return candidate
if (await fileExists(candidate)) {
return candidate
}
const parent = dirname(directory)
if (parent === directory) {
return undefined
}
directory = parent
}
const parent = dirname(directory)
return parent === directory ? undefined : findInParents(parent)
}
export async function resolveProjectFilename(
@@ -13,8 +13,8 @@ describe("createLinguiConfig", () => {
catalogSources: ["@workspace/ui/locales/{locale}"],
exclude: ["**/*.test.ts"],
include: ["src"],
locales: ["en", "zh-Hans"],
sourceLocale: "en",
locales: ["en-US", "zh-Hans"],
sourceLocale: "en-US",
})
).toEqual({
catalogs: [
@@ -24,11 +24,11 @@ describe("createLinguiConfig", () => {
path: "<rootDir>/locales/{locale}/messages",
},
],
locales: ["en", "zh-Hans"],
locales: ["en-US", "zh-Hans"],
runtimeConfigModule: {
Trans: ["@workspace/i18n", "Translate"],
},
sourceLocale: "en",
sourceLocale: "en-US",
})
})
@@ -37,8 +37,8 @@ describe("createLinguiConfig", () => {
{
catalogPath: "locales/{locale}/messages",
include: ["src"],
locales: ["en"],
sourceLocale: "en",
locales: ["en-US"],
sourceLocale: "en-US",
},
"/workspace/apps/web"
)
+8 -8
View File
@@ -4,16 +4,16 @@ import { describe, expect, it } from "vitest"
import { AnsiText, resolveAnsiComponent } from "./ansi-text"
function TestComponent({ children }: { children?: string }) {
return <code>{children}</code>
}
describe("AnsiText", () => {
it("resolves direct, CommonJS, and nested default exports", () => {
const Component = ({ children }: { children?: string }) => (
<code>{children}</code>
)
expect(resolveAnsiComponent(Component)).toBe(Component)
expect(resolveAnsiComponent({ default: Component })).toBe(Component)
expect(resolveAnsiComponent({ default: { default: Component } })).toBe(
Component
expect(resolveAnsiComponent(TestComponent)).toBe(TestComponent)
expect(resolveAnsiComponent({ default: TestComponent })).toBe(TestComponent)
expect(resolveAnsiComponent({ default: { default: TestComponent } })).toBe(
TestComponent
)
})
+1 -1
View File
@@ -42,7 +42,7 @@ export function DevtoolApp() {
const locale =
state.project.locales.find(
(locale) => locale !== state.project.sourceLocale
(candidateLocale) => candidateLocale !== state.project.sourceLocale
) ?? state.project.sourceLocale
if (!state.project.locales.includes(locale)) {
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+1
View File
@@ -2,6 +2,7 @@ import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { DevtoolApp } from "./app"
// oxlint-disable-next-line import/no-unassigned-import -- Loads the standalone Devtool stylesheet.
import "./styles.css"
const root = document.getElementById("root")
@@ -5,17 +5,18 @@ import { resolveI18nDevtoolLocale } from "./devtool-localization"
describe("resolveI18nDevtoolLocale", () => {
it("normalizes common BCP 47 language tags", () => {
expect(resolveI18nDevtoolLocale("zh-CN")).toBe("zh-Hans")
expect(resolveI18nDevtoolLocale("zh-TW")).toBe("zh-Hant")
expect(resolveI18nDevtoolLocale("es-419")).toBe("es")
expect(resolveI18nDevtoolLocale("en-GB")).toBe("en-US")
})
it("selects the first supported preferred locale", () => {
expect(
resolveI18nDevtoolLocale(undefined, ["pt-BR", "fr-CA", "en-US"])
).toBe("fr")
resolveI18nDevtoolLocale(undefined, ["pt-BR", "fr-CA", "zh-CN"])
).toBe("zh-Hans")
})
it("falls back to English", () => {
expect(resolveI18nDevtoolLocale(undefined, ["pt-BR"])).toBe("en")
expect(resolveI18nDevtoolLocale(undefined, ["pt-BR", "zh-TW"])).toBe(
"en-US"
)
})
})
@@ -1,15 +1,6 @@
import * as React from "react"
export const I18N_DEVTOOL_LOCALES = [
"en",
"de",
"es",
"fr",
"ja",
"ko",
"zh-Hans",
"zh-Hant",
] as const
export const I18N_DEVTOOL_LOCALES = ["en-US", "zh-Hans"] as const
export type I18nDevtoolLocale = (typeof I18N_DEVTOOL_LOCALES)[number]
@@ -104,7 +95,7 @@ interface DevtoolMessages {
}
const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = {
en: {
"en-US": {
actions: {
compile: "Compile",
compileCompleted: "Compilation completed.",
@@ -195,460 +186,6 @@ const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = {
open: "Open I18n Devtool",
},
},
de: {
actions: {
compile: "Kompilieren",
compileCompleted: "Kompilierung abgeschlossen.",
compileFailed: "Kompilierung fehlgeschlagen.",
compiling: "Wird kompiliert…",
enterFullscreen: "Vollbildmodus aktivieren",
exitFullscreen: "Vollbildmodus beenden",
extract: "Extrahieren",
extractCompleted: "Extraktion abgeschlossen.",
extractFailed: "Extraktion fehlgeschlagen.",
extracting: "Wird extrahiert…",
label: "Aktionen",
},
commandOutput: {
close: "Befehlsausgabe schließen",
collapse: "Befehlsausgabe einklappen",
expand: "Befehlsausgabe ausklappen",
title: "Befehlsausgabe",
},
locale: {
controlLabel: "Zielsprache der Nachrichten",
label: "Zielsprache",
noTargetLocales: "Keine Zielsprachen konfiguriert.",
sourceLabel: "Ausgangssprache",
},
messagePanel: {
additionalSourceLocations: (count) => `${count} weitere Quellorte`,
chooseCopyContent: "Inhalt zum Kopieren auswählen",
collapseNamespace: (path) => `${path} einklappen`,
collapsePackage: (packageName) => `${packageName} einklappen`,
copied: "Kopiert",
copyDescriptor: "Nachrichten-Deskriptor kopieren",
copyFailed: "Kopieren fehlgeschlagen",
copyMessageId: "Nachrichten-ID kopieren",
copySource: "Quelltext kopieren",
copySourceLocation: "Quellort kopieren",
copyTranslation: "Aktuelle Übersetzung kopieren",
couldNotSave: "Speichern fehlgeschlagen",
emptyCatalog:
"Es sind noch keine Nachrichten verfügbar. Extrahieren Sie das Projekt, um diesen Katalog zu füllen.",
emptyPackage: "In diesem Paket wurden keine Nachrichten gefunden.",
expandNamespace: (path) => `${path} ausklappen`,
expandPackage: (packageName) => `${packageName} ausklappen`,
loading: (locale) => `${locale} wird geladen…`,
messageCount: (count) => `${count} Nachrichten`,
messageEditor: "Nachrichteneditor",
messageNavigation: "Nachrichtennavigation",
missingCount: (count) => `${count} fehlen`,
missingOnly: "Nur fehlende",
noMatches: "Keine Nachrichten entsprechen den aktuellen Filtern.",
noMissingMessages: "Alle Nachrichten sind übersetzt.",
noSearchResults: "Keine Nachrichten entsprechen Ihrer Suche.",
reset: "Zurücksetzen",
retry: "Erneut versuchen",
save: "Speichern",
saved: "Gespeichert",
saving: "Wird gespeichert…",
search: "Nachrichten durchsuchen",
selectMessage:
"Wählen Sie eine Nachricht aus, um ihre Übersetzung anzuzeigen und zu bearbeiten.",
source: "Quelle",
sourceLocations: "Quellorte",
translation: "Übersetzung",
translationFor: (id, locale) => `${id}: Übersetzung für ${locale}`,
unknownSourceLocation: "Unbekannter Quellort",
},
settings: {
auto: "Automatisch",
autoTheme: "Automatisches Design",
center: "Mitte",
dark: "Dunkel",
darkTheme: "Dunkles Design",
interfaceLanguage: "Oberflächensprache",
interfaceLanguageControl: "Oberflächensprache des I18n Devtools",
label: "I18n-Devtool-Einstellungen",
left: "Links",
light: "Hell",
lightTheme: "Helles Design",
open: "I18n-Devtool-Einstellungen öffnen",
panelPosition: "Panelposition",
panelPositionControl: "Position des I18n-Devtool-Panels",
right: "Rechts",
theme: "Design",
themeControl: "I18n-Devtool-Design",
},
trigger: {
close: "I18n Devtool schließen",
open: "I18n Devtool öffnen",
},
},
es: {
actions: {
compile: "Compilar",
compileCompleted: "Compilación completada.",
compileFailed: "Error de compilación.",
compiling: "Compilando…",
enterFullscreen: "Entrar en pantalla completa",
exitFullscreen: "Salir de pantalla completa",
extract: "Extraer",
extractCompleted: "Extracción completada.",
extractFailed: "Error de extracción.",
extracting: "Extrayendo…",
label: "Acciones",
},
commandOutput: {
close: "Cerrar salida del comando",
collapse: "Contraer salida del comando",
expand: "Expandir salida del comando",
title: "Salida del comando",
},
locale: {
controlLabel: "Idioma de destino de los mensajes",
label: "Idioma de destino",
noTargetLocales: "No hay idiomas de destino configurados.",
sourceLabel: "Idioma de origen",
},
messagePanel: {
additionalSourceLocations: (count) =>
`${count} ubicaciones de origen adicionales`,
chooseCopyContent: "Elegir contenido para copiar",
collapseNamespace: (path) => `Contraer ${path}`,
collapsePackage: (packageName) => `Contraer ${packageName}`,
copied: "Copiado",
copyDescriptor: "Copiar descriptor del mensaje",
copyFailed: "No se pudo copiar",
copyMessageId: "Copiar ID del mensaje",
copySource: "Copiar texto de origen",
copySourceLocation: "Copiar ubicación de origen",
copyTranslation: "Copiar traducción actual",
couldNotSave: "No se pudo guardar",
emptyCatalog:
"Aún no hay mensajes disponibles. Extraiga el proyecto para completar este catálogo.",
emptyPackage: "No se encontraron mensajes en este paquete.",
expandNamespace: (path) => `Expandir ${path}`,
expandPackage: (packageName) => `Expandir ${packageName}`,
loading: (locale) => `Cargando ${locale}`,
messageCount: (count) => `${count} mensajes`,
messageEditor: "Editor de mensajes",
messageNavigation: "Navegación de mensajes",
missingCount: (count) => `${count} faltantes`,
missingOnly: "Solo faltantes",
noMatches: "Ningún mensaje coincide con los filtros actuales.",
noMissingMessages: "Todos los mensajes tienen traducción.",
noSearchResults: "Ningún mensaje coincide con la búsqueda.",
reset: "Restablecer",
retry: "Reintentar",
save: "Guardar",
saved: "Guardado",
saving: "Guardando…",
search: "Buscar mensajes",
selectMessage: "Seleccione un mensaje para ver y editar su traducción.",
source: "Origen",
sourceLocations: "Ubicaciones de origen",
translation: "Traducción",
translationFor: (id, locale) => `Traducción de ${id} para ${locale}`,
unknownSourceLocation: "Ubicación de origen desconocida",
},
settings: {
auto: "Automático",
autoTheme: "Tema automático",
center: "Centro",
dark: "Oscuro",
darkTheme: "Tema oscuro",
interfaceLanguage: "Idioma de la interfaz",
interfaceLanguageControl: "Idioma de la interfaz de I18n Devtool",
label: "Ajustes de I18n Devtool",
left: "Izquierda",
light: "Claro",
lightTheme: "Tema claro",
open: "Abrir ajustes de I18n Devtool",
panelPosition: "Posición del panel",
panelPositionControl: "Posición del panel de I18n Devtool",
right: "Derecha",
theme: "Tema",
themeControl: "Tema de I18n Devtool",
},
trigger: {
close: "Cerrar I18n Devtool",
open: "Abrir I18n Devtool",
},
},
fr: {
actions: {
compile: "Compiler",
compileCompleted: "Compilation terminée.",
compileFailed: "Échec de la compilation.",
compiling: "Compilation…",
enterFullscreen: "Passer en plein écran",
exitFullscreen: "Quitter le plein écran",
extract: "Extraire",
extractCompleted: "Extraction terminée.",
extractFailed: "Échec de lextraction.",
extracting: "Extraction…",
label: "Actions",
},
commandOutput: {
close: "Fermer la sortie de commande",
collapse: "Réduire la sortie de commande",
expand: "Développer la sortie de commande",
title: "Sortie de commande",
},
locale: {
controlLabel: "Langue cible des messages",
label: "Langue cible",
noTargetLocales: "Aucune langue cible nest configurée.",
sourceLabel: "Langue source",
},
messagePanel: {
additionalSourceLocations: (count) =>
`${count} emplacements source supplémentaires`,
chooseCopyContent: "Choisir le contenu à copier",
collapseNamespace: (path) => `Réduire ${path}`,
collapsePackage: (packageName) => `Réduire ${packageName}`,
copied: "Copié",
copyDescriptor: "Copier le descripteur du message",
copyFailed: "Échec de la copie",
copyMessageId: "Copier lidentifiant du message",
copySource: "Copier le texte source",
copySourceLocation: "Copier lemplacement source",
copyTranslation: "Copier la traduction actuelle",
couldNotSave: "Échec de lenregistrement",
emptyCatalog:
"Aucun message nest encore disponible. Extrayez le projet pour remplir ce catalogue.",
emptyPackage: "Aucun message na été trouvé dans ce package.",
expandNamespace: (path) => `Développer ${path}`,
expandPackage: (packageName) => `Développer ${packageName}`,
loading: (locale) => `Chargement de ${locale}`,
messageCount: (count) => `${count} messages`,
messageEditor: "Éditeur de messages",
messageNavigation: "Navigation des messages",
missingCount: (count) => `${count} manquants`,
missingOnly: "Manquants uniquement",
noMatches: "Aucun message ne correspond aux filtres actuels.",
noMissingMessages: "Tous les messages sont traduits.",
noSearchResults: "Aucun message ne correspond à votre recherche.",
reset: "Réinitialiser",
retry: "Réessayer",
save: "Enregistrer",
saved: "Enregistré",
saving: "Enregistrement…",
search: "Rechercher des messages",
selectMessage:
"Sélectionnez un message pour afficher et modifier sa traduction.",
source: "Source",
sourceLocations: "Emplacements source",
translation: "Traduction",
translationFor: (id, locale) => `Traduction de ${id} pour ${locale}`,
unknownSourceLocation: "Emplacement source inconnu",
},
settings: {
auto: "Auto",
autoTheme: "Thème automatique",
center: "Centre",
dark: "Sombre",
darkTheme: "Thème sombre",
interfaceLanguage: "Langue de linterface",
interfaceLanguageControl: "Langue de linterface de loutil I18n",
label: "Paramètres de I18n Devtool",
left: "Gauche",
light: "Clair",
lightTheme: "Thème clair",
open: "Ouvrir les paramètres de I18n Devtool",
panelPosition: "Position du panneau",
panelPositionControl: "Position du panneau I18n Devtool",
right: "Droite",
theme: "Thème",
themeControl: "Thème de I18n Devtool",
},
trigger: {
close: "Fermer I18n Devtool",
open: "Ouvrir I18n Devtool",
},
},
ja: {
actions: {
compile: "コンパイル",
compileCompleted: "コンパイルが完了しました。",
compileFailed: "コンパイルに失敗しました。",
compiling: "コンパイル中…",
enterFullscreen: "全画面表示にする",
exitFullscreen: "全画面表示を終了",
extract: "抽出",
extractCompleted: "抽出が完了しました。",
extractFailed: "抽出に失敗しました。",
extracting: "抽出中…",
label: "操作",
},
commandOutput: {
close: "コマンド出力を閉じる",
collapse: "コマンド出力を折りたたむ",
expand: "コマンド出力を展開する",
title: "コマンド出力",
},
locale: {
controlLabel: "メッセージの翻訳先言語",
label: "翻訳先言語",
noTargetLocales: "翻訳先言語が設定されていません。",
sourceLabel: "原文言語",
},
messagePanel: {
additionalSourceLocations: (count) => `${count} 件のソース位置`,
chooseCopyContent: "コピーする内容を選択",
collapseNamespace: (path) => `${path} を折りたたむ`,
collapsePackage: (packageName) => `${packageName} を折りたたむ`,
copied: "コピーしました",
copyDescriptor: "メッセージ記述子をコピー",
copyFailed: "コピーできませんでした",
copyMessageId: "メッセージ ID をコピー",
copySource: "原文をコピー",
copySourceLocation: "ソース位置をコピー",
copyTranslation: "現在の翻訳をコピー",
couldNotSave: "保存できませんでした",
emptyCatalog:
"メッセージはまだありません。プロジェクトを抽出してカタログを作成してください。",
emptyPackage: "このパッケージにはメッセージがありません。",
expandNamespace: (path) => `${path} を展開`,
expandPackage: (packageName) => `${packageName} を展開`,
loading: (locale) => `${locale} を読み込み中…`,
messageCount: (count) => `${count}`,
messageEditor: "メッセージエディター",
messageNavigation: "メッセージナビゲーション",
missingCount: (count) => `未翻訳 ${count}`,
missingOnly: "未翻訳のみ",
noMatches: "現在のフィルターに一致するメッセージはありません。",
noMissingMessages: "すべてのメッセージが翻訳済みです。",
noSearchResults: "検索に一致するメッセージはありません。",
reset: "リセット",
retry: "再試行",
save: "保存",
saved: "保存済み",
saving: "保存中…",
search: "メッセージを検索",
selectMessage: "翻訳を表示・編集するメッセージを選択してください。",
source: "原文",
sourceLocations: "ソース位置",
translation: "翻訳",
translationFor: (id, locale) => `${id}${locale} 翻訳`,
unknownSourceLocation: "ソース位置不明",
},
settings: {
auto: "自動",
autoTheme: "自動テーマ",
center: "中央",
dark: "ダーク",
darkTheme: "ダークテーマ",
interfaceLanguage: "表示言語",
interfaceLanguageControl: "I18n Devtool の表示言語",
label: "I18n Devtool の設定",
left: "左",
light: "ライト",
lightTheme: "ライトテーマ",
open: "I18n Devtool の設定を開く",
panelPosition: "パネル位置",
panelPositionControl: "I18n Devtool のパネル位置",
right: "右",
theme: "テーマ",
themeControl: "I18n Devtool のテーマ",
},
trigger: {
close: "I18n Devtool を閉じる",
open: "I18n Devtool を開く",
},
},
ko: {
actions: {
compile: "컴파일",
compileCompleted: "컴파일이 완료되었습니다.",
compileFailed: "컴파일에 실패했습니다.",
compiling: "컴파일 중…",
enterFullscreen: "전체 화면으로 전환",
exitFullscreen: "전체 화면 종료",
extract: "추출",
extractCompleted: "추출이 완료되었습니다.",
extractFailed: "추출에 실패했습니다.",
extracting: "추출 중…",
label: "작업",
},
commandOutput: {
close: "명령 출력을 닫기",
collapse: "명령 출력을 접기",
expand: "명령 출력을 펼치기",
title: "명령 출력",
},
locale: {
controlLabel: "메시지 대상 언어",
label: "대상 언어",
noTargetLocales: "대상 언어가 설정되지 않았습니다.",
sourceLabel: "원본 언어",
},
messagePanel: {
additionalSourceLocations: (count) => `추가 소스 위치 ${count}`,
chooseCopyContent: "복사할 내용 선택",
collapseNamespace: (path) => `${path} 접기`,
collapsePackage: (packageName) => `${packageName} 접기`,
copied: "복사됨",
copyDescriptor: "메시지 설명자 복사",
copyFailed: "복사하지 못했습니다",
copyMessageId: "메시지 ID 복사",
copySource: "원문 복사",
copySourceLocation: "소스 위치 복사",
copyTranslation: "현재 번역 복사",
couldNotSave: "저장하지 못했습니다",
emptyCatalog:
"아직 사용할 수 있는 메시지가 없습니다. 프로젝트에서 메시지를 추출해 카탈로그를 채우세요.",
emptyPackage: "이 패키지에는 메시지가 없습니다.",
expandNamespace: (path) => `${path} 펼치기`,
expandPackage: (packageName) => `${packageName} 펼치기`,
loading: (locale) => `${locale} 불러오는 중…`,
messageCount: (count) => `${count}개 메시지`,
messageEditor: "메시지 편집기",
messageNavigation: "메시지 탐색",
missingCount: (count) => `${count}개 누락`,
missingOnly: "누락된 항목만",
noMatches: "현재 필터와 일치하는 메시지가 없습니다.",
noMissingMessages: "모든 메시지가 번역되었습니다.",
noSearchResults: "검색어와 일치하는 메시지가 없습니다.",
reset: "재설정",
retry: "다시 시도",
save: "저장",
saved: "저장됨",
saving: "저장 중…",
search: "메시지 검색",
selectMessage: "번역을 확인하고 편집할 메시지를 선택하세요.",
source: "원문",
sourceLocations: "소스 위치",
translation: "번역",
translationFor: (id, locale) => `${id}${locale} 번역`,
unknownSourceLocation: "알 수 없는 소스 위치",
},
settings: {
auto: "자동",
autoTheme: "자동 테마",
center: "가운데",
dark: "다크",
darkTheme: "다크 테마",
interfaceLanguage: "인터페이스 언어",
interfaceLanguageControl: "I18n Devtool 인터페이스 언어",
label: "I18n Devtool 설정",
left: "왼쪽",
light: "라이트",
lightTheme: "라이트 테마",
open: "I18n Devtool 설정 열기",
panelPosition: "패널 위치",
panelPositionControl: "I18n Devtool 패널 위치",
right: "오른쪽",
theme: "테마",
themeControl: "I18n Devtool 테마",
},
trigger: {
close: "I18n Devtool 닫기",
open: "I18n Devtool 열기",
},
},
"zh-Hans": {
actions: {
compile: "编译",
@@ -738,103 +275,14 @@ const messagesByLocale: Record<I18nDevtoolLocale, DevtoolMessages> = {
open: "打开 I18n 开发工具",
},
},
"zh-Hant": {
actions: {
compile: "編譯",
compileCompleted: "編譯完成。",
compileFailed: "編譯失敗。",
compiling: "正在編譯…",
enterFullscreen: "進入全螢幕",
exitFullscreen: "退出全螢幕",
extract: "擷取",
extractCompleted: "擷取完成。",
extractFailed: "擷取失敗。",
extracting: "正在擷取…",
label: "操作",
},
commandOutput: {
close: "關閉命令輸出",
collapse: "收合命令輸出",
expand: "展開命令輸出",
title: "命令輸出",
},
locale: {
controlLabel: "訊息目標語言",
label: "目標語言",
noTargetLocales: "尚未設定目標語言。",
sourceLabel: "來源語言",
},
messagePanel: {
additionalSourceLocations: (count) => `另外 ${count} 個來源位置`,
chooseCopyContent: "選擇要複製的內容",
collapseNamespace: (path) => `收合 ${path}`,
collapsePackage: (packageName) => `收合 ${packageName}`,
copied: "已複製",
copyDescriptor: "複製 Message Descriptor",
copyFailed: "複製失敗",
copyMessageId: "複製 Message ID",
copySource: "複製原文",
copySourceLocation: "複製來源位置",
copyTranslation: "複製目前譯文",
couldNotSave: "儲存失敗",
emptyCatalog: "尚無可用訊息。請先擷取專案訊息以產生目錄。",
emptyPackage: "這個套件中暫無訊息。",
expandNamespace: (path) => `展開 ${path}`,
expandPackage: (packageName) => `展開 ${packageName}`,
loading: (locale) => `正在載入 ${locale}`,
messageCount: (count) => `${count}`,
messageEditor: "訊息編輯器",
messageNavigation: "訊息導覽",
missingCount: (count) => `缺漏 ${count}`,
missingOnly: "僅顯示缺漏項目",
noMatches: "沒有符合目前篩選條件的訊息。",
noMissingMessages: "所有訊息皆已完成翻譯。",
noSearchResults: "沒有符合搜尋內容的訊息。",
reset: "重設",
retry: "重試",
save: "儲存",
saved: "已儲存",
saving: "正在儲存…",
search: "搜尋訊息",
selectMessage: "請選擇一則訊息以檢視和編輯譯文。",
source: "原文",
sourceLocations: "來源位置",
translation: "譯文",
translationFor: (id, locale) => `${id}${locale} 翻譯`,
unknownSourceLocation: "未知來源位置",
},
settings: {
auto: "自動",
autoTheme: "自動主題",
center: "置中",
dark: "深色",
darkTheme: "深色主題",
interfaceLanguage: "介面語言",
interfaceLanguageControl: "I18n 開發工具介面語言",
label: "I18n 開發工具設定",
left: "左側",
light: "淺色",
lightTheme: "淺色主題",
open: "開啟 I18n 開發工具設定",
panelPosition: "面板位置",
panelPositionControl: "I18n 開發工具面板位置",
right: "右側",
theme: "主題",
themeControl: "I18n 開發工具主題",
},
trigger: {
close: "關閉 I18n 開發工具",
open: "開啟 I18n 開發工具",
},
},
}
const defaultContextValue: {
locale: I18nDevtoolLocale
messages: DevtoolMessages
} = {
locale: "en",
messages: messagesByLocale.en,
locale: "en-US",
messages: messagesByLocale["en-US"],
}
const DevtoolLocalizationContext = React.createContext(defaultContextValue)
@@ -847,30 +295,22 @@ function matchDevtoolLocale(locale: string): I18nDevtoolLocale | undefined {
}
if (
normalizedLocale === "zh-hant" ||
normalizedLocale.startsWith("zh-hant-")
normalizedLocale === "zh" ||
normalizedLocale === "zh-hans" ||
normalizedLocale.startsWith("zh-hans-") ||
normalizedLocale === "zh-cn" ||
normalizedLocale.startsWith("zh-cn-") ||
normalizedLocale === "zh-sg" ||
normalizedLocale.startsWith("zh-sg-")
) {
return "zh-Hant"
}
if (
normalizedLocale === "zh-tw" ||
normalizedLocale.startsWith("zh-tw-") ||
normalizedLocale === "zh-hk" ||
normalizedLocale.startsWith("zh-hk-") ||
normalizedLocale === "zh-mo" ||
normalizedLocale.startsWith("zh-mo-")
) {
return "zh-Hant"
}
if (normalizedLocale === "zh" || normalizedLocale.startsWith("zh-")) {
return "zh-Hans"
}
const language = normalizedLocale.split("-")[0]
if (normalizedLocale === "en" || normalizedLocale.startsWith("en-")) {
return "en-US"
}
return I18N_DEVTOOL_LOCALES.find((candidate) => candidate === language)
return undefined
}
function getNavigatorLocales() {
@@ -899,7 +339,7 @@ export function resolveI18nDevtoolLocale(
}
}
return "en"
return "en-US"
}
export function getI18nDevtoolMessages(locale: I18nDevtoolLocale) {
+13 -13
View File
@@ -24,7 +24,7 @@ afterEach(() => {
function renderDevtool(props?: I18nDevtoolProps) {
return render(
<I18nProvider catalogs={{ en: {} }} locale="en" locales={["en"]}>
<I18nProvider catalogs={{ "en-US": {} }} locale="en-US" locales={["en-US"]}>
<I18nDevtool dark="ui:dark" {...props} />
</I18nProvider>
)
@@ -87,17 +87,17 @@ describe("I18nDevtool", () => {
it("detects the first supported navigator locale", async () => {
vi.spyOn(window.navigator, "languages", "get").mockReturnValue([
"pt-BR",
"ja-JP",
"fr-CA",
"en-US",
])
renderDevtool()
const trigger = await screen.findByRole("button", {
name: "I18n Devtool を開く",
name: "Open I18n Devtool",
})
const devtool = trigger.closest<HTMLElement>('[data-slot="i18n-devtool"]')
expect(devtool?.getAttribute("lang")).toBe("ja")
expect(devtool?.getAttribute("lang")).toBe("en-US")
})
it("falls back to English when an explicit locale is unsupported", async () => {
@@ -109,7 +109,7 @@ describe("I18nDevtool", () => {
})
const devtool = trigger.closest<HTMLElement>('[data-slot="i18n-devtool"]')
expect(devtool?.getAttribute("lang")).toBe("en")
expect(devtool?.getAttribute("lang")).toBe("en-US")
})
it("includes the project source locale as a customization target", async () => {
@@ -118,7 +118,7 @@ describe("I18nDevtool", () => {
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input)
const body = url.endsWith("/project")
? { locales: ["en", "zh-Hans"], sourceLocale: "en" }
? { locales: ["en-US", "zh-Hans"], sourceLocale: "en-US" }
: []
return new Response(JSON.stringify(body), {
@@ -130,10 +130,10 @@ describe("I18nDevtool", () => {
render(
<I18nProvider
catalogs={{ en: {}, "zh-Hans": {} }}
locale="en"
catalogs={{ "en-US": {}, "zh-Hans": {} }}
locale="en-US"
locales={[
{ label: "English", locale: "en" },
{ label: "English", locale: "en-US" },
{ label: "简体中文", locale: "zh-Hans" },
]}
>
@@ -146,7 +146,7 @@ describe("I18nDevtool", () => {
})
await waitFor(() => {
expect(localeSelect).toHaveProperty("value", "en")
expect(localeSelect).toHaveProperty("value", "en-US")
})
expect(screen.getByText("Source locale")).toBeTruthy()
expect(screen.getByText("English", { selector: "output" })).toBeTruthy()
@@ -157,9 +157,9 @@ describe("I18nDevtool", () => {
it("tracks the configured document dark-mode class", async () => {
render(
<I18nProvider
catalogs={{ en: {} }}
locale="en"
locales={["en", "zh-Hans"]}
catalogs={{ "en-US": {} }}
locale="en-US"
locales={["en-US", "zh-Hans"]}
>
<I18nDevtool dark={"ui\\:dark"} />
</I18nProvider>
@@ -24,7 +24,7 @@ const message: DevtoolMessage = {
origins: [{ file: "src/navigation.tsx", line: 12 }],
packageName: "@workspace/ui",
source: "Home",
sourceLocale: "en",
sourceLocale: "en-US",
translation: "",
}
@@ -310,10 +310,10 @@ describe("MessagePanel", () => {
updateMessage,
}
render(<MessagePanel locale="en" repository={repository} />)
render(<MessagePanel locale="en-US" repository={repository} />)
const textarea = await screen.findByLabelText(
"navigation.home translation for en"
"navigation.home translation for en-US"
)
expect(textarea).toHaveProperty("readOnly", true)
+2 -3
View File
@@ -62,14 +62,13 @@ function normalizeLocales(
const definition = typeof input === "string" ? { locale: input } : input
const languageTag = definition.languageTag ?? definition.locale
return {
...definition,
return Object.assign({}, definition, {
direction: definition.direction ?? inferLocaleDirection(languageTag),
label:
definition.label ??
getLocaleDisplayName(languageTag, definition.locale),
languageTag,
}
})
})
if (!definitions.some((definition) => definition.locale === locale)) {
+45 -45
View File
@@ -17,7 +17,7 @@ import { LocalizedText } from "./localized-text"
import { Translate } from "./translate"
const catalogs = {
en: {
"en-US": {
greeting: "Hello",
},
"zh-Hans": {
@@ -39,6 +39,43 @@ function HookProbe() {
)
}
function DirectionProbe() {
return <span>{useLocales()[0]?.direction}</span>
}
function LocaleLabelsProbe() {
return (
<span>
{useLocales()
.map((locale) => locale.label)
.join("|")}
</span>
)
}
function LocaleProbe() {
const locale = useLocaleDefinition()
return (
<span>
{locale.locale}|{locale.languageTag}
</span>
)
}
function FormatterProbe() {
const formatters = useFormatters()
return (
<output>
{formatters.formatNumber(1234.5)}|
{formatters.formatCurrency(1234.5, "USD")}|
{formatters.formatList(["A", "B", "C"])}|
{formatters.formatRelativeTime(-1, "day", { numeric: "auto" })}
</output>
)
}
describe("i18n runtime", () => {
it("provides locale information and reactive translation helpers", () => {
render(
@@ -46,7 +83,7 @@ describe("i18n runtime", () => {
catalogs={catalogs}
locale="zh-Hans"
locales={[
{ label: "English", locale: "en" },
{ label: "English", locale: "en-US" },
{ label: "简体中文", locale: "zh-Hans" },
]}
>
@@ -54,7 +91,7 @@ describe("i18n runtime", () => {
</I18nProvider>
)
expect(screen.getByText("zh-Hans|en,zh-Hans|你好|你好")).toBeTruthy()
expect(screen.getByText("zh-Hans|en-US,zh-Hans|你好|你好")).toBeTruthy()
})
it("merges package catalogs and lets application catalogs override them", () => {
@@ -87,7 +124,7 @@ describe("i18n runtime", () => {
it("wraps Lingui Trans without adding a DOM element", () => {
const { container } = render(
<I18nProvider catalogs={catalogs} locale="en">
<I18nProvider catalogs={catalogs} locale="en-US">
<p>
<Translate id="greeting" message="Fallback" />
</p>
@@ -110,10 +147,6 @@ describe("i18n runtime", () => {
})
it("infers right-to-left locale direction", () => {
function DirectionProbe() {
return <span>{useLocales()[0]?.direction}</span>
}
render(
<I18nProvider locale="ar" locales={["ar"]}>
<DirectionProbe />
@@ -124,36 +157,16 @@ describe("i18n runtime", () => {
})
it("uses localized language names when locale labels are omitted", () => {
function LocaleLabelsProbe() {
return (
<span>
{useLocales()
.map((locale) => locale.label)
.join("|")}
</span>
)
}
render(
<I18nProvider locale="en" locales={["en", "zh-Hans"]}>
<I18nProvider locale="en-US" locales={["en-US", "zh-Hans"]}>
<LocaleLabelsProbe />
</I18nProvider>
)
expect(screen.getByText("English|简体中文")).toBeTruthy()
expect(screen.getByText("American English|简体中文")).toBeTruthy()
})
it("keeps catalog locales separate from Intl language tags", () => {
function LocaleProbe() {
const locale = useLocaleDefinition()
return (
<span>
{locale.locale}|{locale.languageTag}
</span>
)
}
render(
<I18nProvider
locale="zh-Hans"
@@ -167,23 +180,10 @@ describe("i18n runtime", () => {
})
it("formats standard internationalized values through Intl", () => {
function FormatterProbe() {
const formatters = useFormatters()
return (
<output>
{formatters.formatNumber(1234.5)}|
{formatters.formatCurrency(1234.5, "USD")}|
{formatters.formatList(["A", "B", "C"])}|
{formatters.formatRelativeTime(-1, "day", { numeric: "auto" })}
</output>
)
}
render(
<I18nProvider
locale="en"
locales={[{ languageTag: "en-US", locale: "en" }]}
locale="en-US"
locales={[{ languageTag: "en-US", locale: "en-US" }]}
>
<FormatterProbe />
</I18nProvider>
+2
View File
@@ -139,6 +139,8 @@ ${createCatalogLoaderRuntime(`(async () => {
function replaceMessageIds(code: string, schema: MessageIdSchema) {
let transformedCode = code
// ES2022 compatibility: Object.entries returns a fresh array.
// oxlint-disable-next-line unicorn/no-array-sort
for (const [id, compactId] of Object.entries(schema.messages).sort(
([left], [right]) => right.length - left.length
)) {