WWDC Quick Look 💓 By SwiftGGTeam
Localize your SwiftUI app

Localize your SwiftUI app

观看原视频

Highlight

SwiftUI 在 Text、LocalizedStringKey、Markdown、FormatStyle、键盘快捷键和 Xcode 13 导出流程中补齐本地化能力,让开发者把字符串、格式和布局交给系统处理,减少多语言适配中的手工修正。

核心内容

很多应用第一次做本地化,问题都出在细节。

英文界面里,一个按钮写成 Done,一个标签写成 Ingredients,看起来没有问题。到了俄语、阿拉伯语或立陶宛语,文本长度、语义上下文、书写方向、键盘布局都会变化。

过去,开发者经常要自己检查这些变化。哪些字符串能导出给翻译?哪个 Ingredients 是菜谱里的“配料”,哪个是菜单里的“原料”?卡路里单位要怎样按地区显示?快捷键在非美式键盘上能不能按出来?这些问题很容易拖到发布前才暴露。

Apple 在这场 session 里,用 Fruta 示例应用演示了一条新的路径:SwiftUI 默认把 Text 的字符串字面量当成本地化键;自定义视图可以用 LocalizedStringKey 保持同样行为;样式交给 Markdown;数值交给新的格式化 API;布局和键盘快捷键交给系统自动适配。

Xcode 13 也改变了导出流程。导出本地化内容时,Xcode 可以构建目标,并通过 Swift 编译器提取 LocalizedStringKey。多行字符串、SwiftUI 自定义视图里的字符串,都能更准确地进入 localization catalog。

结果是,开发者不需要为每一种语言写一套 UI。你只要把可翻译内容暴露出来,给翻译足够上下文,再用系统格式化数据,SwiftUI 会处理大量语言差异。

详细内容

字符串字面量会自动查本地化表

01:34)SwiftUI 的 Text 接收字符串字面量时,会在 main bundle 里做 localized string lookup。按钮标题、标签、提示文案都可以从这里开始。

import SwiftUI

struct RewardsSheet: View {
    let done: () -> Void

    var body: some View {
        Button(action: done) {
            Text("Done", comment: "Button title to dismiss rewards sheet")
        }
    }
}

关键点:

  • Button(action: done) 使用传入的 done 闭包关闭奖励页面。
  • Text("Done", comment: ...)Done 暴露为可本地化字符串。
  • comment 给翻译说明语境:这是关闭奖励 sheet 的按钮标题。
  • 运行时,SwiftUI 根据当前语言从 bundle 中查找对应翻译。

01:58)字符串插值也会被提取。Xcode 13 会根据插值变量的类型推断 format specifier。

import SwiftUI

struct RewardsCard: View {
    let totalStamps: Int

    var body: some View {
        Text("You are \(10 - totalStamps) points away from a free smoothie!")
    }
}

关键点:

  • totalStamps 是用户已经获得的点数。
  • 10 - totalStamps 计算距离免费 smoothie 还差多少点。
  • 插值表达式会进入导出的本地化字符串文件或 catalog。
  • Xcode 13 根据 Int 类型推断格式占位符,减少手工写错 %d%@ 的机会。

02:06)同一个英文词在不同场景里可能需要不同译法。Text 可以通过 tableName 把字符串放进独立表。

import SwiftUI

struct IngredientHeaders: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text(
                "Ingredients.recipe",
                tableName: "Ingredients",
                comment: "Ingredients in a recipe. For languages that have different words for \"Ingredient\" based on semantic context."
            )
            Text(
                "Ingredients.menu",
                tableName: "Ingredients",
                comment: "Ingredients in a smoothie. For languages that have different words for \"Ingredient\" based on semantic context."
            )
        }
    }
}

关键点:

  • 两个字符串都放在 Ingredients 表里,便于管理同一类文案。
  • Ingredients.recipe 用在菜谱上下文。
  • Ingredients.menu 用在 smoothie 菜单上下文。
  • comment 明确告诉翻译:某些语言会根据语义上下文选择不同词。

自定义 SwiftUI 视图要接收 LocalizedStringKey

02:52)问题经常发生在自定义视图。你把 String 传给组件,再在组件内部交给 Text,Xcode 可能无法把这个字面量当成 SwiftUI 本地化键处理。

解决方式是把参数类型改成 LocalizedStringKey

import SwiftUI

struct Card: View {
    var title: LocalizedStringKey
    var subtitle: LocalizedStringKey

    var body: some View {
        Circle()
            .fill(BackgroundStyle())
            .overlay(
                VStack(spacing: 16) {
                    Text(title)
                    Text(subtitle)
                }
            )
    }
}

