资讯动态

ThinkRail Web 客户端 i18n 深度解析:AppConfig.locale、本地语言文件与唯一生效语言

发布时间:2026/10/27 9:57:07 来源:尧图企业网站定制
【免费下载链接】thinkrailVibe code with pi in a lightweight, real IDE that customises itself around the way you work — The Vibe You Need项目地址https://gitcode.com/GitHub_Trending/th/thinkrail点击查看免费下载本文以apps/web/src/i18n子模块设计文档为主体结合 ThinkRail 仓库中的协议定义、传输层、状态库与共享thinkrail/locales实现完整拆解“用户选择的语言 浏览器语言 内置词表 宿主本地语言文件”如何收敛成整个 Web 客户端唯一生效的一门语言。读完后你能掌握该模块的公开 API 边界、生效效应的触发条件、本地语言文件的校验规则以及新增/调试一种语言时需要检查的每一个环节。模块定位一个“只推导、不渲染”的语言解析器在 ThinkRail 的架构决策中界面文案是具名键词表而不是硬编码文本用户可以通过向全局数据目录放入语言文件来新增或覆盖语言而不是修改代码见 architecture.md Decision 23。这条决策把工作拆成了三段宿主读取并校验文件、Web 端合并并选出生效语言、组件通过useT()渲染。apps/web/src/i18n负责中间那一段。它的职责描述非常克制Turns the syncedAppConfig.localepreference, the browsers languages, the bundled catalogs, and the hosts local files into the one active language of the shared i18next instance. Components never do this themselves: they calluseT().也就是说该模块不拥有i18next 实例实例属于packages/locales的./react入口不拥有语言设置界面属于panels也不读文件属于宿主。它只做一件事从已有的状态推导出“现在该用哪个 tag、该装哪些覆盖词表”然后把结果应用出去。在 Web 模块清单里它的定位被明确标注为“有状态、跨模块”的一格apps/web/SPEC.md归属内容拥有languageOptions推导、生效 tag 推导、唯一一个应用效应公开面barreluseLocaleSync()、useSystemLanguages()、localeSignature以及纯函数languageOptions、effectiveLocale、availableLocales、localOverlays允许依赖store、thinkrail/locales、thinkrail/contracts仅类型禁止渲染 UI、读取文件、第二个 i18next 实例公开面在 apps/web/src/i18n/index.ts 中一次性导出纯函数与 hook 分文件组织推导逻辑在languages.ts副作用与订阅在useLocaleSync.ts桌面菜单标签推送在nativeMenu.ts。数据流全景三个输入源一个效应整个链路可以概括为“两个状态、一个环境、一次应用”AppConfig.locale ─┐ locales.list 快照 ─┼─▶ appStore(locale, localLocales) ─┐ navigator.languages ───────────────────────────────────┼─▶ useLocaleSync │ │ effectiveLocale / localOverlays ┘ ▼ applyLocale(tag, overlays) document.documentElement.lang tag pushNativeMenuLabels() │ ▼ useT() 的 Translator 被替换 → 全部消费者重渲染其中偏好AppConfig.locale默认值为SYSTEM_LOCALE即system见 packages/contracts/src/domain.ts。它在 welcome 快照里随配置下发落入 store 的locale字段apps/web/src/store/appStore.ts。本地快照localLocales类型为LocalLocalesSnapshot | null由传输层调用installLocalLocales写入apps/web/src/store/appStore.ts、L2295-L2297。浏览器语言navigator.languages由useSystemLanguages()通过useSyncExternalStore订阅languagechange事件获得。应用效应在main.tsx的App中挂载一次apps/web/src/main.tsx。输入源一协议协商后才有本地语言本地语言不是“连上就有”而是受协议版本门控。传输层在收到 welcome 后调用refreshLocalLocalesapps/web/src/transport/wireTransport.ts实现如下function refreshLocalLocales(connectionGeneration: number): void { const store useAppStore.getState(); if (store.protocolVersion null || store.protocolVersion LOCALES_PROTOCOL_VERSION) { store.installLocalLocales(null); return; } void getTransport() .request(locales.list, {}) .then((snapshot) { const current useAppStore.getState(); if (isConnectedGeneration(current, connectionGeneration)) current.installLocalLocales(snapshot); }) .catch(() {}); }apps/web/src/transport/wireTransport.ts这里有三个值得注意的设计点LOCALES_PROTOCOL_VERSION 89是水位线packages/contracts/src/wsProtocol.ts。旧宿主直接installLocalLocales(null)于是客户端只剩内置语言 —— 这正是文档中“an older host installsnull, so only bundled languages exist”的含义。locales.changed只是失效通知不是数据。宿主发布的是空载荷客户端收到后重新拉一次locales.listapps/web/src/transport/wireTransport.ts服务端侧发布逻辑见 packages/server/src/host/server.ts。代际校验请求返回后要用isConnectedGeneration确认连接未换代避免旧连接的快照覆盖新连接的状态。快照的结构由契约包定义packages/contracts/src/domain.tsexport interface LocalLocaleFileOk { fileName: string; tag: string; ok: true; strings: Recordstring, string; } export type LocalLocaleFileErrorCode | invalid-name | duplicate | too-large | too-many | unreadable | invalid-json | not-object; export interface LocalLocaleFileError { fileName: string; tag: string | null; ok: false; error: LocalLocaleFileErrorCode; /** Parser or OS detail for invalid-json / unreadable, untranslated. */ detail?: string; } export interface LocalLocalesSnapshot { dir: string; files: LocalLocaleFile[]; }注意error是错误码而非错误文案客户端负责把码翻译成人话settings.language.error.*detail保留未翻译的解析器/系统信息。这样同一个文件问题在不同界面语言下都能正确表达。宿主侧的读取实现保证了“一个坏文件不会拖垮其他文件”packages/server/src/locales/locales.ts大小超限MAX_LOCALE_FILE_BYTES 2 MB先判再读文件、再JSON.parse、再validateCatalog任何一步失败都变成ok: false条目MAX_LOCALE_FILES 64之后的文件统一报too-many同 tag 的重复文件报duplicateL85-L100。输入源二浏览器语言与languagechange系统语言不是快照而是实时读取function subscribeLanguages(onChange: () void): () void { window.addEventListener(languagechange, onChange); return () window.removeEventListener(languagechange, onChange); } function readLanguages(): string { return typeof navigator undefined ? : (navigator.languages ?? []).join(,); } export function useSystemLanguages(): string { return useSyncExternalStore(subscribeLanguages, readLanguages, readLanguages); }apps/web/src/i18n/useLocaleSync.ts它返回的是逗号拼接的字符串而不是数组useSyncExternalStore依赖引用相等来判定是否变化数组每次都会是新引用字符串才能稳定比较。下游在效应里用systemLanguages.split(,)还原成列表L35。typeof navigator undefined的分支让这段代码在 SSR/测试环境下也不会抛错。纯函数层languages.ts的四个推导availableLocales与localOverlaysfunction localTags(snapshot: LocalLocalesSnapshot | null): string[] { return (snapshot?.files ?? []).flatMap((file) (file.ok ? [file.tag] : [])); } export function availableLocales(snapshot: LocalLocalesSnapshot | null): string[] { return [...new Set([...BUNDLED_LOCALES, ...localTags(snapshot)])]; } export function localOverlays(snapshot: LocalLocalesSnapshot | null): Recordstring, Catalog { return Object.fromEntries( (snapshot?.files ?? []).flatMap((file) (file.ok ? [[file.tag, file.strings]] : [])), ); }apps/web/src/i18n/languages.ts可用语言 内置 tag ∪ 有效本地 tag用Set去重内置项排在前面。覆盖词表只收集ok: true的文件键是规范化后的 tag。损坏文件既不参与覆盖也不参与“可用”。两者都不缓存snapshot变了就整体重算逻辑简单且必然一致。effectiveLocale两种解析语义export function effectiveLocale( preference: string, snapshot: LocalLocalesSnapshot | null, systemLanguages: readonly string[], ): string { return resolveLocale(preference, availableLocales(snapshot), systemLanguages); }真正的规则在resolveLocalepackages/locales/src/index.tsexport function resolveLocale(preference, available, systemLanguages): string { if (preference ! SYSTEM_LOCALE) { const lower preference.toLowerCase(); return available.find((candidate) candidate.toLowerCase() lower) ?? en; } for (const language of systemLanguages) { const match matchAvailable(language, available); if (match) return match; } return en; }两条路径的严格程度不同这是实现层面的关键差异偏好值匹配方式匹配失败显式 tag如de-AT仅大小写不敏感的精确匹配直接回落ensystem逐个遍历系统语言先精确匹配再退到基础语言de-AT→de遍历完仍无命中则en也就是说显式选择de-AT而本地只有de.json时不会自动降级到de而系统模式下浏览器报告de-AT时可以命中de。单元测试把这两种语义都钉住了apps/web/src/i18n/languages.test.tsexpect(effectiveLocale(uk, snapshot, [de])).toBe(en); expect(effectiveLocale(system, snapshot, [de-AT, en])).toBe(de); expect(effectiveLocale(de, snapshot, [en])).toBe(de);languageOptions三类条目与四种来源语言设置页需要的不是 tag 列表而是带语义的选项数组。LanguageOption是一个判别联合apps/web/src/i18n/languages.tsexport type LanguageOption | { kind: language; tag: string; name: string; source: LanguageSource } | { kind: broken; fileName: string; tag: string | null; error: LocalLocaleFileErrorCode; detail?: string } | { kind: missing; tag: string };source的四种取值由“是否内置 × 是否存在本地文件”组合而成source含义界面提示键builtin仅内置词表当前只有ensettings.language.source.builtinlocal仅本地文件提供settings.language.source.localbuiltinlocal内置 本地覆盖即“内置被本地改过”settings.language.source.customized无broken文件存在但不可用随附错误码settings.language.source.broken整理逻辑还包含三个容易忽略的细节L45-L84排序按Intl.DisplayNames得到的语言自称排序languageName见 packages/locales/src/index.ts会把首字母大写。注意sort的比较函数在非language条目上返回0且broken/missing是排序后再追加的因此语言列表始终在前。missing只在必要时出现当偏好既不是system、又不在已知集合可用 tag 加上损坏文件里能解析出的 tag全部小写比较中时才补一条{ kind: missing, tag: preference }。这就是“用户选过uk但文件被删掉”的状态。broken与missing不会重复损坏文件若能解析出 tag其 tag 会进入known因此不会再额外报缺失。生效效应localeSignature与“只在真变化时应用”这是整个模块最需要精确理解的一段apps/web/src/i18n/useLocaleSync.tsexport function localeSignature(tag: string, snapshot: LocalLocalesSnapshot | null): string { const overlays localOverlays(snapshot); return JSON.stringify([tag, fallbackChain(tag).map((language) overlays[language] ?? null)]); } const INITIAL_SIGNATURE localeSignature(en, null); export function useLocaleSync(): void { const preference useAppStore((s) s.locale); const snapshot useAppStore((s) s.localLocales); const systemLanguages useSystemLanguages(); const applied useRef(INITIAL_SIGNATURE); useEffect(() { const tag effectiveLocale(preference, snapshot, systemLanguages.split(,)); const signature localeSignature(tag, snapshot); if (signature applied.current) return; applied.current signature; applyLocale(tag, localOverlays(snapshot)); document.documentElement.lang tag; pushNativeMenuLabels(); }, [preference, snapshot, systemLanguages]); }签名为什么是“tag 回退链上的词表”fallbackChain(tag)的定义是[...new Set([tag, baseLanguage(tag), en])]packages/locales/src/index.ts。因此选中de链是[de, en]签名里包含de与en的本地覆盖。选中de-AT链是[de-AT, de, en]。任何一条链上没有本地文件时对应位置为null不参与差异。由此得到文档里那句结论的直接后果编辑一个与当前回退链无关的语言文件签名不变效应虽然因snapshot引用变化而重新执行但会在第一行提前返回。测试用一个极小的断言固定了这一点apps/web/src/i18n/useLocaleSync.test.tsconst base localeSignature(de, snapshot([de])); expect(localeSignature(de, snapshot([de, fr]))).toBe(base); expect(localeSignature(de, snapshot([{ ...de, strings: { a: B } }]))).not.toBe(base); expect(localeSignature(de-AT, snapshot([de]))).not.toBe(base); expect(localeSignature(en, null)).toBe(localeSignature(en, snapshot([fr])));最后一行还说明en的回退链只有自己所以一个仅含fr的快照对英文界面的签名毫无影响。INITIAL_SIGNATURE的作用useRef的初值是localeSignature(en, null)。这带来一个具体行为如果应用启动时生效语言就是en、且没有任何本地文件首次效应会判定“无需应用”并直接返回 —— 因为英文词表在模块导入时就已经同步装好了见下一节。这避免了首帧一次多余的语言切换。效应做的四件事顺序动作失败/缺失时的表现1applyLocale(tag, localOverlays(snapshot))无覆盖时只是重置内置词表并切换语言2document.documentElement.lang tag总是写入供 CSS/无障碍/浏览器使用3pushNativeMenuLabels()无桌面桥时是 no-op4隐式发布新 translator所有useT()消费者重渲染注意第 4 步不是显式调用而是applyLocale内部的副作用见 packages/locales/src/react.tsx它先对touched ∪ 链 ∪ 覆盖键的每个语言removeResourceBundle再addResourceBundle({ ...bundled, ...overlay })然后changeLanguage(tag)并publishLocaleChange()。先删后加是必要设计本地文件被删除后其字符串必须真的消失而不能残留在 i18next 的资源里。桌面原生菜单可选桥接function menuBridge(): NativeMenuBridge | undefined { const bridge: unknown Reflect.get(globalThis, __THINKRAIL_NATIVE_MENU__); return typeof bridge object bridge ! null typeof Reflect.get(bridge, setLabels) function ? (bridge as NativeMenuBridge) : undefined; } export function pushNativeMenuLabels(): void { const keys [...DESKTOP_MENU_KEYS, ...DESKTOP_DIALOG_KEYS]; menuBridge()?.setLabels(Object.fromEntries(keys.map((key) [key, t(key)]))); }apps/web/src/i18n/nativeMenu.ts桥通过globalThis.__THINKRAIL_NATIVE_MENU__注入桌面端 preload 用Object.defineProperty定义且不可写、不可配置apps/desktop/src/preload.ts因此 Web 侧必须做运行时形状校验而不能假设它存在。推送的键是两份白名单DESKTOP_MENU_KEYSEdit/Settings/Quit/Window/Close与DESKTOP_DIALOG_KEYS退出确认、启动失败、更新重启失败及标题、启动与更新收尾的 splash 文案定义在 packages/locales/src/index.ts并用satisfies readonly (keyof EnglishCatalog)[]保证键一定存在于英文词表。这里用的是全局timport { t } from thinkrail/locales/react而非useT()的 translator因为它在效应中调用、不属于渲染期计算 —— 与“渲染期禁止读全局”的规则并不冲突。为什么英文必须先同步装好thinkrail/locales/react在模块求值时立即初始化实例packages/locales/src/react.tsxexport const i18n i18next.createInstance(); void i18n.init({ lng: en, fallbackLng: (code: string) fallbackChain(code).slice(1), load: currentOnly, resources: { en: { [NAMESPACE]: EN } }, initAsync: false, keySeparator: false, nsSeparator: false, returnEmptyString: false, interpolation: { escapeValue: false }, });initAsync: false加上内置en资源意味着首帧之前英文就可用。这正是文档中“English is installed synchronously before first paint, so nothing renders keys while the host connects”的实现依据即使宿主还没连上、locales.list还没回来界面也只会显示英文而不会闪出一片settings.language.title之类的原始键名。同一文件还规定了渲染期契约useT()是一个useSyncExternalStore订阅返回的Translator在语言或其字符串变化时被整体替换L59-L70currentLocale()/localeOf(t)/currentTranslator()则用于事件期代码。这一区分很重要原因见后文“日期与数字”。边界为什么这些事不能在本模块做不渲染 UI语言设置页在panelsapps/web/src/panels/LanguageSettings.tsx它只消费本模块的纯推导const options useMemo(() languageOptions(preference, snapshot), [preference, snapshot]); const systemTag effectiveLocale(SYSTEM_LOCALE, snapshot, systemLanguages); const folder snapshot?.dir;选择语言时它并不直接改 store而是发 RPCrequest(settings.update, { config: { locale: next } })失败才用toast.error提示L95-L100。“打开文件夹”同理走locales.reveal服务端会先mkdirSync再调用文件管理器packages/server/src/host/handlers.ts。这种拆分保证了展示逻辑禁用态、来源提示、错误文案与语言解析逻辑谁是可用语言、谁是生效语言互不污染也让纯函数可以被单元测试直接覆盖无需渲染整棵组件树。不读文件文件读取、大小上限、轮询检测都在宿主packages/server/src/locales/SPEC.md 与 locales.ts。宿主用每秒一次的stat签名比较name、inode、size、mtime、ctime而不是fs.watch并在变化时只发一个“失效”通知。Web 侧因此永远是“请求—替换快照”的单向流没有本地文件句柄、没有路径知识。不允许第二个 i18next 实例packages/locales/react只createInstance()一次并导出单例。如果 Web 端另起实例applyLocale的替换与useT()的订阅就不再指向同一个对象切换语言会出现“部分界面变了、部分界面没变”的割裂状态。契约包与 locales 包之间的分工也说明了这点contracts只放数据类型翻译规则放在locales根入口。四条“必须做对”的规则逐条拆解1. 只在真实变化时应用文档原文是效应在localeSignature生效 tag 加上恰好其回退链上的字符串变化时才应用无关文件的编辑或重复的locales.list什么都不做。实现上还有一层隐含成本考量applyLocale会重建 translator 并通知所有订阅者等于一次全量重渲染。用签名把“数据变化”收窄为“语义变化”避免了每次轮询/每次推送导致的全应用重渲染。副作用是useLocaleSync自身每次效应都会JSON.stringify一次签名这是有意的取舍 —— 相比全量重渲染签名计算的代价可以忽略。2. 英文先于首帧、本地字符串后来就位英文同步安装上一节本地字符串在locales.list返回后通过同一次效应“就地替换”。因此不存在“整页重挂载”的方案 ——packages/locales的设计说明里也明确否决了 remount重挂载会丢草稿附件、计划输入并重跑终端附着等生命周期副作用。3. 选中的本地 tag 消失显示英文偏好不动这条规则把“用户意图”和“当前可用资源”分开languageOptions会产出一条{ kind: missing, tag }设置页把它标为激活且禁用choiceId生成missing:tag见 apps/web/src/panels/LanguageSettings.tsx、L191-L193。effectiveLocale因为精确匹配失败返回en界面回落英文。store 里的preference从未被改写所以文件回来后下一次locales.changed→ 新快照签名变化语言自动恢复。端到端测试完整验证了这条闭环e2e/language-settings.spec.tsremoveLocale(de.json); await expect(page.getByTestId(language-missing-de)).toHaveAttribute(data-active, true); await expect(page.locator(html)).toHaveAttribute(lang, en); await expect(page.getByTestId(settings-nav-language)).toContainText(Language); writeLocale(de.json, JSON.stringify({ settings.nav.language: Sprache })); await expect(page.getByTestId(language-de)).toHaveAttribute(data-active, true); await expect(page.locator(html)).toHaveAttribute(lang, de);注意第二条断言里删掉文件后html lang变回en—— 这正是效应把document.documentElement.lang与生效 tag 同步的结果页面初始值为en见 apps/web/index.html。4. 日期与数字用currentLocale()文档要求日期和数字必须用currentLocale()不能硬编码en-US也不能直接用浏览器默认值。原因是格式化必须与当前生效语言一致而不是与浏览器语言一致 —— 用户可能把界面切成de但系统语言仍是zh-CN。配套的渲染期规则来自packages/locales渲染期组件体、hook 体、useMemo/useState初始化器、.map回调、store selector 及其可达辅助函数必须把useT()返回的Translator作为参数传入而不是读全局t。因为 React Compiler 会按输入缓存渲染表达式读全局的 helper 在语言切换后仍会返回旧语言的字符串。localeOf(t)专门用于“在某次渲染里按该 translator 的语言做Intl格式化”。词表规范与本地文件实战虽然apps/web/src/i18n只做推导但要让它“有东西可推导”本地文件必须通过共享的校验规则。规则实现在thinkrail/locales根入口浏览器与服务器共用见 packages/locales/src/index.ts由宿主在读取时执行。文件放在哪里、叫什么位置全局数据目录下的locales/即localesDir()返回的dataDir/localespackages/server/src/locales/locales.ts仓库贡献文档写作~/.thinkrail/locales/CONTRIBUTING.md。它永远不是工作区目录避免克隆下来的仓库改写用户界面。命名localeTagFromFileName只接受.json、非点号开头、且主干匹配^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$的文件名再经Intl.getCanonicalLocales规范化de.json→dept-BR.json合法backup.json.bak、.de.json、my-lang.json均返回null进而报invalid-name。值的取舍一个“保守但安静”的校验器validateCatalog的返回值只有两种{ ok: true, strings }或{ ok: false, error: not-object }。除了根不是对象其余问题都是静默丢弃单条而不是整份文件报错情形处理值不是字符串或为空字符串丢弃该键界面显示英文值包含$t(嵌套丢弃该键{{placeholder}}集合与英文不一致丢弃该键标签集合与英文不一致或用了带属性/具名标签丢弃该键紧跟{{丢弃该键防止插值拼出标签未知键保留为数据文件可能比应用新或旧键名是constructor/__proto__作为普通键处理只用自有属性查找其中“标签集合”的检查非常严格markup()只允许0、/0、1/这类裸索引标记且排序后比较集合。设计动机在模块说明里写得很直接词表不得覆盖代码所拥有组件的 props例如链接的href—— 否则一个语言文件就能改变链接指向。占位符检查对复数键做了特殊处理key_category与英文key_other比较且{{count}}允许出现或不出现这样译者可以选择显示或省略数字L100-L120。复数用 CLDR 类别缺的用_other补齐completePlurals(tag, strings)会找出词表中所有“英文有对应复数家族”的键按Intl.PluralRules(tag)解析该语言需要的类别把缺失类别用本家族的_other否则任一已存在形式填充L130-L150。applyLocale对每个覆盖词表都调用它packages/locales/src/react.tsx。效果是俄语这类需要_one/_few/_many/_other的语言译者只写三四种形式计数器也不会突然掉回英文。贡献文档给出的形式是CONTRIBUTING.md{ tools.read.firstLines_one: первая {{count}} строка, tools.read.firstLines_few: первые {{count}} строки, tools.read.firstLines_many: первые {{count}} строк }键的类型层也做了配合MessageKey会把_zero/_one/… 折叠回基键因此代码里t(tools.read.firstLines)只需引用基键复数后缀由 i18next 运行时解析。最小可运行示例在语言设置页“Add a language”区块的描述里settings.language.add.steps见 packages/locales/en.json已经把步骤写清楚下载en.json、按语言码命名、翻译值、保存即生效。一个覆盖少量键的de.json{ settings.language.title: Sprache, settings.nav.language: Sprache, settings.language.add.steps: Lade 0en.json/0 herunter und übersetze die Werte., workbench.tool.files: Dateien }注意第三条保留0…/0且位置对应否则按“标签集合不一致”被丢弃。这个片段与 e2e 用例中使用的数据一致e2e/language-settings.spec.ts。质量闸门为什么“新文案不可能漏翻译”bun run check:i18n串起三道检查package.jsoncheck:i18n: bun test scripts/checkI18n.test.ts scripts/i18nRenderReach.test.ts bun scripts/check-i18n.ts硬编码文案检测扫描apps/web/src、packages/ui、packages/resource-renderers/src、thinkrail-extensions对 JSX 文本、aria-label/title/placeholder/alt等文本属性、toast.error(...)与errorText(...)的文案参数报错scripts/check-i18n.ts、L130-L180。白名单只有ThinkRail。词表自身健康en.json键必须有序、值必须是非空字符串、每个键去掉复数后缀必须在消费方源码中被引用L220-L239。渲染期全局 translator 检测i18nRenderReach从t/i18n/currentLocale/currentTranslator这些“全局读取器”出发追踪其在渲染期同步回调useAppStore、useMemo、map、filter、getter 等中的可达性scripts/i18nRenderReach.ts。第 3 项是把“语言切换后部分文案不更新”这类只有运行时才暴露的 bug 前移到静态检查。它的必要性来自applyLocale的机制切换语言并不重挂载组件树只替换 translator 值 —— 因此任何绕过useT()的渲染期读取都会拿到旧值。测试与可验证行为该模块的测试分布在三层覆盖点与文档条目一一对应测试覆盖的行为apps/web/src/i18n/languages.test.ts可用语言 内置 ∪ 有效本地来源标记含builtinlocal损坏文件出现在末尾missing仅在偏好不可用时出现系统模式的基础语言降级apps/web/src/i18n/useLocaleSync.test.ts签名只随 tag 与其回退链字符串变化en不受仅含fr的快照影响apps/web/src/i18n/nativeMenu.test.ts有桥时推送当前语言的菜单与对话框标签链尾未翻译的键回落英文无桥时不抛错e2e/language-settings.spec.ts本地文件列出/可选/热更新/删除后回落/恢复html lang同步缓存标签、T富文本、已挂载编辑器的控件都随语言切换重渲染其中 e2e 的第一个用例还验证了一个容易被忽略的行为写入非法 JSON 的fr.json后设置页出现language-broken-fr.json条目且输入框禁用L64-L67—— 坏文件既不能选也不会影响de的使用。已知限制packages/locales与apps/web/src/i18n的设计文档都把以下条目列为明确的延后项在规划自建语言方案时应当把它们当作当前事实边界内置非英文语言BUNDLED映射目前只有enpackages/locales/src/index.ts新增内置语言需要改这张表并通过构建进入产物。首帧本地语言在宿主返回locales.list之前即使偏好是de界面也会短暂显示英文。这是“英文同步安装、本地稍后到达”的必然结果。RTL 布局dir属性未处理只设置了lang。服务端错误文案错误文本不翻译仍按原样展示若要做多语言需先迁移为错误码。语言感知的数字格式token 计数、成本、上下文百分比仍使用英文分隔符。复合短语嵌入已翻译计数片段的句子例如“1 skill”列表尚未组件化。这些限制都不影响本地语言文件的完整能力增删改文件、热更新、坏文件隔离、删除后回落英文并保留偏好均已由上述源码与测试确认。小结apps/web/src/i18n的价值在于把“多源输入”压缩成“一个可比较的签名 一次幂等的应用”输入侧偏好来自AppConfig.locale本地词表来自受协议版本门控的locales.list快照系统语言来自navigator.languages的实时订阅推导侧四个纯函数把快照整理成可用 tag、覆盖词表、生效 tag 与选项列表其中显式偏好只做精确匹配、system才允许基础语言降级应用侧localeSignature用“tag 恰好其回退链上的字符串”判定真实变化然后用applyLocalehtml lang 原生菜单标签完成一次原子更新约束侧模块不渲染、不读文件、不新建 i18next 实例把 UI 交给panels、文件交给宿主、实例交给thinkrail/locales/react并由check:i18n的三道闸门守住“没有硬编码文案、词表健康、渲染期不读全局 translator”。把这四条串起来就能解释为什么在 ThinkRail 里改一个de.json并保存后界面会就地换语言而不丢草稿也能解释为什么删掉它之后界面会退回英文、而设置里仍然记得你选过de。赞分享【免费下载链接】thinkrailVibe code with pi in a lightweight, real IDE that customises itself around the way you work — The Vibe You Need项目地址https://gitcode.com/GitHub_Trending/th/thinkrail点击查看免费下载相关推荐Conductor多语言客户端与SDK深度解析Conductor多语言客户端与SDK深度解析 本文深入解析了Conductor多语言客户端与SDK的核心架构和实现细节。首先详细介绍了Java SDK的核心接工作流自动化任务调度后端LibreChat 多语言本地化新增语言全流程与 i18n 底层机制深度解析LibreChat 多语言本地化新增语言全流程与 i18n 底层机制深度解析 本篇以 LibreChat 仓库中的 本地化指南 https://link.gi人工智能AI 应用大模型后端前端AI AgentRAGTinode 多语言本地化i18n完整指南服务端邮件/SMS 模板与各端客户端翻译实践Tinode 多语言本地化i18n完整指南服务端邮件/SMS 模板与各端客户端翻译实践 本指南围绕 docs/translations.md https:即时通讯后端WebSocket认证鉴权可观测性上一篇Translumo打破语言障碍的实时屏幕翻译神器让你畅玩全球游戏下一篇KMS智能激活工具终极指南如何一键永久激活Windows和Office创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