chore: complete clean Agent Desk source snapshot

This commit is contained in:
2026-10-16 12:03:16 +00:00
parent 756ae33d05
commit e57610a27c
123 changed files with 37513 additions and 0 deletions

57
web/i18n/config.test.mjs Normal file
View File

@@ -0,0 +1,57 @@
import assert from "node:assert/strict"
import test from "node:test"
import ts from "typescript"
import { readFile } from "node:fs/promises"
import vm from "node:vm"
async function loadConfig() {
const source = await readFile(new URL("./config.ts", import.meta.url), "utf8")
const compiled = ts.transpileModule(source, {
compilerOptions: {
target: ts.ScriptTarget.ES2017,
module: ts.ModuleKind.CommonJS,
},
fileName: "config.ts",
})
const sandbox = {
exports: {},
module: { exports: {} },
}
sandbox.exports = sandbox.module.exports
vm.runInNewContext(compiled.outputText, sandbox)
return sandbox.module.exports
}
test("normalizes supported locale aliases", async () => {
const { DEFAULT_LOCALE, normalizeLocale } = await loadConfig()
assert.equal(DEFAULT_LOCALE, "en-US")
assert.equal(normalizeLocale("zh-CN"), "zh-CN")
assert.equal(normalizeLocale("zh_CN"), "zh-CN")
assert.equal(normalizeLocale("zh"), "zh-CN")
assert.equal(normalizeLocale("en-US"), "en-US")
assert.equal(normalizeLocale("en_US"), "en-US")
assert.equal(normalizeLocale("en"), "en-US")
assert.equal(normalizeLocale("ru-RU"), "ru-RU")
assert.equal(normalizeLocale("ru_ru"), "ru-RU")
assert.equal(normalizeLocale("RU"), "ru-RU")
assert.equal(normalizeLocale("fr-FR"), DEFAULT_LOCALE)
})
test("lists every supported locale in SUPPORTED_LOCALES", async () => {
const { SUPPORTED_LOCALES } = await loadConfig()
assert.deepEqual([...SUPPORTED_LOCALES], ["zh-CN", "en-US", "ru-RU"])
})
test("reads the configured locale without browser language detection", async () => {
const { configureLocale, readStoredLocale } = await loadConfig()
assert.equal(readStoredLocale(), "en-US")
configureLocale("en-US")
assert.equal(readStoredLocale(), "en-US")
configureLocale("ru-RU")
assert.equal(readStoredLocale(), "ru-RU")
})

51
web/i18n/config.ts Normal file
View File

@@ -0,0 +1,51 @@
export const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ru-RU"] as const
export type AppLocale = (typeof SUPPORTED_LOCALES)[number]
export const DEFAULT_LOCALE: AppLocale = "en-US"
const LOCALE_ALIASES: Record<string, AppLocale> = {
zh: "zh-CN",
"zh-cn": "zh-CN",
zh_cn: "zh-CN",
"zh-hans": "zh-CN",
en: "en-US",
"en-us": "en-US",
en_us: "en-US",
ru: "ru-RU",
"ru-ru": "ru-RU",
ru_ru: "ru-RU",
}
export function normalizeLocale(value: string | null | undefined): AppLocale {
return normalizeSupportedLocale(value) ?? DEFAULT_LOCALE
}
function normalizeSupportedLocale(
value: string | null | undefined
): AppLocale | null {
if (!value) {
return null
}
const key = value.trim().toLowerCase()
return LOCALE_ALIASES[key] ?? null
}
export function isSupportedLocale(
value: string | null | undefined
): value is AppLocale {
if (!value) {
return false
}
return SUPPORTED_LOCALES.includes(value as AppLocale)
}
let configuredLocale: AppLocale = DEFAULT_LOCALE
export function readStoredLocale(): AppLocale {
return configuredLocale
}
export function configureLocale(locale: string | null | undefined): AppLocale {
configuredLocale = normalizeLocale(locale)
return configuredLocale
}

View File

