全球化(全球化,也称国际化,i18n)——在不修改源代码的情况下,为移动应用支持多种语言和区域格式做准备的过程。包括从代码中提取字符串资源、支持不同的日期、数字和货币格式、考虑文本方向(LTR/RTL)以及根据不同语言调整布局。iOS 使用 NSLocalizedString 和 Localizable.strings,Android 使用 values-{lang} 目录中的 strings.xml。更多信息请参阅 Apple 国际化文档。
要点
全球化(缩写为 i18n——在「i」和「n」之间有 18 个字母)——为应用在架构上做好准备,以支持任何语言和区域。i18n 的关键规则是:没有文本字符串应在源代码中被硬编码。字符串应被提取到资源文件中,代码通过键来引用它们。添加新语言时,只需添加翻译文件——代码保持不变。这使 i18n 与本地化 (l10n) 区分开来,后者需要翻译字符串本身。
商业理由——全球化扩大了市场。根据 Common Sense Advisory(2023)的数据,超过 70% 的用户更喜欢用母语在应用中进行购买。本地化到 10 种语言可将潜在受众增加 80%。没有 i18n,每次扩展到新语言都需要修改代码,这会减缓上市速度并增加 5-10 倍的成本。正确的 i18n 架构能以最低成本支持 40 多种语言。
i18n 组件包括:字符串外部化(String externalization)、复数形式(pluralization,针对 1/2/5+)、日期和数字格式化(DateFormatter/SimpleDateFormat)、RTL 语言支持(Right-to-Left)、按区域规则排序(Collator)、区域符号(千位分隔符、小数点)。在 IT Sectr,我们在架构阶段就引入 i18n,而不是事后添加——这可以在后续本地化中节省高达 60% 的时间。
NSLocalizedString——用于处理翻译的主要 Swift 宏。格式:NSLocalizedString(«key», comment: «给翻译人员的说明»)。宏根据设备的当前区域(NSLocale.preferredLanguages)自动从 Localizable.strings 替换字符串。如果未找到键的翻译,则返回键本身或开发语言(通常为 en)中的值。Apple 建议使用有意义的键,而不是将英文字符串用作键。
// Localizable.strings (en)
// "welcome_title" = "欢迎!";
// Localizable.strings (ru)
// "welcome_title" = "欢迎!";
// Swift 代码——所有语言统一
titleLabel.text = NSLocalizedString(
"welcome_title",
comment: "欢迎屏幕标题"
)
// 通过 Localizable.stringsdict 实现复数形式
//
// <dict>
// <key>items_count</key>
// <dict>
// <key>NSStringLocalizedFormatKey</key>
// <string>%#@items@</string>
// <key>items</key>
// <dict>
// <key>one</key>
// <string>%d 商品</string>
// <key>few</key>
// <string>%d 商品</string>
// <key>many</key>
// <string>%d 商品</string>
// </dict>
// </dict>
// </dict>
// 复数形式的使用
let items = 5
let label = String.localizedStringWithFormat(
NSLocalizedString("items_count", comment: ""), items
)
XLIFF——开发人员和翻译人员之间交换翻译的格式。Xcode 导出 XLIFF 文件(Editor → Export for Localization),其中包含所有需要翻译的字符串。翻译人员在 CAT 工具(Trados、memoQ、Smartcat)中使用 XLIFF 工作。翻译完成后,XLIFF 被重新导入 Xcode(Editor → Import Localizations)。XLIFF 会自动更新所有 .lproj 目录。这是生产环境中 iOS 应用本地化的标准工作流程。
SwiftUI 通过 Text 初始化器与 NSLocalizedString 配合使用。SwiftUI 中的文本会自动国际化:Text(«welcome_title») 会像 NSLocalizedString 一样在 Localizable.strings 中查找翻译。对于复数形式,请使用 Text(«%d items», count: items)。SwiftUI 支持通过 Text(date, style: .date) 进行日期格式化——它会自动使用 Locale.current。Apple 建议新项目使用 SwiftUI,因为其中的国际化更加透明。
Android i18n 基于资源系统构建。字符串被提取到默认语言(通常是英语)的 res/values/strings.xml 中。每种语言都创建单独的目录:res/values-ru/strings.xml(俄语)、res/values-de/strings.xml(德语)、res/values-fr/strings.xml(法语)。Android 根据设备的系统语言(Locale.getDefault())自动选择字符串。如果没有精确的区域设置,则使用基础版本(values/strings.xml)。
// res/values/strings.xml(英语,默认)
<resources>
<string name="welcome_title">Welcome!</string>
<string name="items_count">%d item(s)</string>
</resources>
// res/values-ru/strings.xml(俄语)
<resources>
<string name="welcome_title">欢迎!</string>
<plurals name="items_count">
<item quantity="one">%d 商品</item>
<item quantity="few">%d 商品</item>
<item quantity="many">%d 商品</item>
</plurals>
</resources>
// Kotlin 代码
textView.text = getString(R.string.welcome_title)
// 复数形式
val items = 5
textView.text = resources.getQuantityString(
R.plurals.items_count, items, items
)
// 代码中的 RTL 支持
textView.textDirection = View.TEXT_DIRECTION_LOCALE
// Manifest: supportsRtl="true"
RTL(从右到左)——支持文本从右到左阅读的语言(阿拉伯语、希伯来语、乌尔都语、波斯语)。Android 通过 android:layoutDirection 和 android:textDirection 属性支持 RTL。在清单中设置 android:supportsRtl="true"——Android 会自动镜像布局。导航抽屉、返回/前进图标、文本对齐应在两个方向上都有效。在代码中使用 View.LAYOUT_DIRECTION_LOCALE 和 Gravity.START/END 替代 LEFT/RIGHT。
Android Resource Qualifiers 不仅可以本地化字符串,还可以本地化图像(res/drawable-ru/)、布局(res/layout-ru/)、动画和颜色。对于阿拉伯语和希伯来语,需要使用具有镜像元素排列的单独布局——使用 res/layout-ar/(阿拉伯语)。Android 还支持区域变体:values-rUS、values-rGB、values-de-DE。限定符可以组合使用:values-ldrtl-ru——用于 RTL 屏幕的俄语。
日期和时间——i18n 的关键方面之一。不同区域使用不同的格式:俄罗斯——DD.MM.YYYY,美国——MM/DD/YYYY,日本——YYYY.MM.DD。使用固定格式(yyyy-MM-dd)向用户显示是错误的。在 iOS 中使用带有 Locale(identifier: locale) 的 DateFormatter,在 Android 中使用 DateFormat.getDateInstance(DateFormat.SHORT, locale)。对于语音助手和 AI 搜索,日期在内部表示中应为 ISO 8601 格式。
数字和货币——不同区域使用不同的分隔符:1,234.56(美国)vs 1.234,56(俄罗斯),1 234,56(法国)。iOS:使用 .locale = locale 的 NumberFormatter。Android:使用带有 DecimalFormatSymbols(locale) 的 DecimalFormat。对于货币:格式 ¥1,234(日本)vs $1,234.56(美国)vs 1 234,56 ₽(俄罗斯)。切勿手动拼接货币和数字——使用 NumberFormatter.currencyCode 和 .currencySymbol。
| 区域 | 日期 | 数字 | 货币 |
|---|---|---|---|
| 俄罗斯 | 31.12.2024 | 1 234,56 | 1 234,56 ₽ |
| 美国 | 12/31/2024 | 1,234.56 | $1,234.56 |
| 德国 | 31.12.2024 | 1.234,56 | 1.234,56 € |
| 日本 | 2024/12/31 | 1,234 | ¥1,234 |
| 沙特阿拉伯 | 31/12/2024 | 1,234.56 | 1,234.56 SAR |
排序(Collation)——按字母顺序排序在不同语言中有所不同。在西班牙语中,「ch」在「c」之后。在瑞典语中,「ä」在字母表末尾。在德语中,「ß」排序为「ss」。iOS:LocalizedComparison(String.localizedCompare)。Android:Collator.getInstance(locale)。切勿对显示给用户的字符串使用 compareTo()——它使用 Unicode 码点顺序,不考虑区域规则。
架构原则——从第一次提交开始就引入 i18n。代码中的每个字符串都应通过包装函数(tr("key")),该函数在配置 i18n 之前不存在——这会迫使开发人员立即提取字符串。不要使用英文字符串作为键——当英文措辞发生变化时,您需要更新所有翻译。使用有意义的键:「profile.title」、「settings.language.label」。
伪本地化——在实际翻译之前测试 i18n 的技术。将每个拉丁字母替换为带变音符号的字符(á、é、ñ、ü)以检查编码,添加前缀 [XXX] 以检查字符串截断。Xcode:启动方案——伪语言「Double-Length Pseudolanguage」。Android:开发者选项——Force RTL layout direction,System font scale 至 200%。伪本地化可以在没有翻译人员参与的情况下发现 80% 的 i18n 问题。
IT Sectr i18n 检查清单——发布前我们检查:(1)代码中没有硬编码的字符串(日志除外),(2)复数形式对所有语言正常工作,(3)日期/数字通过 Locale API 格式化,(4)布局在 RTL 语言中正确显示,(5)字符串在最大缩放时不会被截断,(6)伪本地化未发现错误,(7)商店中声明的所有语言都有完整的翻译集。
常见问题解答
i18n(国际化)——准备代码:提取字符串、支持 RTL、格式化。由开发人员一次性完成。l10n(本地化)——将字符串翻译成特定语言。由翻译人员为每个本地化多次完成。i18n 是架构,l10n 是内容。没有 i18n,本地化原则上是不可能的。
NSLocalizedString——根据设备的当前区域在 Localizable.strings 中按键查找值的宏。如果找到翻译——返回它。如果没有——返回键。格式:NSLocalizedString(«key», comment: «说明»)。如需带参数的格式化,请使用 String.localizedStringWithFormat()。
strings.xml——res/values/{lang}/ 目录中包含翻译的文件。基础版本在 values/strings.xml 中,翻译在 values-ru/strings.xml 中。代码通过 getString(R.string.key) 访问。Android 根据系统语言自动选择所需的文件。对于复数形式,使用带有 zero/one/few/many/other 限定符的 <plurals> 资源。
RTL(从右到左)——阿拉伯语、希伯来语、乌尔都语、波斯语的书写方向。Android:清单中的 supportsRtl="true"、android:layoutDirection、Gravity.START/END。iOS:使用 UISemanticContentAttribute.forceLeftToRight 强制 RTL。布局应镜像翻转:菜单在右侧,文本从右到左,导航图标反转。
对于全球发布,最低要求包括:英语、西班牙语、法语、德语、日语、中文、韩语、葡萄牙语、俄语、意大利语。App Store 至少需要英语本地化。每增加一个本地化都会增加潜在受众。对于本地市场,1-2 种语言就足够了。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。