面向聊天生成应用的国际化架构
面向聊天生成应用的国际化架构:定义稳定的字符串键、复数规则,以及在 Web 与移动端一致的翻译工作流。

添加更多语言时首先先坏掉的是什么
首先出问题的不是代码,而是文字。
聊天生成的应用通常以快速原型开始:你打出“加一个写着 Save 的按钮”,UI 就出现了,然后继续做别的事。几周后,你想要西班牙语和德语,却发现那些“临时”标签散落在屏幕、组件、邮件和错误信息中。
文案的改动也比代码更频繁。产品名称被重命名、法律文本变更、引导修改、支持团队要求更清晰的错误提示。如果文本直接写在 UI 代码里,每次小的措辞改动都会变成一次有风险的发布,同时你也会错过那些把相同概念用不同说法表述的地方。
以下是表明你正在积累翻译债务的早期症状:
- 同一屏幕出现混合语言(有些字符串已翻译,其他仍为英文)。
- 相同标签被重复但有细微差别(“Sign up”、“Sign Up”、“Create account”)。
- 文本变长时布局被破坏(按钮溢出、标题换行不佳)。
- 移动端和 Web 端逐渐不同步(同一操作使用了不同的措辞)。
- 支持内容和系统消息落后于 UI。
一个现实例子:你在 Koder.ai 中构建了一个简单的 CRM。Web 应用显示 “Deal stage”,移动端显示 “Pipeline step”,而错误 toast 显示 “Invalid status”。即便三处都被翻译了,用户仍会感到不一致,因为概念不匹配。
“统一”并不意味着“到处字符都完全相同”。它意味着:
- 同一个概念在各处使用相同的键和相同的含义。
- Web 与移动端共享同一份翻译真相来源。
- 语气与术语稳定(正式或随意,“customer” 与 “client” 的选择)。
- UI 布局能适应更长或更短的短语。
一旦你把文本当作产品数据而不是装饰,添加语言就不再是仓促应对,而是构建流程中的常规环节。
基本概念与一个简单目标
Internationalization(i18n)是为了让应用支持多种语言而进行的工作,不需要重写代码。Localization(l10n)是为特定语言与地区提供的实际内容,例如使用恰当措辞、日期格式与语气的法语(加拿大)。
一个简单的目标:所有面向用户的文本都由稳定的键选取,而不是直接写入 UI 代码。如果你可以在不打开 React 组件或 Flutter Widget 的情况下修改一句话,那你就走在正确的路上。这是面向聊天生成应用的国际化架构的核心,因为在聊天会话中很容易意外地生成并硬编码文案。
面向用户的文本比大多数团队想象的要广:按钮、标签、验证错误、空状态、引导提示、推送通知、邮件、PDF 导出以及用户能看见或听到的任何消息。通常不包括内部日志、数据库列名、分析事件 ID、功能开关或仅供管理员的调试输出。
翻译应当放在哪里?实际上通常既在前端也在后端,并需有明确边界。
- 前端负责 UI 外壳:导航、表单、菜单和大部分屏幕文本。
- 后端负责它生成的消息:事务邮件、服务端验证错误以及作为错误响应返回的内容。
- 共享领域(如订单状态标签)应来自同一份真相来源,以免 Web 和移动端发生漂移。
要避免的错误是职责混淆。如果后端返回完整的英文句子作为 UI 错误,前端就无法干净地本地化。更好的模式是:后端返回一个错误代码(以及安全的参数),客户端根据该代码映射为本地化信息。
文案归属是产品决策,而非技术细节。尽早决定谁可以更改文字并批准语气。
如果产品负责文案,把翻译当作内容管理:版本化、审校,并给产品一条安全的修改请求路径。如果工程负责文案,设定规则:任何新的 UI 字符串在发布前必须附带一个键和默认翻译。
示例:如果你的注册流程在三个不同屏幕都写着 “Create account”,就让它成为一个在各处复用的键。这样能保持含义一致,加快翻译速度,并防止小的措辞变化演变成多屏幕的大量清理工作。
如何构造稳定的字符串键
键是 UI 与翻译之间的契约。如果这份契约一直在变动,你就会遇到缺失文本、匆忙修复以及 Web 与移动端措辞不一致。面向聊天生成应用的良好国际化架构从一条规则开始:键应描述含义,而不是当前的英文句子。
使用稳定的 ID 作为键(例如 billing.invoice.payNow),而不是完整文案(例如 "Pay now")。以句子作为键会在有人修改措辞、添加标点或更改大小写时崩坏。
一个既可读又实用的模式是:屏幕(或域) + 组件 + 意图。保持枯燥且可预测。
示例:
auth.login.titleauth.login.emailLabelbilling.checkout.payButtonnav.settingserrors.network.offline
决定何时重用键与何时创建新键时,问自己:“在每个地方含义是否完全相同?”对真正通用的动作重用键,但当上下文变化时拆分键。例如,个人资料屏幕里的 “Save” 可能是简单操作,而复杂编辑器中的 “Save” 在某些语言中可能需要更具体的语气。
将共享 UI 文本放在专门的命名空间中,避免在不同屏幕重复。常见的分组:
common.actions.*(保存、取消、删除)common.status.*(加载中、成功)common.fields.*(搜索、密码)errors.*(验证、网络)nav.*(标签、菜单项)
当措辞改变但含义不变时,保留键并只更新翻译值。这就是稳定 ID 的目的。如果含义发生变化(即便是细微变化),创建一个新键并保留旧键直到确认不再被使用。这可以避免旧翻译在技术上存在但语义错误的“沉默”不匹配。
一个来自 Koder.ai 风格流程的小例子:你的聊天生成了 React Web 应用和 Flutter 移动应用。如果两者都使用 common.actions.save,你将获得一致的翻译。但如果 Web 使用 profile.save 而移动使用 account.saveButton,即便英文现在看起来相同,随着时间推移你也会出现分歧。
字符串放在哪里以及如何组织翻译文件
把源语言(通常是英语)视为唯一真相来源。将其保存在一个地方,像审查代码一样审查它,并避免让字符串出现在随机组件中“临时使用”。这是避免硬编码 UI 文案并减少后期返工的最快方式。
一个简单规则有用:应用只能显示来自 i18n 系统的文本。如果有人需要新文案,他们先添加键和默认消息,然后在 UI 中使用该键。即便功能迁移,这也能保持面向聊天生成应用的国际化架构稳定。
一种能保持整洁的文件夹布局
如果你同时发布 Web 与移动端,想要一套共享键目录,同时又希望功能团队有各自工作的空间,以下布局实用:
- /i18n
- /i18n/locales/en.json(源)
- /i18n/locales/es.json、/i18n/locales/fr.json、...
- /i18n/features/billing.json、/i18n/features/auth.json、...
- /i18n/shared.json(按钮、通用标签、错误)
在不同平台保持相同的键名,即便实现不同(Web 上是 React,移动上是 Flutter)。如果你使用像 Koder.ai 的平台从聊天生成两个应用,当两个项目都指向相同的键名和相同的消息格式时,导出源码后更容易维护。
防止翻译债务的版本控制与审查
翻译会随时间变化。把改动当作产品改动:小、受审、可追踪。一次好的审查侧重于含义与复用,而非仅仅拼写。
- 要求对源语言的任何变更通过 PR 审查
- 阻止在没有搜索与计划的情况下删除键
- 在文本含糊时为翻译者添加注释
- 在 CI 中使用简单的“缺失翻译”检查
为防止键在团队间漂移,让键归属于功能(billing.、auth.),且不要因为措辞变化而重命名键。更新消息,保留键。键是标识符,不是文案。
不靠技巧的复数与语法处理
每种语言的复数规则不同,因此简单的英语模式(1 与其他)很快会失效。有些语言对 0、1、2-4 有不同形式,另一些语言会改变整个句子而不仅仅是名词。如果你把复数逻辑写死在 UI 里,最终会重复文案并漏掉边缘情况。
更安全的做法是为一个概念保留一个灵活的消息,让 i18n 层选择正确形式。ICU 风格的消息正是为此设计。它将语法决策放在翻译中,而不是组件里。
下面是人们常忘记的一个小例子:
\nitemsCount = "{count, plural, =0 {No items} one {# item} other {# items}}"\n
这个单一键覆盖了 0、1 与其他情况。翻译者可以为他们的语言替换合适的复数形式,而你无需改动代码。
当你需要基于性别或角色的措辞时,避免创建 welcome_male 与 welcome_female 之类的独立键,除非产品确实需要。使用 select,让句子保持为一个整体:
\nwelcomeUser = "{gender, select, female {Welcome, Ms. {name}} male {Welcome, Mr. {name}} other {Welcome, {name}}}"\n
为了避免被语法格(cases)限制住,尽量让句子尽可能完整。不要把片段拼接在一起,例如 "{count} " + t('items'),因为许多语言无法按这种方式重排词序。优先使用包含数字、名词与周围词语的完整消息。
在聊天生成的应用(包括 Koder.ai 项目)中,一个简单规则是:如果句子包含数字、人员或状态,从第一天起就以 ICU 方式处理。这样会在前期多花一点功夫,但能节省大量翻译债务。
让 Web 与移动端的翻译保持一致
如果你的 React Web 应用和 Flutter 移动应用各自维护翻译文件,它们会发生漂移。相同的按钮会出现不同措辞,键在 Web 上代表一个意思而在移动端代表另一个,支持票据开始抱怨“App 说 X,但网站说 Y”。
最简单且最重要的修复是:选择一个真相来源格式并像对待代码一样对待它。对大多数团队而言,这意味着一套共享的 locale 文件(例如使用 ICU 风格消息的 JSON),被 Web 与移动端共同消费。在通过聊天与生成器构建应用时,这一点尤为重要,因为很容易在两个地方意外创建新文本。
一个共享真相来源
一个实用设置是建立一个小的 “i18n 包” 或文件夹,包含:
- 每种语言的 locale 文件(所有地方都使用相同键)
- 消息格式规则(用于复数与占位符的 ICU)
- 一个简短的 README 说明如何添加键
React 与 Flutter 成为使用者。它们不应在本地发明新键。在 Koder.ai 风格的工作流中(React Web、Flutter 移动),你可以用同一套键集生成两个客户端,并像处理其他代码改动一样对变更进行审查。
后端的对齐也是同一故事的一部分。错误、通知与邮件不应在 Go 中写成手工的英文句子。相反,返回稳定的错误代码(如 auth.invalid_password)以及安全参数。然后客户端将错误代码映射为翻译文本。对于服务器发送的邮件,服务器可以使用相同的键与 locale 文件渲染模板。
保持同步的规则
制定一小段规则书并在代码审查中强制执行:
- 新的 UI 文本首先需要出现在共享 locale 文件中
- 键必须包含清晰的命名空间与意图(而非屏幕位置)
- 每个键的占位符须在一处定义并在各处以相同方式使用
- 若两句短语含义不同,绝不可共用一个键
- 若两个键含义相同,删除其中一个并选定一个统一键
为防止含义接近但不同的重复键,为翻译者和未来的你添加 “说明” 字段(或注释文件)。例如:billing.trial_days_left 应说明它是作为横幅、邮件还是两者显示。那一句话通常能阻止“差不多可复用”的误用,从而产生翻译债务。
这种一致性是面向聊天生成应用的国际化架构的基石:一套共享词汇表,多种呈现面,在发布下一种语言时不会出现意外。
可在真实项目上遵循的逐步设置
面向聊天生成应用的良好国际化架构从简单开始:一套消息键、一份文案真相来源,以及 Web 与移动端统一的规则。如果你以快速方式构建(例如使用 Koder.ai),这种结构能在保持速度的同时避免形成翻译债务。
一个实用的设置(Web + 移动)
尽早选择支持的语言,并决定缺失翻译时的处理方式。一个常见选择是:当用户偏好语言可用时显示偏好语言,否则回退到英文,并记录缺失键以便在下次发布前修复。
然后按下列步骤执行:
- 定义 locales 与回退策略: 决定支持的语言、默认语言与回退顺序。并约定如何检测语言(浏览器/应用设置、用户资料)。
- 创建翻译函数与键约定: 使用稳定、基于含义的键(而非整句英文)。例如:
billing.plan_name.pro或auth.error.invalid_password。在各处使用相同键。 - 在 React 与 Flutter 中接入: 在 React 中用 i18n provider 包裹应用,并在组件中使用
t("key")。在 Flutter 中使用本地化包装器并在 Widget 中调用同样的键查找。目标是相同的键,而不是相同的库。 - 从第一天起支持变量与复数规则: 使用 ICU 风格的消息处理复数与占位符,例如 “{count, plural, one {# file} other {# files}}” 与 “Hello, {name}”。这避免了
if (count === 1)式的零散逻辑。 - 添加轻量的文案审查步骤: 在发布前审查新增或更改的字符串:检查键命名、移除硬编码 UI 文案、确认占位符一致,并确保 Web 与移动端都拾取到了变更。
最后,用一个词更长的语言(德语是经典示例)和一种标点不同的语言做测试。这样能快速暴露按钮溢出、标题换行差以及假设英文词长的布局问题。
如果你把翻译放在共享文件夹(或生成的包)里,并把文案变更当作代码变更来管理,Web 与移动端即便通过聊天快速生成,也能保持一致。
动态内容:日期、数字与用户文本
被翻译的 UI 字符串只是问题的一半。大多数应用还显示会变化的数值,如日期、价格、计数和姓名。如果把这些值当作纯文本,你会遇到奇怪的格式、错误的时区,以及在多种语言中听起来不自然的句子。
首先用本地化规则格式化数字、货币与日期,而不是自写格式器。法国用户预期 "1 234,50 €",美国用户则预期 "$1,234.50"。日期亦然:"03/04/2026" 易产生歧义,而按 locale 格式化能让其清晰。
时区也是陷阱。服务器通常以中性形式存储时间戳(通常为 UTC),但用户期望看到其本地时区时间。例如:某订单在 23:30 UTC 创建,对于东京的用户可能显示为“明天”。为每个屏幕决定一条规则:个人事件显示用户本地时间;像取货时间这类与营业相关的时间可显示固定业务时区并明确标注。
避免通过拼接翻译片段来构造句子。它会破坏语法,因为词序在不同语言中可能不同。不要写:
"{count} " + t("items") + " " + t("in_cart")
而应使用一个带占位符的完整消息,例如:"{count} items in your cart"。翻译者可安全地重排词序。
从右到左(RTL)的语言
RTL 不仅是文本方向。布局流需要翻转,有些图标需镜像(如返回箭头),混合内容(阿拉伯语加英文产品编码)可能呈现出令人意外的顺序。测试真实屏幕而非单个标签,确保 UI 组件支持方向变更。
用户生成内容
绝不要翻译用户写的内容(姓名、地址、支持工单、聊天消息)。你可以翻译环绕它的标签,并按 locale 格式化相关元数据(日期、数字),但内容本身必须保持原样。如果日后加入自动翻译,把它做成显式功能并提供“原文/译文”切换。
一个实用示例:Koder.ai 构建的应用可能显示 “{name} renewed on {date} for {amount}”。把它保持为一个消息,按 locale 格式化 {date} 与 {amount},并以用户时区显示。这一个模式能避免大量翻译债务。
快速规则通常能防止 bug:
- 将时间戳存为 UTC,为查看者按其 locale 与时区格式化。
- 在完整句子内使用占位符,绝不拼接片段。
- 按 locale 规则与正确货币代码格式化货币。
- 至少在真实屏幕上测试一个 RTL 语言。
- 默认不翻译用户生成文本。
导致翻译债务的常见错误
翻译债务通常以“只改一个字符串”为开端,演变成后续数周的清理工作。在聊天生成的项目中,这种情况会更快出现,因为 UI 文本可能直接在组件、表单甚至后端生成。
后期更伤的那些问题
最昂贵的问题是那些在应用中扩散并难以定位的。
- 在 UI 组件中硬编码文案,包括占位符、空状态与按钮标签。你无法审计或复用它,且小的措辞变更需要改代码。
- 让后端验证与 API 错误返回英文句子。UI 会出现混合语言,你无法可靠地将错误映射到翻译消息。
- 使用整句英文作为翻译键。开始时便捷,但当文案改变时会导致键频繁变更、旧键残留并丢失翻译记忆。
- 在 Web 与移动端拷贝键并分别编辑。随着时间推移,“相同”屏幕会漂移,用户会注意到措辞不一致。
- 把复数规则拖到添加第一种非英语语言之后再做。到那时你会发现大量 “1 items” 的 bug 和无法用简单 if 语句修复的尴尬语法。
一个真实场景的快速示例
想象一个 React Web 应用和一个 Flutter 移动应用都显示计费横幅:“You have 1 free credit left”。有人把 Web 文本改为 “You have one credit remaining” 并把键仍设为整句。移动端仍使用旧键。现在你有两个键代表同一概念,翻译者会看到并为两者分别翻译。
更好的模式是使用稳定键(例如 billing.creditsRemaining)并用 ICU 消息处理复数,以便在各语言中语法正确。如果你使用像 Koder.ai 的即写即生成工具,早早添加一条规则:任何在聊天中生成的面向用户文本都应落入翻译文件,而不是写在组件或服务器错误里。这个小习惯能保护你的面向聊天生成应用的国际化架构随着项目增长仍然健康。
快速核对清单、一个现实例子与下一步
当国际化显得混乱时,通常是因为基础规则从未被写下来。一个小清单和一个具体示例能让你的团队(以及未来的你)远离翻译债务。
下面是在每个新屏幕上可执行的快速清单:
- 稳定键: 每个含义一个键,而非每种措辞一个键(示例:
billing.invoice.paidStatus,而不是billing.greenLabel)。 - 回退策略: 定义清晰的默认语言,并决定缺失键时的处理(显示回退文本、记录日志或阻止发布)。
- 复数规则: 使用 ICU 风格消息处理计数(0、1、多)而非字符串技巧。
- 格式化: 按 locale 格式化日期、金额与数字(货币与语言分离)。
- RTL 检查: 及早测试至少一种从右到左的语言,以便在你拥有 200 屏前发现布局问题。
一个简单示例:你要在英语、西班牙语与日语中发布计费屏。UI 包含:“Invoice”、“Paid”、“Due in 3 days”、“1 payment method / 2 payment methods” 以及总额如 “$1,234.50”。如果你用面向聊天生成应用的国际化架构构建它,键只需定义一次(在 Web 与移动端共享),每种语言只填充对应值。“Due in {days} days” 成为 ICU 消息,金额由按 locale 的格式化器处理,而非硬编码逗号。
按功能逐步推出语言,而非一次性大改:
- 从一个高流量流程开始(计费、引导或结账)。
- 将硬编码的 UI 文案移入翻译文件,并替换为键。
- 为同一流程添加复数消息与格式化支持。
- 在缺失键接近为零后再扩展到下一个功能。
记录两件事以保持新功能一致:你的键命名规则(含示例)以及字符串的“完成定义”(无硬编码文案、复数采用 ICU、日期/数字已格式化、已加入共享目录)。
下一步:如果你在 Koder.ai 上构建,使用 Planning Mode 在生成 UI 之前定义屏幕与键。然后用快照与回滚安全地迭代文案与翻译,避免造成损坏的发布。
常见问题
翻译键应该如何命名?
使用稳定、基于含义的键名,例如 billing.invoice.payNow,而不是直接使用英文句子。这样你可以修改措辞,而无需更改代码引用或创建重复翻译。
哪些文本需要放入翻译文件?
将所有面向用户的字符串都放入翻译系统,包括错误信息、空状态、电子邮件、通知和引导提示。内部日志、分析 ID 和数据库字段通常无需翻译。
如何保持网页端和移动端的文案一致?
为两个客户端共用一套键目录和语言消息。React 和 Flutter 可以使用不同的库,但应引用相同的键名并遵循相同的消息规则。
后端应该返回已翻译的错误信息吗?
让后端返回稳定的错误代码和安全的值,再由客户端显示本地化消息。例如,返回 auth.invalid_password,而不是一句英文提示。
应该如何处理不同语言中的复数?
使用 ICU 风格的复数消息,让每种语言应用自己的语法规则。将完整句子放在一条消息中,而不是把数字与单独翻译的名词拼接起来。
翻译文件应该放在哪里?
将源语言环境(通常是英语)作为唯一事实来源,并与代码一起审查变更。按功能组织消息,并为常用操作、字段、导航和错误预留共享命名空间。
每次文案变更都需要新建键吗?
如果含义不变,即使措辞改变也保留现有键。只有在含义或上下文改变时才创建新键,然后在确认没有任何地方使用旧键后将其删除。
应该如何本地化日期、货币和时区?
以 UTC 存储时间戳,然后根据查看者的语言环境和时区格式化日期、时间、数字和货币。使用支持语言环境的格式化工具,不要硬编码逗号、货币符号或日期格式。
对于从右到左书写的语言,应该测试什么?
在 RTL 语言环境中设计完整页面,而不只是标签。必要时翻转布局方向,镜像返回箭头等具有方向性的图标,并测试产品代码等混合的阿拉伯文和拉丁文本。
为现有应用添加 i18n 最简单的方法是什么?
从一个使用频繁的流程开始,例如引导、账单或结账。将其中硬编码的文案移入共享消息,添加复数和格式化支持,测试更长的翻译,然后对下一个功能重复这一过程。