mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-09 22:08:30 +00:00
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
|
|
const appLocales = [
|
|
"ar",
|
|
"br",
|
|
"bs",
|
|
"da",
|
|
"de",
|
|
"es",
|
|
"fr",
|
|
"ja",
|
|
"ko",
|
|
"no",
|
|
"pl",
|
|
"ru",
|
|
"uk",
|
|
"th",
|
|
"tr",
|
|
"zh",
|
|
"zht",
|
|
] as const
|
|
const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr")
|
|
|
|
const domains = [
|
|
{
|
|
name: "app",
|
|
source: "./en.ts",
|
|
target: (locale: string) => `./${locale}.ts`,
|
|
locales: appLocales,
|
|
},
|
|
{
|
|
name: "ui",
|
|
source: "../../../ui/src/i18n/en.ts",
|
|
target: (locale: string) => `../../../ui/src/i18n/${locale}.ts`,
|
|
locales: appLocales,
|
|
},
|
|
{
|
|
name: "desktop",
|
|
source: "../../../desktop/src/renderer/i18n/en.ts",
|
|
target: (locale: string) => `../../../desktop/src/renderer/i18n/${locale}.ts`,
|
|
locales: desktopLocales,
|
|
},
|
|
] as const
|
|
|
|
describe("i18n parity", () => {
|
|
test("non-English locales have every English key", async () => {
|
|
for (const domain of domains) {
|
|
const source = await dictionary(domain.source)
|
|
for (const locale of domain.locales) {
|
|
const target = await dictionary(domain.target(locale))
|
|
const missing = Object.keys(source).filter((key) => !Object.hasOwn(target, key))
|
|
const extra = Object.keys(target).filter((key) => !Object.hasOwn(source, key))
|
|
expect({ domain: domain.name, locale, missing, extra }).toEqual({
|
|
domain: domain.name,
|
|
locale,
|
|
missing: [],
|
|
extra: [],
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
test("non-English locales preserve English placeholders", async () => {
|
|
for (const domain of domains) {
|
|
const source = await dictionary(domain.source)
|
|
for (const locale of domain.locales) {
|
|
const target = await dictionary(domain.target(locale))
|
|
const mismatched = Object.keys(source).filter(
|
|
(key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(),
|
|
)
|
|
expect({ domain: domain.name, locale, mismatched }).toEqual({ domain: domain.name, locale, mismatched: [] })
|
|
}
|
|
}
|
|
})
|
|
|
|
test("non-English locales translate targeted unseen session keys", async () => {
|
|
const source = await dictionary("./en.ts")
|
|
for (const locale of appLocales) {
|
|
const target = await dictionary(`./${locale}.ts`)
|
|
for (const key of ["command.session.previous.unseen", "command.session.next.unseen"]) {
|
|
expect(target[key]).toBeDefined()
|
|
expect(target[key]).not.toBe(source[key])
|
|
}
|
|
}
|
|
})
|
|
|
|
test("changed-file summary keys preserve rendered English copy and localize complete phrases", async () => {
|
|
const source = await dictionary("../../../ui/src/i18n/en.ts")
|
|
expect(source["ui.sessionTurn.diffs.changed.one"].replace("{{count}}", "1")).toBe("1 Changed file")
|
|
expect(source["ui.sessionTurn.diffs.changed.other"].replace("{{count}}", "2")).toBe("2 Changed files")
|
|
expect(source["ui.sessionTurn.diffs.changed"]).toBeUndefined()
|
|
|
|
for (const locale of appLocales) {
|
|
const target = await dictionary(`../../../ui/src/i18n/${locale}.ts`)
|
|
for (const key of ["ui.sessionTurn.diffs.changed.one", "ui.sessionTurn.diffs.changed.other"]) {
|
|
expect(target[key].trim()).not.toBe("")
|
|
expect(placeholders(target[key])).toEqual(["count"])
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
async function dictionary(file: string) {
|
|
const module: unknown = await import(file)
|
|
if (typeof module !== "object" || module === null || !("dict" in module) || !isDictionary(module.dict)) {
|
|
throw new Error(`Invalid translation dictionary: ${file}`)
|
|
}
|
|
return module.dict
|
|
}
|
|
|
|
function isDictionary(value: unknown): value is Record<string, string> {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false
|
|
return Object.values(value).every((item) => typeof item === "string")
|
|
}
|
|
|
|
function placeholders(value: string) {
|
|
return Array.from(value.matchAll(/{{\s*([^}]+?)\s*}}/g), (match) => match[1]).sort()
|
|
}
|