@@ -0,0 +1,91 @@
import assert from "node:assert/strict"
import test from "node:test"
import { readFile } from "node:fs/promises"
const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ru-RU"]
async function loadMessages(locale) {
const source = await readFile(new URL(`../messages/${locale}.json`, import.meta.url), "utf8")
return JSON.parse(source)
}
function flatten(source, prefix = "", target = {}) {
for (const [key, value] of Object.entries(source)) {
const path = prefix ? `${prefix}.${key}` : key
if (value && typeof value === "object" && !Array.isArray(value)) {
flatten(value, path, target)
continue
}
target[path] = value
}
return target
}
function placeholders(value) {
return ((String(value).match(/\{(\w+)\}/g) ?? []).slice().sort().join("|"))
}
test("legal pages have localized document content", async () => {
for (const locale of SUPPORTED_LOCALES) {
const messages = await loadMessages(locale)
assert.equal(typeof messages.legal.terms.title, "string")
assert.equal(typeof messages.legal.privacy.title, "string")
assert.ok(messages.legal.terms.sections.length >= 6)
assert.ok(messages.legal.privacy.sections.length >= 6)
for (const page of [messages.legal.terms, messages.legal.privacy]) {
assert.equal(typeof page.updatedAt, "string")
assert.equal(typeof page.relatedLabel, "string")
assert.equal(typeof page.relatedLink, "string")
for (const section of page.sections) {
assert.equal(typeof section.title, "string")
assert.equal(typeof section.body, "string")
}
}
}
})
test("every locale bundle defines the same keys as the default locale", async () => {
const base = flatten(await loadMessages("zh-CN"))
for (const locale of SUPPORTED_LOCALES) {
const messages = flatten(await loadMessages(locale))
for (const key of Object.keys(base)) {
assert.ok(key in messages, `${locale}.json is missing key ${key}`)
}
for (const key of Object.keys(messages)) {
assert.ok(key in base, `${locale}.json has extra key ${key}`)
}
}
})
test("every locale keeps the same interpolation placeholders", async () => {
const base = flatten(await loadMessages("zh-CN"))
for (const locale of SUPPORTED_LOCALES) {
const messages = flatten(await loadMessages(locale))
for (const [key, value] of Object.entries(base)) {
if (typeof value !== "string" || Array.isArray(value)) {
continue
}
assert.equal(
placeholders(messages[key]),
placeholders(value),
`${locale}.json key ${key} placeholders differ`
)
}
}
})
test("locale labels exist for every supported locale", async () => {
for (const locale of SUPPORTED_LOCALES) {
const messages = await loadMessages(locale)
for (const supported of SUPPORTED_LOCALES) {
assert.equal(typeof messages.locale[supported], "string")
}
}
})

View File

