10 个常见的 i18n 错误及其避免方法
了解开发人员常犯的最常见的国际化错误,以及如何修复它们。使用这些最佳实践来提升应用的本地化质量。
国际化(i18n)看起来很简单,直到你发现你的德语用户看到被截断的按钮、日本用户看到被截断的句子片段,以及阿拉伯语用户看到的布局完全错乱。这些并非边缘案例,而是开发团队首次进行本地化时常见错误的可预见后果。在本指南中,我们将介绍最常见的 10 个 i18n 错误,逐一解释它们为何会发生,并一步一步展示如何修正每一个。无论你是在构建新应用还是对现有应用进行改造,避免这些陷阱将为你省下数周的调试时间。
错误 #1:硬编码字符串
硬编码字符串是最基本的 i18n 错误。
['在编写第一行 UI 代码之前,设置你的 i18n 框架', '使用一个 Linter 插件来检测模板和组件中的硬编码字符串', '让翻译键具有描述性并按功能或页面组织']
在编写第一行 UI 代码之前设置好你的 i18n 框架
使用 Linter 插件来检测模板和组件中的硬编码字符串
保持翻译键具有描述性,并按功能或页面进行组织
一个典型场景
一个开发者在 JSX 中直接编写按钮标签:<button>Submit Order</button>。
該應用程式以英語版本發布,運作正常。六個月後,該公司向德國拓展。
本地化团队发现了 2000+ 的硬编码字符串。后续改造需3周并导致47个错误。
为什么这是一个问题
在成熟的代码库中,硬编码的字符串可能达到成千上万。事後提取需要修改每个文件、重新测试每个组件,并冒着全局回归的风险。
硬编码的字符串直接嵌入在源代码、模板或组件中。它们在运行时不可提取、翻译或替换,除非修改代码本身。
非英语本地化环境的用户会看到未翻译的 UI 元素与已翻译的内容混在一起——给人一种混乱且不专业的印象。
解决方法
将所有对用户可见的字符串从一开始就移到资源文件中。
在编写第一个组件之前,设置一个翻译框架(例如 next-intl、react-i18next、vue-i18n)。
创建一个资源文件结构(例如 messages/de.json),并通过翻译键(如 t('checkout.submitButton'))引用所有字符串。
添加一个 lint 规则或 pre-commit 钩子,用于标记 UI 组件中的原始字符串字面量。
错误 #10:逐字翻译所有内容
并非所有内容都应翻译。商标名、法人名、技术术语以及某些产品名必须保持原语言。过度翻译可能导致法律问题、商标不一致和用户混淆。
["保持一个“不翻译”词汇表并与所有翻译人员共享", '使用被锁定的段落或用于品牌和法律术语的独立命名空间', '始终为含糊的字符串提供上下文注释以避免错误翻译']
维护一个“不翻译”的术语表,并与所有译者共享
对于品牌和法律术语,使用受保护的分段或单独的命名空间
始终为歧义字符串提供上下文注释,以防止翻译错误
一个典型场景
一个翻译文件包含公司名称 'CloudForge Inc.' 和技术术语 'OAuth 2.0 Token' 作为可翻译的字符串。
西班牙翻译将「CloudForge」翻译为「ForjaNube」,并将「OAuth 2.0 Token」翻译为「token OAuth 2.0」。
结果:用户无法以其真实名称找到该公司,阅读西班牙文档的开发人员会被翻译得不熟悉的专业术语所困惑。
为什么这会是一个问题
单个错误翻译的商标名可能使合同无效。纠正所有语言中每个字符串的翻译工作需要数周时间。
当所有字符串在没有上下文的情况下交给翻译,翻译人员可能会把商标名('Apple' → 'Apfel')、法律术语('GmbH' → 'LLC')或必须保持英文的技术名词翻译成其他语言。
用户找不到他们熟悉的品牌名称所命名的产品,法律文件引用错误的公司,技术文档在翻译像 'API Endpoint' 这样的术语后变得难以理解。
解决方法
清晰标记不可翻译的内容,并为翻译者提供上下文注释。
创建一个“不翻译”词汇表,列出所有必须保持不变的品牌名称、产品名称、法定实体和专业术语。
使用单独的命名空间或专用键来处理不可翻译的内容。许多 i18n 工具支持“锁定”段,译者无法修改。
在你的翻译文件中添加译者注释/描述以解释上下文:'这是一个商标名——不翻译' 或 '术语——保留英文'。
错误 #2:字符串拼接
将片段拼接成完整句子在英语中听起来合乎逻辑,但在其他语言中并不成立。词序、语法和句法结构在不同语言之间差异显著,使拼接的字符串无法翻译。
['切勿通过拼接已翻译片段构建句子', '使用命名占位符('{name}')而非位置参数({0})以提高清晰度', '为翻译者提供上下文注释,说明每个占位符的内容']
切勿通过拼接翻译片段来组成句子
使用命名占位符('{name}')而非位置参数({0})以提高清晰度
为翻译者提供上下文注释,解释每个占位符包含的内容。
一个典型场景
开发者写道:'You have ' + count + ' items in your ' + cartType + ' cart' — 在英语中完美运行。
德语翻译收到三个独立的片段,无法组成一个语法正确的句子,因为词序必须改变。
结果:德语用户看到 'Sie haben 5 Artikel in Ihrem Warenkorb Standard'——生硬且不专业。
为什么这是一个问题
每个拼接的字符串都是一个倒计时炸弹。对于20种语言和50个拼接字符串,你将面临多达1000个潜在的语法错误,需要人工修复。
像 'Willkommen ' + userName + ', du hast ' + count + ' neue Nachrichten' 这样的字符串拼接需要特定的词序。翻译者得到的是上下文不连贯的片段,无法重新排序。
用户看到语法错误的句子,例如“1 Nachrichten”或完全错误的复数形式。在正式场景中,这会破坏可信度。
解决办法
使用带名称的占位符的参数化消息,以便翻译人员能够重新排列整个句子。
用带占位符的单一消息键替代拼接:'cart.summary': '你在'{cartType}'购物车中有{count}件商品'
将变量作为参数传递给翻译函数:t('cart.summary', { count: 5, cartType: 'Premium' }).
翻译者可以自由调整:'你的'{cartType}'购物车中有{count}件商品'——语法正确的德语。
错误 #3:忽略复数形式
英语只有两种复数形式:单数和复数。开发者常常假设所有语言都像英语一样工作。事实并非如此。波兰语有4种形式,阿拉伯语有6种形式,甚至像法语这样的语言对0的处理也与英语不同。
['始终使用 ICU MessageFormat 或等效库来处理可计数的内容', '永远不要编写自己的复数逻辑——相信 CLDR 规则', '用 0、1、2、5、21 和 100 等值测试复数,以覆盖所有类别']
始终使用 ICU MessageFormat 或等效库来处理可计数的内容。
切勿自行编写复数逻辑——遵循 CLDR 规则。
使用诸如 0、1、2、5、21、100 等值来测试复数,以覆盖所有类别。
一個典型情景
开发者写道:count + (count === 1 ? ' Datei' : ' Dateien') —— 正确处理德语。
波兰语翻译需要4种形式:1 plik、2-4 pliki、5-21 plików、22-24 pliki。简单的三元运算无法表达这一点。
结果:波兰用户看到 '5 pliki'(错误形式)而非 '5 plików'(正确形式),应用看起来像是坏掉了。
为什么这会是一个问题
你应用中的每个可数名词都需要处理复数。若有50个这样的字符串和20种语言,将有多达1000条复数规则——手动管理几乎不可能。
简单的 if/else 检查(count === 1 ? 'Datei' : 'Dateien')仅能处理德语/英语。CLDR 定义多达 6 种复数类别:zero、one、two、few、many 和 other。每种语言使用不同的子集。
用户看到语法错误的文本,例如“1 Nachrichten”或完全错误的复数形式。在正式场景中,这会破坏可信度。
解决办法
使用 ICU MessageFormat,它开箱即用地支持所有 CLDR 的复数规则。
用 ICU 语法定义消息:'fileCount': '{count, plural, one {# Datei} other {# Dateien}}'.
翻译者为他们的语言提供所有必需的复数形式。波兰语:'{count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}'。
你的 i18n 库在运行时会基于活动区域设置的 CLDR 规则自动选择正确的形式。
错误 #4:固定的 UI 元素宽度
设计师用英语创建像素级排版,开发者用固定宽度实现它们。但翻译后的文本可能显著更长或更短。德文文本大约比英文长约 30%,而中文文本可能短约 50%。
['在可翻译文本的元素上绝不使用固定像素宽度', '将文本扩展 40% 作为基线——某些语言甚至扩展得更多', '使用 CSS Flexbox 或 Grid 来实现适应可变内容长度的布局']
切勿在包含可翻译文本的元素上使用固定像素宽度。
将文本扩展量设定为基线的40%进行规划——某些语言的扩展甚至更多。
对适应不同内容长度的布局,使用 CSS Flexbox 或 Grid。
一个典型场景
设计师创建了一个包含 5 个按钮的导航栏,每个按钮宽度恰好为 100px——在英文界面看起来很棒。
德语翻译:'Settings' 将变为 'Einstellungen'(13 对比 8 字符),'Submit' 将变为 'Absenden'(8 对 6)。导航栏溢出。
结果:在移动设备上,按钮会堆叠在一起,或者文本被截断,从而使德国用户的导航无法使用。
这为什么是一个问题
每个固定宽度的元素都是一个潜在的断点。一个典型的应用程序拥有数百个按钮、标签和卡片,它们都需要承受文本扩展。
固定宽度的容器(width: 120px)和固定尺寸的按钮在文本扩展时会被截断或溢出。CSS overflow: hidden 会静默隐藏内容,而 overflow: visible 会破坏布局。
用户看到像 'Einstellu...' 这样的标签被截断,而不是 'Einstellungen',或者按钮覆盖相邻的元素。关键操作变得不可读或不可点击。
解决办法
设计并实现灵活的布局,使其能够适应所有语言环境中内容的长度。
将固定宽度替换为 min-width、max-width 和 Flex 布局。使用 CSS Grid 或 Flexbox 动态分配空间。
将文本容器设置为换行:使用 overflow-wrap: break-word,并在可翻译的内容上避免 white-space: nowrap。
使用伪本地化对 UI 进行测试,将所有字符串扩展 40% 以模拟最坏情况——在发送字符串给翻译人员之前。
错误 #5:日期和数字格式化
数据和数字看起来似乎通用。但 01/02/2025 在美国表示 2 月 1 日,在欧洲表示 1 月 2 日。数字中的逗号和句点的含义不同:1,000.50(美国) vs 1.000,50(德国)。搞错会导致混淆、数据错误和信任下降。
['永远不要手动使用字符串模板来格式化数据或数字——始终使用 Intl API', '内部将所有数据以 ISO 8601 存储,货币以最小单位(分)存储', '测试具有不同小数点分隔符、日期顺序和日历系统的 Locale']
切勿在包含可翻译文本的元素上使用固定像素宽度。
在内部将所有数据以 ISO 8601 保存,货币以最小单位(分)保存。
使用不同的小数分隔符、日期顺序和日历系统的语言环境进行测试。
一个典型场景
开发者将日期格式化为 MM/DD/YYYY,将价格格式化为 $1,234.50——这对美国用户是正确的。
德国用户看到日期 03/04/2025,并将其解读为 3 月 4 日(DD/MM/YYYY 规则)。价格 $1,234.50 看起来应该是 1.234,50。
结果:用户为错误的日期预订了航班,并质疑价格格式。在德国市场,支持工单数量上涨了15%。
为什么这会是一个问题
日期和数字格式化影响到应用中每个数据显示:表格、图表、表单、发票、报告。一个全局修复可以覆盖数百个实例。
硬编码的格式字符串,如 toLocaleDateString('en-US'),或使用模板字面量进行手动格式化,忽略用户的实际区域设置。即使 Locale 正确但日历系统(公历 vs. Hijri)也会造成问题。
用户错误读取数据,并以错误的格式输入数据。看到 03/04/2025 的欧洲用户可能将其解读为 3 月 4 日,而不是 4 月 3 日——错过的日期或错误的预订。
解决办法
使用内置的 Intl API 或支持区域设置的格式化库来处理所有日期、时间、数字和货币。
用 Intl.DateTimeFormat(locale) 处理日期,用 Intl.NumberFormat(locale, { style: 'currency', currency }) 处理价格,替代手动格式化。
内部以 ISO 8601 格式(YYYY-MM-DD)存储数据,仅在显示时按照用户的 Locale 进行格式化。
用至少五个不同的区域设置(en-US、de-DE、ja-JP、ar-SA 和 zh-CN)测试关键的日期/数字显示,以覆盖最重要的格式变体。
错误 #6:忘记 RTL 语言
描述:像阿拉伯语、希伯来语和波斯语这样的从右到左(RTL)语言被超过5亿人使用。然而,大多数应用仍然为从左到右(LTR)的布局而设计。RTL 支持不仅仅是翻转文本 — 整个用户界面必须被镜像。
['自从第一天起就仅使用逻辑 CSS 属性(inline-start/end、block-start/end)', '在每次 Sprint 评审时,在 HTML 元素上使用 dir="rtl" 进行测试', '为导航、图标、表单与进度指示建立 RTL 测试清单']
从第一天起就仅使用逻辑的 CSS 属性(inline-start/end、block-start/end)
在每次 Sprint 评审时,在 HTML 元素上用 dir='rtl' 测试应用程序。
为导航、图标、表单和进度指示创建 RTL 测试清单
一个典型场景
开发人员在整个应用中使用 margin-left: 16px 和 text-align: left——这是标准的从左到右(LTR)做法。
应用程序在沙特阿拉伯启动。返回箭头显示为前进方向,侧边栏出现在错误的一侧,数字数据对齐错误。
结果:阿拉伯用户在 30 秒内离开应用。团队需要 4 周的紧急 CSS 重构来解决问题。
为什么这是一个问题
RTL 支持影响到应用中的每个组件。事后引入 RTL 通常需要重写 30-50% 的 CSS 规则并检查每个图标和布局。
像 margin-left、padding-right、text-align: left 和 float: left 这样的 CSS 属性硬编码了方向。带有方向含义的图标(箭头、进度条)显示在错误的方向。甚至 border-radius 的值也必须对称镜像。
阿拉伯用户看到导航在错误的角落,进度条反向滚动,文本与 UI 元素发生冲突。应用感觉陌生且不可用。
解决办法
使用逻辑的 CSS 属性,并从一开始就用 RTL 布局进行测试。
用逻辑等价属性替换所有物理 CSS 属性:margin-left → margin-inline-start,padding-right → padding-inline-end,text-align: left → text-align: start。
在 HTML 根元素上根據活動 Locale 設定 dir 屬性。使用 :dir(rtl) 的 CSS 偽類進行 RTL 覆寫。
检查所有图标的方向含义。用对称版本替换定向图标,或在 RTL 场景中使用 CSS transform: scaleX(-1)。
错误 #7:带文本的图片
将文本嵌入到图片中——无论是在 Hero 横幅、按钮、信息图还是屏幕截图——都是本地化的一场噩梦。每个带文本的图片都必须为每种语言重新制作,这会让设计成本翻倍并推迟发布。
['切勿将可翻译文本直接嵌入光栅图像(PNG、JPG)中', '在背景图片上使用 CSS 文字覆盖层,用于 Hero 横幅和 CTA', '为 App Store 列表与营销页面自动生成屏幕截图']
切勿直接将可翻译文本嵌入栅格图像(PNG、JPG)
在背景图片上使用 CSS 文本覆盖用于横幅和 CTA。
自动化 App Store 列表和营销页面的截图生成
一个典型场景
设计师直接在图片中嵌入文本“Start Your Free Trial”来创建促销横幅。
本地化团队翻译了所有 UI 字符串,但德语页面上的横幅仍然显示英文文本。
结果:德语着陆页有一个让人恼火的英文横幅。为15种语言创建本地化横幅需要3天的设计工作,推迟上线。
為什麼這是個問題
一个典型的营销页面有5-10张带文本的图片。使用15种语言时,约有75-150种图片变体需要创建、维护,并在每次设计变更时更新。
嵌入到图片中的文本(PNG、JPG、SVG 含嵌入文本)无法被翻译工具提取。每个本地化版本都需要设计师手动编辑源文件、导出并上传。
用户看到带有外语文本的图片,或者更糟,是翻译后的界面与未翻译的图片的混合。这看起来不一致,削弱品牌信任。
解决方法
通过 CSS 覆盖层将文本与图像分离,使用可翻译文本元素的 SVG,或进行动态图像生成。
使用 CSS 将可翻译文本覆盖在背景图片上:通过对图像容器应用绝对定位来定位文本层。
对于信息图或图表,使用带有 <text> 元素的 SVG,引用翻译键而不是嵌入原始字符串。
在营销材料中的应用程序屏幕截图,使用 Fastlane(移动)或 Playwright(Web)等工具自动生成截图,覆盖每个语言区域的屏幕。
错误 #8:未处理的缺失翻译
在开发过程中,翻译总是不完整。新特性会比译者翻译更快地添加字符串。没有合适的回退处理,缺失的翻译会导致崩溃、空的 UI 元件或向用户显示未翻译的键。
['在 i18n 设置中始终至少配置一个回退语言', '将缺少翻译的键记录到监控系统以便跟踪', '在 Locale 上线前设定最低翻译覆盖率']
在 i18n 设置中始终至少配置一个回退语言
将缺失的翻译键记录到你的监控系统以便追踪
在区域上线之前设定最低翻译覆盖率
一个典型场景
开发人员新增一个“Premium Features”区域,包含15个新的翻译键。英文版本会立即发布。
法语翻译尚未完成。法语页面显示原始键:'premium.feature1.title', 'premium.feature1.description'。
结果:法语用户看到一个充满开发者端键名的损坏页面。支持团队收到数十个错误报告。
为什么这是一个问题
随着应用程序变大,英文字符串与其他语言翻译之间的差距也会变大。一个支持100种语言、2000个字符串的应用可能在任何时刻有超过10,000条缺失的翻译。
如果没有回退逻辑,缺失的翻译键将返回 undefined、null 或原始键字符串(例如 'checkout.confirmButton')。模板引擎可能会抛出错误,导致页面崩溃或根本不渲染。
用户看到崩溃的界面:空按钮、缺失标签或像 'nav.settings.title' 这样的难以理解的字符串,而不是实际文本。这让人困惑且不专业。
解決方法
配置一个健壮的回退链,并在所有语言环境中监控翻译覆盖率。
在 i18n 配置中设置一个回退语言链:缺失的法语(fr)键将自动回退到英语(en)。
添加一个 Missing-Key 处理程序,当遇到未翻译的键时,将其记录到你的监控系统(如 Sentry、Datadog),以免影响用户体验。
创建一个翻译覆盖率仪表板,跟踪每个 Locale 的完成度;如果覆盖率低于阈值(例如 95%),则阻止版本发布。
错误 #9:字符编码问题
字符编码问题是本地化的隐形杀手。对于英语和欧洲语言,一切看起来都正常,但一旦你添加中文、日语、韩语、阿拉伯语或表情符号,字符就会被截断或显示为乱码(Mojibake)。这些错误以难以发现而著称。
['在所有地方使用 UTF-8 编码 — 源文件、数据库、API 响应和 HTML 元标签', '在 MySQL 中使用 utf8mb4(而不是 utf8),以支持包括表情符号在内的完整 Unicode', '用真实内容在 CJK、阿拉伯文和表情符号方面测试,以尽早发现编码问题']
在所有地方使用 UTF-8 编码——源文件、数据库、API 响应和 HTML 元标签
在 MySQL 中使用 utf8mb4(而不是 utf8)以支持完整的 Unicode 区域,包括表情符号。
用真实内容进行测试,包含 CJK、阿拉伯语和表情符号,以尽早发现编码问题。
一个典型场景
开发人员将 MySQL 数据库配置为 latin1 排序规则(旧标准)。应用程序代码使用 UTF-8。
日本用戶以真實姓名註冊。資料庫以損壞的位元組儲存 '田中太郎'。
结果:用户资料显示被截断的文本。更糟的是:对所有 CJK 名称的搜索和排序都会失败,影响数千名用户。
为什么这是一个问题
编码问题会扩散到整个堆栈。数据库的排序规则配置错误可能会损坏数百万条记录,修复需要昂贵的数据迁移。
Stack 中的编码不一致——源文件为 UTF-8,数据库为 Latin-1,API 响应为 Windows-1252——破坏多字节字符。一个配置错误的层就会把 '日本語' 变成 '????' 或 '日本èª'.
用户看到文本被截断、问号或空方块,应该显示的语言却显示不出来。最坏的情况,表单输入将永久损坏到数据库中。
解决办法
强制在应用堆栈的每一层保持 UTF-8 编码的一致性。
将所有源文件设置为 UTF-8(配置编辑器和 .editorconfig)。在 HTML 中添加 <meta charset='UTF-8'>,在 API 响应中加入 'Content-Type: application/json; charset=utf-8'。
将数据库配置为 utf8mb4(不仅仅是 utf8,这是 MySQL 的 3 字节子集)。将连接的 Collation 设置为 utf8mb4_unicode_ci。
选择覆盖目标书写系统的字体:拉丁字母、西里尔字母、CJK、阿拉伯字母、天城文。使用系统字体栈或 Google Fonts 的语言子集以实现最佳加载。
测试你的 i18n 实现
长度扩展测试
为所有翻译后的字符串再增加 30-40% 的长度,以模拟在德语、芬兰语或希腊语等词汇丰富的语言中文本的扩展。这将覆盖固定宽度的容器、被截断的标签和溢出的按钮,在开始翻译之前就能看到。许多伪本地化工具提供此功能作为内置特性。
"Senden" → "Ṡééééñðéñ_éxpáñðéð" (40% länger)
伪本地化
伪本地化会将每个字符替换为带变音的等价字符(例如,将 'a' 替换为 'á'),并用标记如 [!! 和 !!] 将字符串包裹起来。这样可以立即看出哪些字符串是硬编码的,哪些来自翻译系统。将伪本地化作为 CI 流水线的一部分运行,以自动捕获回归。
屏幕上的每个未被 [!! !!] 标记包围的文本都是硬编码,必须从代码中分离。此测试可以在不到一分钟的时间内捕捉到 95% 的漏测字符串。
"发送消息" → "[!! Ñáçḥŕíçḥṫ ṡéñðéñ !!]"
RTL 布局测试
即使没有阿拉伯语或希伯来语的翻译,也可以通过在 HTML 根元素中添加 dir='rtl' 来测试 RTL 布局。这可以立即暴露方向性 CSS 的问题:图标错位、边距在错误的一侧、导航崩溃以及 Flex 项目排序错误。把它变成每次 Sprint 评审的标准检查——切换只需 10 秒,并能捕捉在生产中可能需要数周修复的问题。
i18n 检查清单
['所有对用户可见的字符串都已从代码中分离并放入资源文件中', '避免使用字符串拼接来组成句子', '已用 ICU MessageFormat 或等效实现复数规则', '日期、时间和数字格式化使用区域感知的 API', '已测试包含阿拉伯语或希伯来语内容的 RTL 布局', '对文本密集的元素设计灵活的 UI,避免固定宽度', '已配置回退语言并在缺少键时进行了测试', 'UTF-8 编码在所有文件和数据库中保持一致', '为每个市场本地化 App Store 与 Play Store 的元数据', '为每种语言更新屏幕截图与营销资源']