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

首先出问题的不是代码,而是文字。
聊天生成的应用通常以快速原型开始:你打出“加一个写着 Save 的按钮”,UI 就出现了,然后继续做别的事。几周后,你想要西班牙语和德语,却发现那些“临时”标签散落在屏幕、组件、邮件和错误信息中。
文案的改动也比代码更频繁。产品名称被重命名、法律文本变更、引导修改、支持团队要求更清晰的错误提示。如果文本直接写在 UI 代码里,每次小的措辞改动都会变成一次有风险的发布,同时你也会错过那些把相同概念用不同说法表述的地方。
以下是表明你正在积累翻译债务的早期症状:
一个现实例子:你在 Koder.ai 中构建了一个简单的 CRM。Web 应用显示 “Deal stage”,移动端显示 “Pipeline step”,而错误 toast 显示 “Invalid status”。即便三处都被翻译了,用户仍会感到不一致,因为概念不匹配。
“统一”并不意味着“到处字符都完全相同”。它意味着:
一旦你把文本当作产品数据而不是装饰,添加语言就不再是仓促应对,而是构建流程中的常规环节。
Internationalization(i18n)是为了让应用支持多种语言而进行的工作,不需要重写代码。Localization(l10n)是为特定语言与地区提供的实际内容,例如使用恰当措辞、日期格式与语气的法语(加拿大)。
一个简单的目标:所有面向用户的文本都由稳定的键选取,而不是直接写入 UI 代码。如果你可以在不打开 React 组件或 Flutter Widget 的情况下修改一句话,那你就走在正确的路上。这是面向聊天生成应用的国际化架构的核心,因为在聊天会话中很容易意外地生成并硬编码文案。
面向用户的文本比大多数团队想象的要广:按钮、标签、验证错误、空状态、引导提示、推送通知、邮件、PDF 导出以及用户能看见或听到的任何消息。通常不包括内部日志、数据库列名、分析事件 ID、功能开关或仅供管理员的调试输出。
翻译应当放在哪里?实际上通常既在前端也在后端,并需有明确边界。
要避免的错误是职责混淆。如果后端返回完整的英文句子作为 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 与移动端,想要一套共享键目录,同时又希望功能团队有各自工作的空间,以下布局实用:
在不同平台保持相同的键名,即便实现不同(Web 上是 React,移动上是 Flutter)。如果你使用像 Koder.ai 的平台从聊天生成两个应用,当两个项目都指向相同的键名和相同的消息格式时,导出源码后更容易维护。
翻译会随时间变化。把改动当作产品改动:小、受审、可追踪。一次好的审查侧重于含义与复用,而非仅仅拼写。
为防止键在团队间漂移,让键归属于功能(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 方式处理。这样会在前期多花一点功夫,但能节省大量翻译债务。
如果你的 React Web 应用和 Flutter 移动应用各自维护翻译文件,它们会发生漂移。相同的按钮会出现不同措辞,键在 Web 上代表一个意思而在移动端代表另一个,支持票据开始抱怨“App 说 X,但网站说 Y”。
最简单且最重要的修复是:选择一个真相来源格式并像对待代码一样对待它。对大多数团队而言,这意味着一套共享的 locale 文件(例如使用 ICU 风格消息的 JSON),被 Web 与移动端共同消费。在通过聊天与生成器构建应用时,这一点尤为重要,因为很容易在两个地方意外创建新文本。
一个实用设置是建立一个小的 “i18n 包” 或文件夹,包含:
React 与 Flutter 成为使用者。它们不应在本地发明新键。在 Koder.ai 风格的工作流中(React Web、Flutter 移动),你可以用同一套键集生成两个客户端,并像处理其他代码改动一样对变更进行审查。
后端的对齐也是同一故事的一部分。错误、通知与邮件不应在 Go 中写成手工的英文句子。相反,返回稳定的错误代码(如 auth.invalid_password)以及安全参数。然后客户端将错误代码映射为翻译文本。对于服务器发送的邮件,服务器可以使用相同的键与 locale 文件渲染模板。
制定一小段规则书并在代码审查中强制执行:
为防止含义接近但不同的重复键,为翻译者和未来的你添加 “说明” 字段(或注释文件)。例如:billing.trial_days_left 应说明它是作为横幅、邮件还是两者显示。那一句话通常能阻止“差不多可复用”的误用,从而产生翻译债务。
这种一致性是面向聊天生成应用的国际化架构的基石:一套共享词汇表,多种呈现面,在发布下一种语言时不会出现意外。
面向聊天生成应用的良好国际化架构从简单开始:一套消息键、一份文案真相来源,以及 Web 与移动端统一的规则。如果你以快速方式构建(例如使用 Koder.ai),这种结构能在保持速度的同时避免形成翻译债务。
尽早选择支持的语言,并决定缺失翻译时的处理方式。一个常见选择是:当用户偏好语言可用时显示偏好语言,否则回退到英文,并记录缺失键以便在下次发布前修复。
然后按下列步骤执行:
billing.plan_name.pro 或 auth.error.invalid_password。在各处使用相同键。t("key")。在 Flutter 中使用本地化包装器并在 Widget 中调用同样的键查找。目标是相同的键,而不是相同的库。if (count === 1) 式的零散逻辑。最后,用一个词更长的语言(德语是经典示例)和一种标点不同的语言做测试。这样能快速暴露按钮溢出、标题换行差以及假设英文词长的布局问题。
如果你把翻译放在共享文件夹(或生成的包)里,并把文案变更当作代码变更来管理,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 不仅是文本方向。布局流需要翻转,有些图标需镜像(如返回箭头),混合内容(阿拉伯语加英文产品编码)可能呈现出令人意外的顺序。测试真实屏幕而非单个标签,确保 UI 组件支持方向变更。
绝不要翻译用户写的内容(姓名、地址、支持工单、聊天消息)。你可以翻译环绕它的标签,并按 locale 格式化相关元数据(日期、数字),但内容本身必须保持原样。如果日后加入自动翻译,把它做成显式功能并提供“原文/译文”切换。
一个实用示例:Koder.ai 构建的应用可能显示 “{name} renewed on {date} for {amount}”。把它保持为一个消息,按 locale 格式化 {date} 与 {amount},并以用户时区显示。这一个模式能避免大量翻译债务。
快速规则通常能防止 bug:
翻译债务通常以“只改一个字符串”为开端,演变成后续数周的清理工作。在聊天生成的项目中,这种情况会更快出现,因为 UI 文本可能直接在组件、表单甚至后端生成。
最昂贵的问题是那些在应用中扩散并难以定位的。
想象一个 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)。一个简单示例:你要在英语、西班牙语与日语中发布计费屏。UI 包含:“Invoice”、“Paid”、“Due in 3 days”、“1 payment method / 2 payment methods” 以及总额如 “$1,234.50”。如果你用面向聊天生成应用的国际化架构构建它,键只需定义一次(在 Web 与移动端共享),每种语言只填充对应值。“Due in {days} days” 成为 ICU 消息,金额由按 locale 的格式化器处理,而非硬编码逗号。
按功能逐步推出语言,而非一次性大改:
记录两件事以保持新功能一致:你的键命名规则(含示例)以及字符串的“完成定义”(无硬编码文案、复数采用 ICU、日期/数字已格式化、已加入共享目录)。
下一步:如果你在 Koder.ai 上构建,使用 Planning Mode 在生成 UI 之前定义屏幕与键。然后用快照与回滚安全地迭代文案与翻译,避免造成损坏的发布。