struct OrderCompleteView: View {
    var body: some View {
        Card(
            title: "Thank you for your order!",
            subtitle: "We will notify you when your order is ready."
        )
    }
}

关键点:

  • titlesubtitle 使用 LocalizedStringKey,保留 SwiftUI 的本地化语义。
  • Text(title)Text(subtitle) 在运行时从 bundle 读取翻译。
  • Card(...) 调用处仍然传字符串字面量,写法不变。
  • Xcode 导出本地化内容时,可以提取传给 Card 的标题和副标题。

03:47)Xcode 13 导出本地化内容时可以使用编译器。多行字符串也能被正确解析。

import SwiftUI

struct SmoothieDescription: View {
    var body: some View {
        Text("""
        A delicious blend of tropical fruits and blueberries will
        have you sambaing around like you never knew you could!
        """,
        comment: "Tropical Blue smoothie description")
    }
}

关键点:

  • 三引号声明多行字符串,适合较长描述文案。
  • Text 仍然接收可本地化内容。
  • comment 标明这是 Tropical Blue smoothie 的描述。
  • Xcode 13 借助 Swift 编译器提取字符串,减少多行文本解析失败。

布局默认支持更长文本和从右到左语言

04:12)本地化之后,文本通常会变长。SwiftUI 控件在合适场景下会让 Text 换行,避免直接裁切。

04:26)从右到左语言也会影响布局方向。SwiftUI 会翻转表格单元格布局,并在合适时镜像 tab bar 里的符号。

开发者需要避免写死 leftright。Apple 在演示中强调了 leading 这样的方向无关写法。

import SwiftUI

struct SmoothieSummary: View {
    let smoothieTitle: LocalizedStringKey
    let ingredients: LocalizedStringKey

    var body: some View {
        VStack(alignment: .leading) {
            Text(smoothieTitle)
                .font(.headline)
            Text(ingredients)
        }
    }
}

关键点:

  • VStack(alignment: .leading) 使用语义方向。
  • 在从左到右语言里,leading 通常对应左侧。
  • 在从右到左语言里,系统可以把 leading 映射到另一侧。
  • Text 负责显示本地化后的 smoothie 名称和配料文字。

Markdown 让翻译决定样式落点

05:13)SwiftUI 的 Text 可以直接显示带 Markdown 的本地化字符串。这样做的重点是把样式和翻译放在同一个字符串里。

import SwiftUI

struct LemonberryDescription: View {
    var body: some View {
        Text(
            "A refreshing blend that's a *real kick*!",
            comment: "Lemonberry smoothie description"
        )
    }
}

关键点:

  • *real kick* 使用 Markdown emphasis。
  • 字符串仍然会作为本地化内容导出。
  • 翻译可以根据目标语言调整被强调的词。
  • SwiftUI 直接渲染带样式的 Text,不需要拆分多个文本片段。

FormatStyle 直接处理地区化数值

06:04)iOS 15 之前,Fruta 需要创建 MeasurementFormatter,再把卡路里转成字符串。

import Foundation
import SwiftUI

struct LegacyCaloriesView: View {
    let nutritionFactKilocalories: Double

    static let measurementFormatter: MeasurementFormatter = {
        let formatter = MeasurementFormatter()
        formatter.unitStyle = .long
        formatter.unitOptions = .providedUnit
        return formatter
    }()

    var body: some View {
        let calories = Measurement<UnitEnergy>(
            value: nutritionFactKilocalories,
            unit: .kilocalories
        )

        return VStack(alignment: .leading) {
            Text(Self.measurementFormatter.string(from: calories))
            Text("Energy: \(calories, formatter: Self.measurementFormatter)")
        }
    }
}

关键点:

  • Measurement<UnitEnergy> 表示带单位的能量值。
  • MeasurementFormatter 设置单位样式为 .long
  • .providedUnit 保留传入的 .kilocalories 单位。
  • 第一个 Text 先把测量值转成字符串。
  • 第二个 Text 在插值中使用 formatter。

06:22)iOS 15 的新格式化 API 可以在展示位置声明格式。

import Foundation
import SwiftUI

struct CaloriesView: View {
    let nutritionFactKilocalories: Double

    var body: some View {
        let calories = Measurement<UnitEnergy>(
            value: nutritionFactKilocalories,
            unit: .kilocalories
        )

        return VStack(alignment: .leading) {
            Text(calories.formatted(.measurement(width: .wide, usage: .food)))
            Text("Energy: \(calories, format: .measurement(width: .wide, usage: .food))")
        }
    }
}

