Highlight
Apple 本地化团队的 Andreas 以天气应用为核心案例,从字符串声明、格式化到布局适配,系统性地讲解了如何构建高质量的多语言应用。
核心内容
天气 App 看起来很普通:一行风速说明、一个降雨量数字、一张十天预报表。
放到多语言环境里,它们会变成三个工程问题。
同一个英文词,在西班牙语里可能因为语境变成两个词。一个城市名插入句子,在德语里可能改变介词。一个 44 点高的标签,在印地语里可能放不下文字。
这场 Session 用天气 App 串起完整链路:先把字符串写成可翻译的资源,再用系统 API 选择远端内容语言,接着用格式化 API 处理数字、单位和复数,最后让 SwiftUI 布局承接更长、更高、方向不同的文本。
详细内容
字符串要从代码里给出语境
(01:59)本地化的第一步,是用 String(localized:) 声明用户可见文本。Xcode 会在导出本地化内容时发现这些字符串,并交给译者处理。
let windPerceptionLabelText = String(
localized: "Wind is making it feel cooler",
comment: "Explains the wind is lowering the apparent temperature"
)
关键点:
String(localized:)标记这段文本需要进入本地化流程。localized里的英文是开发语言下的显示文本。comment告诉译者这句话描述的是风让体感温度更低。- 注释属于翻译上下文,不会显示给用户。
(02:46)问题会出现在同一个英文词上。Mail 里的 “Archive” 既可以是侧边栏文件夹名称,也可以是菜单动作。英文相同,西班牙语需要不同译法。
let filter = String(localized: "Archive.label",
defaultValue: "Archive",
comment: "Name of the Archive folder in the sidebar")
let filter = String(localized: "Archive.menuItem",
defaultValue: "Archive",
comment: "Menu item title for moving the email into the Archive folder")
关键点:
Archive.label和Archive.menuItem是两个不同的本地化 key。defaultValue让英文运行时仍然显示同一个 “Archive”。- 第一个
comment指明它是侧边栏里的文件夹名。 - 第二个
comment指明它是把邮件移入归档文件夹的菜单项。 - 译者可以在其他语言里分别处理名词和动作。
(03:40)字符串插值也要谨慎。Show weather in Cupertino 和 Show weather in My Location 在英文里都能成立,在德语里却可能需要不同语法。
String(localized: "Show weather in \(locationName)",
comment: "Title for a user activity to show weather at a specific city")
String(localized: "Show weather in My Location",
comment: "Title for a user activity to show weather at the user's current location")
关键点:
- 第一段只用于插入具体城市名。
locationName的运行时含义在注释里写清楚。- “My Location” 被拆成独立字符串,而不是当作变量塞进同一句话。
- 这样译者可以为当前定位场景选择不同的语法。
远端内容也要匹配用户语言
(06:40)天气预警来自服务器。服务器可能支持多种语言,但设备才知道用户的语言偏好。Apple 建议让服务器返回支持的语言 ID,再由客户端调用 Bundle.preferredLocalizations(from:) 做匹配。
let allServerLanguages = ["bg", "de", "en", "es", "kk", "uk"]
let language = Bundle.preferredLocalizations(from: allServerLanguages).first
关键点:
allServerLanguages表示服务器可返回内容的语言列表。- 语言 ID 交给系统框架处理,不需要自己比较用户偏好。
Bundle.preferredLocalizations(from:)返回按匹配程度排序的候选语言。.first通常是最适合当前用户设置的语言。- 后续请求应该使用这个语言加载用户可见内容。
这个细节很容易漏掉。App 壳本地化了,服务端警报还是另一种语言,用户在关键时刻就读不懂信息。
数字、单位、复数交给系统格式化
(07:56)降雨量这样的文案常常带数字。英文里 “one hour” 和 “two hours” 已经不同,乌克兰语等语言的复数规则更复杂。Session 给出两个方向:stringsdict 复数规则,或 Automatic Grammar Agreement(自动语法一致)。
String(localized: "\(amountOfRain) in last \(numberOfHours) hour",
comment: "Label showing how much rain has fallen in the last number of hours")
String(localized: "\(amountOfRain) in last ^[\(numberOfHours) hour](inflect: true)",
comment: "Label showing how much rain has fallen in the last number of hours")
关键点:
- 第一段把
numberOfHours直接插入英文句子。 - 第二段用
^[...](inflect: true)标记需要随数量变形的片段。 amountOfRain和numberOfHours都通过字符串插值进入本地化字符串。- 注释说明这是一段显示过去若干小时降雨量的标签。
(09:29)如果值带有单位、百分比、日期或时间,优先使用格式化 API。它会处理符号位置、空格和用户偏好的数字系统。
let humidity = 54
// In a SwiftUI view
Text(humidity, format: .percent)
// In Swift code
humidity.formatted(.percent)
关键点:
humidity保存原始数值。- SwiftUI 里用
Text(_:format:)直接指定百分比格式。 - 普通 Swift 代码里用
.formatted(.percent)得到格式化结果。 - 百分号位置和数字系统由系统根据用户设置处理。
(11:10)更复杂的例子是未来 24 小时降雨量。代码先把服务器返回的毫米值转成用户偏好的单位,再把格式化后的值放进可本地化句子。
func expectedPrecipitationIn24Hours(for valueInMillimeters: Measurement<UnitLength>) -> String {
// Use user's preferred measures
let preferredUnit = UnitLength(forLocale: .current, usage: .rainfall)
let valueInPreferredSystem = valueInMillimeters.converted(to: preferredUnit)
// Format the amount of rainfall
let formattedValue = valueInPreferredSystem
.formatted(.measurement(width: .narrow, usage: .asProvided))
let integerValue = Int(valueInPreferredSystem.value.rounded())
// Load and use formatting string
return String(localized: "EXPECTED_RAINFALL",
defaultValue: "\(integerValue) \(formattedValue) expected in next \(24)h.",
comment: "Label - How much precipitation (2nd formatted value, in mm or Inches) is expected in the next 24 hours (3rd, always 24).")
}
关键点:
- 函数参数
Measurement<UnitLength>表示服务器传入的降雨量。 UnitLength(forLocale: .current, usage: .rainfall)选择当前用户查看降雨量时偏好的单位。converted(to:)把毫米值转成用户单位。.formatted(.measurement(width: .narrow, usage: .asProvided))生成窄宽度的单位文本。integerValue用于触发复数规则。EXPECTED_RAINFALL对应stringsdict里的 key。defaultValue同时插入复数判断值、格式化后的降雨量和固定的 24 小时。comment明确说明每个插值参数的含义,方便译者处理语序。
(12:22)stringsdict 再负责语言里的复数分支。英文只用 other,西班牙语为 one 和 other 提供不同句子。
<key>EXPECTED_RAINFALL</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@next_expected_precipitation_amount_24h@</string>
<key>next_expected_precipitation_amount_24h</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Se prevé %2$@ en las próximas %3$d h.</string>
<key>other</key>
<string>Se prevén %2$@ en las próximas %3$d h.</string>
</dict>
</dict>
关键点:
EXPECTED_RAINFALL和 Swift 代码里的 key 对应。NSStringLocalizedFormatKey指向一个复数规则条目。NSStringPluralRuleType表示按语言复数规则选择分支。NSStringFormatValueTypeKey使用d,对应 Swift 里传入的整数值。one是西班牙语单数分支。other是其他数量分支。%2$@使用格式化后的降雨量文本。%3$d使用固定的小时数。
Swift Package 也进入 Xcode 本地化流程
(13:40)如果用户可见文本在 Swift Package 里,需要在包清单里声明默认本地化语言。Xcode 2022 会读取这个参数,并在 Product 菜单里为包提供 Export Localizations / Import Localizations 工作流。
let package = Package(
name: "FoodTruckKit",
defaultLocalization: "en",
products: [
.library(
name: "FoodTruckKit",
targets: ["FoodTruckKit"]),
],
…
)
关键点:
Package描述 Swift Package 的结构。defaultLocalization: "en"声明包内用户可见内容的开发语言是英文。- Xcode 看到这个设置后,会把包纳入本地化导出和导入流程。
- 本地化文件会通过
.xcloc交给译者,再导回包内正确路径。
(14:41)包内加载字符串时,还要指定 bundle: .module。
let title = String(localized: "Wind",
bundle: .module,
comment: "Title for section that
shows data about wind.")
关键点:
localized: "Wind"是包内用户可见标题。bundle: .module告诉系统从当前 Swift Package 的资源包查找本地化内容。comment说明这个标题用于显示风相关数据的区域。
布局要允许文本变长、变高、变方向
(16:37)文字翻译后可能更长,也可能因为书写系统更高。Session 明确提醒:不要因为英文 44 点高能放下,就给所有语言固定高度。
(18:19)SwiftUI 的 Grid 适合天气预报这种按列对齐的界面。最长的星期标签决定列宽,天气图标仍能保持垂直对齐。
// Requires data types "Row" and "row" to be defined
struct WeatherTestView: View {
var rows: [Row]
var body: some View {
Grid(alignment: .leading) {
ForEach(rows) { row in
GridRow {
Text(row.dayOfWeek)
Image(systemName: row.weatherCondition)
.symbolRenderingMode(.multicolor)
Text(row.minimumTemperature)
.gridColumnAlignment(.trailing)
Capsule().fill(Color.orange).frame(height: 4)
Text(row.maximumTemperature)
}
.foregroundColor(.white)
}
}
}
}
关键点:
Grid(alignment: .leading)使用前导对齐;从左到右语言在左侧对齐,从右到左语言在右侧对齐。ForEach(rows)为每一天生成一行。GridRow定义一组横向内容。Text(row.dayOfWeek)显示星期标签,列宽会受最长文本影响。Image(systemName:)显示天气符号。.gridColumnAlignment(.trailing)让最低温列按尾部对齐。Capsule()是最灵活的元素,文本变长时可以收缩。foregroundColor(.white)统一设置行内前景色。
(19:53)空间不够时,SwiftUI 还提供 ViewThatFits。逐字稿里的做法是准备多个布局,放进 ViewThatFits,让 SwiftUI 选择第一个不会裁剪的版本。Apple 同时强调:只切换布局,不要因为翻译太长就隐藏界面元素。
核心启发
1. 给现有项目做一次字符串语境审计
- 做什么:找出相同英文文本在不同界面位置重复使用的地方。
- 为什么值得做:Session 的 “Archive” 示例说明,同一个英文词在其他语言里可能需要不同译法。
- 怎么开始:把复用字符串拆成不同 key,使用
String(localized:defaultValue:comment:),在comment里写清楚控件类型和所在界面。
2. 为服务端文案加语言协商
- 做什么:让客户端根据用户语言偏好选择服务端内容语言。
- 为什么值得做:天气预警这类远端内容如果没有本地化,用户可能读不懂关键提示。
- 怎么开始:让服务端返回支持的语言 ID 数组,客户端调用
Bundle.preferredLocalizations(from:),把第一个候选语言用于后续请求。
3. 把数字文案从字符串拼接改成格式化 API
- 做什么:检查百分比、日期、单位、文件大小、距离和降雨量等显示逻辑。
- 为什么值得做:格式化 API 会处理单位、符号位置和用户偏好的数字系统。
- 怎么开始:SwiftUI 里使用
Text(value, format:),普通 Swift 代码里使用value.formatted(...),带复数的句子再接入stringsdict。
4. 让 Swift Package 暴露本地化资源
- 做什么:为包含用户可见文本的 Swift Package 补齐本地化配置。
- 为什么值得做:Xcode 可以直接为包导出和导入
.xcloc,库作者能把本地化纳入常规发布流程。 - 怎么开始:在
Package.swift里设置defaultLocalization,包内字符串加载时指定bundle: .module。
5. 用真实语言测试布局弹性
- 做什么:用阿拉伯语、西班牙语、希腊语、印地语等语言跑一遍核心界面。
- 为什么值得做:逐字稿展示了方向变化、文本变长、文字变高都会影响布局。
- 怎么开始:移除固定高度假设;十天预报这类表格界面试用
Grid;空间受限的操作区准备多个布局并交给ViewThatFits选择。
关联 Session
- Get it right (to left) — 继续讲从右到左语言下的布局方向、符号和界面适配。
- Design for Arabic — 从设计层面补充阿拉伯语界面的排版、文本和文化细节。
- Compose custom layouts with SwiftUI — 深入讲解 SwiftUI Grid、自定义 Layout 和适配不同内容尺寸的布局能力。
评论
GitHub Issues · utterances