WWDC Quick Look 💓 By SwiftGGTeam
Build global apps: Localization by example

Build global apps: Localization by example

观看原视频

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.labelArchive.menuItem 是两个不同的本地化 key。
  • defaultValue 让英文运行时仍然显示同一个 “Archive”。
  • 第一个 comment 指明它是侧边栏里的文件夹名。
  • 第二个 comment 指明它是把邮件移入归档文件夹的菜单项。
  • 译者可以在其他语言里分别处理名词和动作。

03:40)字符串插值也要谨慎。Show weather in CupertinoShow 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) 标记需要随数量变形的片段。
  • amountOfRainnumberOfHours 都通过字符串插值进入本地化字符串。
  • 注释说明这是一段显示过去若干小时降雨量的标签。

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:22stringsdict 再负责语言里的复数分支。英文只用 other,西班牙语为 oneother 提供不同句子。

<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

评论

GitHub Issues · utterances