多语言国际化
JL_Health(杰理健康)iOS 客户端的多语言国际化(i18n)机制:通过 Apple 标准的 .lproj 资源包组织各语言文案,配合 LanguageCls.localizableTxt("中文原文") 统一取词 API,实现界面、Toast、启动屏与 App 元数据的全量本地化。
Purpose and Scope
本文档说明 JL_Health 应用的多语言国际化实现方式,包括:
- 本地化资源在仓库中的目录结构与文件职责(
Localizable.strings/InfoPlist.strings/LaunchScreen.strings); - 代码侧统一取词入口
LanguageCls.localizableTxt的调用模式与设计意图; - 运行时语言检测与回退逻辑(基于
Locale.current.languageCode); - SDK 测试工程(
SDKTestHelper)中由 R.swift 生成的Localizable表访问方式,以及按功能分组(通话/同步/表盘)的语言文案定义。
本文档不覆盖具体业务页面的功能逻辑(参见各自页面的文档),也不涉及服务端下发文案的机制(本仓库未见相关实现)。
Overview
JL_Health 是一个面向全球市场的健康穿戴设备配套 App,支持英语、中文(简/繁)、德语、西班牙语、法语、阿拉伯语、波斯语等多种语言。多语言国际化是它的基础能力:任何新增页面、弹窗、Toast、按钮标题都必须通过本地化 API 取词,而不是硬编码中文文本。
仓库内的本地化实现遵循 Apple iOS 的标准机制:
- 资源组织:每个语言一个
xx.lproj目录(如en.lproj、zh-Hans.lproj、ar.lproj),目录内放置同名翻译表Localizable.strings(界面文案)、InfoPlist.strings(App 显示名称、权限描述)与LaunchScreen.strings(启动屏文案)。 - 代码取词:业务代码统一调用
LanguageCls.localizableTxt("中文"),由该工具类根据系统语言在正确的.lproj中查找翻译;找不到时返回原始中文 key,保证任何语言环境下 UI 都不会出现空字符串。 - 语言判定:通过
Locale.current.languageCode判断当前语言,zh及zh-*前缀视为中文环境,其余语言按系统语言码匹配对应语言包。 - SDK 侧:测试/工具工程(
SDKTestHelper)使用 R.swift 生成的类型安全资源访问器,显式指定tableName: "Localizable"与preferredLanguages,并在TranslateTools中按功能分组定义语言文案。
这套设计把"语言选择"与"文案获取"解耦:系统语言由 iOS 决定,App 只负责"按语言码取正确的 Bundle 和翻译表",因此新增语言只需添加一个 .lproj 目录和翻译文件,无需改动业务代码。
Architecture
资源与调用架构
flowchart TD
subgraph sg_App["JL_Health App(业务层)"]
VC["UIViewController / ViewModel<br/>(DialHistory / DialPay / DeviceSub ...)"]
LC["LanguageCls<br/>localizableTxt(key)"]
Locale["Locale.current.languageCode<br/>Bundle 语言选择"]
end
subgraph sg_Bundles["本地化资源包(.lproj 目录)"]
Zh["zh-Hans.lproj / Localizable.strings"]
En["en.lproj / Localizable.strings"]
Ar["ar.lproj / Localizable.strings"]
De["de.lproj / Localizable.strings"]
Es["es.lproj / Localizable.strings"]
Fr["fr.lproj / Localizable.strings"]
Info["InfoPlist.strings<br/>(App 名称 / 权限描述)"]
Launch["LaunchScreen.strings<br/>(启动屏文案)"]
end
VC -->|"LanguageCls.localizableTxt(\"中文原文\")"| LC
LC --> Locale
Locale -->|"语言码匹配"| Zh
Locale -->|"语言码匹配"| En
Locale -->|"语言码匹配"| Ar
Locale -->|"语言码匹配"| De
Locale -->|"语言码匹配"| Es
Locale -->|"语言码匹配"| Fr
Info -->|"随语言包加载"| Zh
Launch -->|"随语言包加载"| Zh
架构说明:
- 业务层(上层):各
UIViewController/ViewModel只调用LanguageCls.localizableTxt("中文"),完全不感知语言包细节。这样文案改动、新增语言都不会触及业务代码。 - 工具层(中间):
LanguageCls是唯一取词入口,负责把"中文原文"当作 key 去查找翻译,屏蔽Bundle与NSLocalizedString的底层细节。 - 资源层(下层):
.lproj目录按语言划分,每个语言包内有三类strings文件,分别服务界面文案、App 元数据(InfoPlist)与启动屏。
运行时取词流程
sequenceDiagram
participant VC as UIViewController
participant LC as LanguageCls
participant B as Bundle.main
participant S as Localizable.strings
VC->>LC: localizableTxt("购买记录")
LC->>B: 解析当前语言(Locale.current.languageCode)
B-->>LC: 语言码(如 "zh-Hans" / "en")
LC->>S: 在对应 .lproj 的 Localizable 表中查找该 key
S-->>LC: 命中 → 返回翻译文本;未命中 → 返回 key 原文
LC-->>VC: 本地化字符串(赋给 title / label / toast)
设计意图:以"中文原文"作为 key 是这套方案最核心的取舍——开发时无需维护独立的 key 命名空间,代码可读性高(localizableTxt("免费") 一眼可知含义);同时"未命中回退返回 key 本身"保证了即使某个语言包漏翻译,界面也不会出现空白,只会暂时显示中文,便于 QA 快速发现缺失项。
本地化资源结构
.lproj 语言包
仓库中所有本地化资源位于 code/JL_Health/JieliJianKang/ 下,按语言目录组织。当前至少包含以下语言包(每个目录内含若干 strings 文件):
| 语言目录 | 面向区域 | 说明 |
|---|---|---|
zh-Hans.lproj | 简体中文 | 默认开发语言,Localizable.strings 中的 key 即中文原文 |
zh-Hant.lproj | 繁体中文 | 港台地区 |
en.lproj | 英语 | 国际通用语言 |
en-GB.lproj | 英式英语 | 英/澳等地区 |
de.lproj | 德语 | 德语区 |
es.lproj | 西班牙语 | 西语区 |
fr.lproj | 法语 | 法语区 |
ar.lproj | 阿拉伯语 | 中东地区(RTL 从右到左布局) |
fa.lproj | 波斯语 | 伊朗等地区(RTL) |
每个语言包内文件职责:
Localizable.strings:主界面文案表,形如"购买记录" = "Purchase History";。业务代码LanguageCls.localizableTxt("购买记录")的 key 与之对应。InfoPlist.strings:本地化 App 显示名称与系统权限描述文案(如相册、蓝牙权限说明),随系统语言切换。LaunchScreen.strings:启动屏文案,保证冷启动阶段即显示正确语言。
SDK / 测试工程的资源访问
code/SDKTestHelper/ 使用 R.swift 生成类型安全的资源访问器(R.generated.swift),显式声明从 Localizable 表取词:
var localizable: localizable { .init(source: .init(bundle: bundle, tableName: "Localizable", preferredLanguages: preferredLanguages, locale: locale)) }
设计意图:与 LanguageCls.localizableTxt 一样都指向名为 Localizable 的翻译表,保证 App 与 SDK 测试工程共享同一套文案约定;同时 R.swift 方案在编译期生成资源符号,配合 preferredLanguages / locale 参数可精确指定取词语言,适合 SDK 演示 App 内做语言预览。
使用示例(来自实际源码)
页面标题取词
DialHistoryViewController(购买记录页)在初始化 UI 时通过导航栏标题使用本地化文案:
override func initUI() {
super.initUI()
navigationView.title = LanguageCls.localizableTxt("购买记录")
self.view.backgroundColor = .white
}
来源:DialHistoryViewController.swift
同理,表盘商城页标题、管理按钮也走同一入口:
navigationView.title = LanguageCls.localizableTxt("表盘")
...
editBtn.setTitle(LanguageCls.localizableTxt("管理"), for: .normal)
条件文案与字符串拼接
DialPayViewController(表盘购买页)根据价格与状态动态选择文案,并把本地化文本与数字拼接(杰币 单价以分为单位):
if dialModel.price == 0 {
watchSubTitleLab.text = LanguageCls.localizableTxt("免费")
}else{
watchSubTitleLab.text = LanguageCls.localizableTxt("杰币") + "\(dialModel.price/100)"
}
...
bottomBtn.setTitle(LanguageCls.localizableTxt("下载"))
...
if !dialModel.status {
bottomBtn.setTitle(LanguageCls.localizableTxt("购买"))
} else {
bottomBtn.setTitle(LanguageCls.localizableTxt("下载"))
}
来源:DialPayViewController.swift、DialPayViewController.swift
注意:拼接式文案("杰币" + "\(price/100)")在德语、阿拉伯语等语序/数字体系不同的语言中可能产生不自然表达;理想做法是使用带占位符的格式串(如 "杰币 %d")并通过 String(format:) 取词,属后续可优化点。
Toast 提示取词
网络请求失败等场景的即时提示同样走本地化:
guard let list = resp else {
AlertViewOnWindows.getFirstWindow()?.makeToast(LanguageCls.localizableTxt("网络有点问题"),position: .center)
return
}
语言环境判定(SDKTestHelper)
SDK 演示工程用 Locale.current.languageCode 判断是否中文环境,作为工具类/示例的本地化开关:
var isZh: Bool {
let languageCode = Locale.current.languageCode
return languageCode == "zh" || languageCode?.hasPrefix("zh-") == true
}
按功能分组定义语言(SDKTestHelper)
TranslateTools 按功能(通话 / 同步 / 表盘)组织文案,Switch 分支返回对应语言的文本常量:
case .call:
return TranslateTools.callLanguage
case .sync:
return TranslateTools.syncLanguage
case .face:
return TranslateTools.faceLanguage
设计意图:SDK 功能文案按域拆分(call / sync / face),避免单一超大字典,便于按功能模块独立维护和复用;这与 App 侧按 .lproj 语言包组织的思路互补——App 按语言横向划分,SDK 工具按功能纵向划分。
语言判定与切换流程
flowchart TD
Start([应用启动 / 切换系统语言]) --> GetLang["读取 Locale.current.languageCode"]
GetLang --> IsZh{"languageCode == \"zh\" 或<br/>以 \"zh-\" 前缀开头?"}
IsZh -->|"是"| Zh["中文环境<br/>命中 zh-Hans.lproj"]
IsZh -->|"否"| Match["按语言码匹配对应 .lproj"]
Match --> Found{"对应语言包存在?"}
Found -->|"是"| Use["使用该语言包取词"]
Found -->|"否"| Fallback["回退:返回原始 key(中文)"]
Zh --> Use
Use --> Done([界面显示本地化文本])
Fallback --> Done
关键点:
- 系统语言是唯一语言源:仓库内未发现 App 内手动切换语言的实现(无
UserDefaults覆盖、无语言设置页),语言完全跟随 iOS 系统设置,App 重启/系统切换后自动生效。这与多数健康类 App 的"跟随系统"策略一致,省去语言包热切换的复杂度。 zh-前缀匹配:SourceHelper.isZh与 iOS 语言码规则一致——zh-Hans、zh-Hant、zh-CN、zh-TW等都被识别为中文环境,避免按完整 locale 精确匹配导致的漏判。- RTL 语言:
ar.lproj、fa.lproj的存在意味着阿拉伯语/波斯语用户会获得从右到左的界面布局;文案中的语序、数字、标点需符合 RTL 阅读习惯。 - 回退策略:翻译表未命中时
localizableTxt返回中文原文 key,保证 UI 永不空白,便于发现漏翻译。
配置选项
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
.lproj 语言目录 | 目录 | 至少 zh-Hans / en / en-GB / de / es / fr / ar / fa | 每新增一种语言即新增一个目录及翻译文件 |
Localizable.strings | strings 表 | 每语言包必含 | 主界面文案表,key 为中文原文 |
InfoPlist.strings | strings 表 | 每语言包可选 | App 显示名称、权限描述文案 |
LaunchScreen.strings | strings 表 | 每语言包可选 | 启动屏文案 |
Locale.current.languageCode | 系统只读 | 系统设置 | 运行时语言判定唯一依据 |
tableName: "Localizable"(R.swift) | 编译期常量 | Localizable | SDK 侧取词表名,与 App 保持一致 |
注:工程级配置(如 Xcode 中
Info.plist的CFBundleDevelopmentRegion、knownRegions列表)不在本仓库源码文件内,具体值需在 Xcode 工程设置中确认。
API 参考
LanguageCls.localizableTxt(_ key: String) -> String
业务代码取词的统一入口(类方法,实现在 SDK/工具库中,仓库内未包含其定义源码;以下签名依据全部调用点的用法归纳)。
参数:
key(String):中文原文,同时作为翻译表Localizable.strings中的查找 key,如"购买记录"、"免费"、"网络有点问题"。
返回:String —— 当前系统语言对应的翻译文本;若语言包中不存在该 key,返回 key 本身(即中文原文)。
典型调用点(全部来自仓库实际源码):
| 调用场景 | 示例代码 | 来源 |
|---|---|---|
| 导航栏标题 | navigationView.title = LanguageCls.localizableTxt("购买记录") | DialHistoryViewController.swift |
| 按钮标题 | editBtn.setTitle(LanguageCls.localizableTxt("管理"), for: .normal) | DialViewController.swift |
| 条件文案 | watchSubTitleLab.text = LanguageCls.localizableTxt("免费") | DialPayViewController.swift |
| Toast 提示 | makeToast(LanguageCls.localizableTxt("网络有点问题"), position: .center) | DeviceSubViewModel.swift |
约束(依据调用点归纳,非源码显式声明):
- key 必须与
Localizable.strings中的条目一致;否则回退显示中文。 - 返回字符串可直接赋给
UILabel.text/UIButton.setTitle/ Toast 内容等。
Locale.current.languageCode(语言判定)
系统提供,返回当前首选语言的语言码(如 "zh"、"en"、"de")。SDK 侧通过 == "zh" || hasPrefix("zh-") 判定中文环境(见 SourceHelper.swift)。
失败模式与边界情况
- 漏翻译:某 key 在目标语言包缺失时,
localizableTxt回退返回中文原文。设计上保证 UI 不空白,但会造成"部分中文部分外语"的混合界面;QA 应逐语言包做全量 key 比对。 - 大小写与变体语言:
zh-CN/zh-TW/zh-HK等变体均以zh-前缀命中中文逻辑;若未来要区分简繁,需在取词层按完整语言码(zh-Hans/zh-Hant)分流,isZh这种粗粒度判断将不够用。 - RTL 布局:
ar/fa为从右到左语言,拼接式文案(如"杰币" + price)的语序、标点方向在 RTL 下可能错位,需逐条人工校验。 - 拼接/占位符:当前存在
"杰币" + "\(price/100)"的拼接写法,德语的"币种后置"、阿拉伯数字体系等场景下表达不准确;应迁移为String(format: "杰币 %d", ...)的格式串并做各语言包翻译。 - 动态内容:设备名、固件版本号等运行时数据若混入文案,需注意不要整串翻译(应拆分为"固定文案 + 参数"),否则语言包无法复用。
性能与运维注意
- 取词开销:
localizableTxt本质是Bundle内 strings 表的字典查找,命中后系统有缓存,单次调用开销可忽略;但不要在tableView的cellForRowAt高频路径上每次都传动态构造的 key,应复用常量。 - 语言包体积:多语言包会整体打进 App 包;对
ar/fa等 RTL 语言还需包含对应的布局资源,注意包体积与启动加载时间。 - 新增语言流程:新增
.lproj目录 → 翻译三个 strings 文件 → 确保 key 与中文原文一一对应 → 真机切换系统语言回归验证(重点:RTL 布局、启动屏、权限弹窗、拼接文案)。 - App 与 SDK 文案一致性:App 用
LanguageCls、SDKTestHelper 用 R.swift,二者都指向Localizable表,修改 key 时必须同步两处,否则 SDK 演示工程会出现回退中文。
扩展点
- 语言包扩展:新增语言只需添加
.lproj目录,业务代码零改动——这是本方案最大的扩展优势。 - 应用内语言切换:若未来需要"App 内选择语言",可在取词层引入
preferredLanguages覆盖(R.swift 已支持该参数,见 R.generated.swift),用UserDefaults保存用户选择并重建根视图即可,无需改业务调用点。 - 格式串迁移:将
"文案" + 变量改为String(format:)+ 占位符,可显著提升德语、阿拉伯语等语言的翻译质量,是当前最值得做的本地化增强。 - SDK 功能文案分组:
TranslateTools的 call / sync / face 分组模式可推广到更多 SDK 功能,保持文案按域可维护。