关键点:

  • calories.formatted(...) 直接返回适合当前 locale 的显示文本。
  • .measurement(width: .wide, usage: .food) 声明测量值的展示宽度和使用场景。
  • usage: .food 告诉系统这是食品能量场景。
  • Text 的插值可以直接接收 format:,代码更靠近 UI 语义。

键盘快捷键会按当前键盘布局重映射

06:53)macOS Monterey 和 iPadOS 15 会根据用户当前键盘布局调整 SwiftUI 里定义的快捷键。

Apple 的例子是 Command +。在美式键盘上它容易输入;在立陶宛语键盘上,原来的组合无法在按住 Command 时打出。系统会把它重映射成能输入的组合。

import SwiftUI

struct SmoothieCommands: Commands {
    let smoothie: Smoothie

    var body: some Commands {
        CommandMenu(Text("Smoothie", comment: "Menu title for smoothie-related actions")) {
            SmoothieFavoriteButton(smoothie)
                .keyboardShortcut("+")
        }
    }
}

struct Smoothie {}

struct SmoothieFavoriteButton: View {
    let smoothie: Smoothie

    init(_ smoothie: Smoothie) {
        self.smoothie = smoothie
    }

    var body: some View {
        Button("Favorite") {}
    }
}

关键点:

  • CommandMenu 的标题用 Text,可以添加翻译注释。
  • SmoothieFavoriteButton(smoothie) 是菜单中的动作入口。
  • .keyboardShortcut("+") 定义添加收藏的快捷键。
  • 系统根据当前键盘布局调整可输入的组合,开发者不需要为每种键盘布局写分支。

Xcode 13 的本地化工作流更依赖编译器

08:47)Xcode 13 增强了 Swift 字符串提取。项目里的 Build Setting Use Compiler to Extract Swift Strings 对新 Swift 项目默认开启,旧 SwiftUI 项目可以手动打开。

import SwiftUI

struct StepperView: View {
    var label: LocalizedStringKey
    @Binding var value: Int

    var body: some View {
        Stepper(value: $value) {
            Text(label)
        }
    }
}

关键点:

  • label 使用 LocalizedStringKey,避免自定义视图吞掉本地化信息。
  • @Binding var value 接收外部状态。
  • Stepper(value: $value) 修改绑定值。
  • Text(label) 把可本地化标签显示在步进器旁边。

13:53)Xcode localization catalog 会展示 key、source string、translation 和 comment。翻译看不到变量名,所以有歧义的字符串要补注释。

import SwiftUI

struct FavoritesTab: View {
    var body: some View {
        Label {
            Text("Favorites", comment: "Tab title. Noun: the list of favorite smoothies.")
        } icon: {
            Image(systemName: "heart")
        }
    }
}

关键点:

  • Label 用于 tab bar 项。
  • 标题用 Text 初始化,才能提供 comment
  • 注释说明 Favorites 是名词,指收藏列表。
  • 翻译人员不用猜这是动作还是栏目名称。

核心启发

  • 做什么:给自定义组件做一次本地化类型审计。 为什么值得做:这场 session 明确指出,自定义 SwiftUI 视图应使用 LocalizedStringKey 或接收 Text,否则字符串可能无法正确导出。 怎么开始:搜索项目里的 var title: Stringlet label: String,优先把展示型参数改成 LocalizedStringKey,再用伪语言预览检查。

  • 做什么:为支付、收藏、订阅等高风险文案补翻译注释。 为什么值得做:Xcode catalog 里翻译看不到变量名,Buy recipe for %@ 这类字符串会产生歧义。 怎么开始:把 Text("Favorites") 改成 Text("Favorites", comment: "...")Label 需要用 Label { Text(...) } icon: { ... } 写法。

  • 做什么:把数值展示从手写字符串迁移到 FormatStyle为什么值得做:卡路里、长度、重量、日期在不同地区有不同格式,Text("%lf Calories") 会把地区差异丢给翻译。 怎么开始:把 MeasurementFormatter 或字符串拼接替换为 value.formatted(...),在 Text 插值里使用 format:

  • 做什么:用 Markdown 管理本地化文本里的强调样式。 为什么值得做:不同语言强调的位置可能不同,把样式嵌在可翻译字符串里,翻译可以调整重点词。 怎么开始:把多个 Text 拼接的强调文案合并成一个 Markdown 字符串,例如 Text("A *real kick*!")

  • 做什么:发布前加入伪语言和键盘布局检查。 为什么值得做:伪语言能暴露漏提取的字符串,非美式键盘能暴露不可输入的快捷键。 怎么开始:在 scheme editor 的 App Language 里选择 Accented Pseudolanguage;为常用命令保留 .keyboardShortcut(...),让 macOS 和 iPadOS 做布局重映射。

关联 Session

评论

GitHub Issues · utterances