@@ -0,0 +1,174 @@
import assert from "node:assert/strict"
import test from "node:test"
import { readFile, readdir } from "node:fs/promises"
import path from "node:path"
const WEB_ROOT = new URL("../", import.meta.url)
const LOCALES = ["zh-CN", "en-US", "ru-RU"]
const SOURCE_DIRS = ["app", "components", "lib"]
const SKIP_DIRS = new Set(["node_modules", ".next", "out", "public"])
async function collectSourceFiles(dir) {
const entries = await readdir(new URL(dir, WEB_ROOT), { withFileTypes: true })
const files = []
for (const entry of entries) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) files.push(...(await collectSourceFiles(path.posix.join(dir, entry.name))))
continue
}
if (/\.(ts|tsx)$/.test(entry.name) && !/\.test\.mjs$/.test(entry.name)) {
files.push(path.posix.join(dir, entry.name))
}
}
return files
}
/**
* Finds duplicate keys inside any JSON object. JSON.parse keeps only the last
* occurrence, so a repeated top-level key silently deletes every sibling of the
* block that shadowed it - that is how the workflowRun namespace lost 39 keys.
*/
function findDuplicateKeys(text) {
const stack = []
const trail = []
const duplicates = []
let index = 0
let inString = false
let escaped = false
let stringStart = -1
let expectingKey = false
while (index < text.length) {
const char = text[index]
if (inString) {
if (escaped) escaped = false
else if (char === "\\") escaped = true
else if (char === '"') {
inString = false
const raw = text.slice(stringStart + 1, index)
const top = stack[stack.length - 1]
if (expectingKey && top && !top.isArray) {
if (top.keys.has(raw)) duplicates.push([...trail, raw].join("."))
else top.keys.add(raw)
expectingKey = false
}
}
index += 1
continue
}
if (char === '"') {
inString = true
escaped = false
stringStart = index
index += 1
continue
}
if (char === "{") {
stack.push({ isArray: false, keys: new Set() })
trail.push("<object>")
expectingKey = true
} else if (char === "[") {
stack.push({ isArray: true, keys: new Set() })
trail.push("<array>")
expectingKey = false
} else if (char === "}" || char === "]") {
stack.pop()
trail.pop()
expectingKey = false
} else if (char === ",") {
const top = stack[stack.length - 1]
expectingKey = Boolean(top) && !top.isArray
} else if (char === ":") {
expectingKey = false
}
index += 1
}
return duplicates
}
test("no message bundle declares the same key twice", async () => {
for (const locale of LOCALES) {
const source = await readFile(new URL(`messages/${locale}.json`, WEB_ROOT), "utf8")
assert.deepEqual(findDuplicateKeys(source), [], `${locale}.json has duplicate keys`)
}
})
// Keys the UI resolves through t()/translateMessage(), plus the data-driven
// titleKey-style props. Scanning only t() calls misses lib/navigation.tsx,
// which is where the reported sidebar gaps lived.
const KEY_PATTERNS = [
/\b(?:t|translateCurrentMessage|translateMessage)\(\s*["'`]([A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+)["'`]/g,
/\b(?:titleKey|labelKey|descriptionKey|textKey|placeholderKey|ariaLabelKey|tooltipKey):\s*["'`]([A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+)["'`]/g,
]
test("every key the UI asks for exists in every locale", async () => {
const bundles = {}
for (const locale of LOCALES) {
bundles[locale] = JSON.parse(await readFile(new URL(`messages/${locale}.json`, WEB_ROOT), "utf8"))
}
const lookup = (bundle, key) => key.split(".").reduce((value, segment) => (value == null ? undefined : value[segment]), bundle)
const files = (await Promise.all(SOURCE_DIRS.map((dir) => collectSourceFiles(dir)))).flat()
const missing = []
for (const file of files) {
const source = await readFile(new URL(file, WEB_ROOT), "utf8")
for (const pattern of KEY_PATTERNS) {
for (const match of source.matchAll(pattern)) {
for (const locale of LOCALES) {
if (lookup(bundles[locale], match[1]) === undefined) {
missing.push(`${file}: ${match[1]} (missing in ${locale})`)
}
}
}
}
}
assert.deepEqual([...new Set(missing)], [])
})
// Template keys such as `supportPublic.comment.sort.${sort}` cannot be resolved
// by scanning source, so their known value sets are pinned here.
const DYNAMIC_KEYS = {
"supportPublic.comment.sort": ["default", "hot", "latest"],
"supportPublic.help": ["previous", "next"],
supportChat: ["connecting", "connected", "disconnected"],
agentTeamSchedule: ["weekdayMon", "weekdayTue", "weekdayWed", "weekdayThu", "weekdayFri", "weekdaySat", "weekdaySun"],
supportHelpWorkbench: [
"publishedAction",
"publishedConfirmTitle",
"publishedConfirmDescription",
"publishedSuccess",
"draftAction",
"draftConfirmTitle",
"draftConfirmDescription",
"draftSuccess",
"hiddenAction",
"hiddenConfirmTitle",
"hiddenConfirmDescription",
"hiddenSuccess",
],
}
test("every dynamically composed key variant exists in every locale", async () => {
const bundles = {}
for (const locale of LOCALES) {
bundles[locale] = JSON.parse(await readFile(new URL(`messages/${locale}.json`, WEB_ROOT), "utf8"))
}
const lookup = (bundle, key) => key.split(".").reduce((value, segment) => (value == null ? undefined : value[segment]), bundle)
const missing = []
for (const [prefix, variants] of Object.entries(DYNAMIC_KEYS)) {
for (const variant of variants) {
for (const locale of LOCALES) {
if (lookup(bundles[locale], `${prefix}.${variant}`) === undefined) {
missing.push(`${prefix}.${variant} (missing in ${locale})`)
}
}
}
}
assert.deepEqual(missing, [])
})

53
web/i18n/messages.ts Normal file
View File

@@ -0,0 +1,53 @@
import { DEFAULT_LOCALE, type AppLocale, readStoredLocale } from "@/i18n/config"
import enUSMessages from "@/messages/en-US.json"
import ruRUMessages from "@/messages/ru-RU.json"
import zhCNMessages from "@/messages/zh-CN.json"
const messages = {
"zh-CN": zhCNMessages,
"en-US": enUSMessages,
"ru-RU": ruRUMessages,
} satisfies Record<AppLocale, typeof zhCNMessages>
export function translateMessage(
locale: AppLocale,
key: string,
values?: Record<string, string | number>
): string {
const value = getMessageValue(messages[locale], key)
if (typeof value === "string") {
return formatMessage(value, values)
}
const fallback = getMessageValue(messages[DEFAULT_LOCALE], key)
return typeof fallback === "string" ? formatMessage(fallback, values) : key
}
export function translateCurrentMessage(
key: string,
values?: Record<string, string | number>
): string {
return translateMessage(readStoredLocale(), key, values)
}
function getMessageValue(source: unknown, key: string): unknown {
let current = source
for (const part of key.split(".")) {
if (!current || typeof current !== "object" || !(part in current)) {
return undefined
}
current = (current as Record<string, unknown>)[part]
}
return current
}
function formatMessage(
message: string,
values?: Record<string, string | number>
): string {
if (!values) {
return message
}
return message.replace(/\{(\w+)\}/g, (match, key) =>
Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match
)
}

87
web/i18n/provider.tsx Normal file
View File

@@ -0,0 +1,87 @@
"use client"
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react"
import {
DEFAULT_LOCALE,
type AppLocale,
configureLocale,
} from "@/i18n/config"
import { translateMessage } from "@/i18n/messages"
import { fetchPublicConfig } from "@/lib/api/config"
type LocaleContextValue = {
locale: AppLocale
setLocale: (locale: AppLocale) => void
t: (key: string, values?: Record<string, string | number>) => string
}
const LocaleContext = createContext<LocaleContextValue>({
locale: DEFAULT_LOCALE,
setLocale: () => {},
t: (key) => key,
})
export function AppI18nProvider({ children }: { children: ReactNode }) {
const [locale, setLocaleState] = useState<AppLocale>(DEFAULT_LOCALE)
const [isLocaleReady, setIsLocaleReady] = useState(false)
useEffect(() => {
let cancelled = false
fetchPublicConfig()
.then((config) => configureLocale(config.language))
.catch(() => configureLocale(DEFAULT_LOCALE))
.then((configuredLocale) => {
if (cancelled) {
return
}
setLocaleState(configuredLocale)
document.documentElement.lang = configuredLocale
document.title = translateMessage(configuredLocale, "app.metadataTitle")
setIsLocaleReady(true)
})
return () => {
cancelled = true
}
}, [])
useEffect(() => {
document.title = translateMessage(locale, "app.metadataTitle")
}, [locale])
const value = useMemo<LocaleContextValue>(
() => ({
locale,
t: (key, values) => translateMessage(locale, key, values),
setLocale: () => {},
}),
[locale]
)
if (!isLocaleReady) {
return null
}
return (
<LocaleContext.Provider value={value}>
{children}
</LocaleContext.Provider>
)
}
export function useAppLocale() {
return useContext(LocaleContext)
}
export function useI18n() {
return useContext(LocaleContext).t
}