WWDC Quick Look 💓 By SwiftGGTeam
Streamline your localized strings

Streamline your localized strings

观看原视频

Highlight

Xcode 13 和 Foundation 在 iOS 15、macOS Monterey 中改进了本地化字符串声明、提取、导入和语法处理流程,让开发者用 SwiftUI、String(localized:)AttributedString(localized:) 和格式化 API 更可靠地交付多语言界面。

核心内容

一个售票 App 的按钮写着 Order。英文用户能理解它是“下单”。翻译人员看到这个词时,可能不知道它是按钮、名词、排序动作,还是订单状态。

这就是本地化最常见的麻烦。开发者写 UI 时知道上下文。翻译人员处理 .strings 文件时只看到一行文本。相同英文词在不同语言里可能要使用不同词形。带数量、性别、货币、列表的字符串还要跟随用户语言和地区变化。

过去,开发者经常把字符串拼接起来,用 String(format:) 填变量,再手动维护 .strings.stringsdict 文件。代码能跑,翻译质量容易出问题。阿拉伯语这样的从右到左语言会涉及数字和变量隔离。俄语这样的语言有多套复数规则。西班牙语还会遇到称谓和性别一致。

Apple 在这场 session 里把流程拆成三件事。

第一,写代码时直接声明可本地化字符串。SwiftUI 的 TextButton 默认会把字符串作为本地化内容。普通 Swift 代码使用 iOS 15 和 macOS Monterey 新增的 String(localized:)

第二,把上下文交给本地化系统。每个字符串都应该带 comment。复杂项目可以用 table 分文件,用 bundle 指向框架或扩展自己的资源。

第三,让 Xcode 13 生成和更新本地化文件。Export Localizations 会从 Swift、Foundation 方法、Info.plist 和被标记为 localized 的资源里提取内容。Import Localizations 会把翻译结果写回 .strings.stringsdict 和其他资源。

这样做以后,增加语言主要变成资源更新和测试工作。UI 代码不用为了每一种语言改分支。

详细内容

1. 在 UI 代码里声明本地化字符串

03:30)SwiftUI 视图里的可见文本默认参与本地化。普通 Swift、UIKit 或 AppKit 代码里,可以使用 String(localized:) 显式声明。

import SwiftUI

struct OrderView: View {
    var body: some View {
        VStack(spacing: 12) {
            Text("Your order is ready.")

            Button("Order") {
                placeOrder()
            }

            Text(verbatim: "Sample data")
        }
    }

    private func placeOrder() {
    }
}

let title = String(localized: "Order")

关键点:

  • Text("Your order is ready.") 是 SwiftUI 文本视图,字符串会作为本地化内容处理。
  • Button("Order") 声明按钮标题,标题同样会进入本地化流程。
  • placeOrder() 是按钮动作,和本地化无关。
  • Text(verbatim: "Sample data") 明确告诉 SwiftUI 这段文本不需要翻译,适合预览或 mock 数据。
  • String(localized: "Order") 用在 SwiftUI 视图之外,例如 model、UIKit、AppKit 或返回字符串的函数。

05:15)字符串插值也能参与本地化。Apple 在演示里把票数放进按钮标题。

import SwiftUI

struct TicketOrderButton: View {
    let count = 3

    var body: some View {
        Button("Order \(count) Tickets") {
            placeOrder(count: count)
        }
    }

    private func placeOrder(count: Int) {
    }
}

let count = 3
let title = String(localized: "Order \(count) Tickets")

关键点:

  • let count = 3 提供运行时变量。
  • Button("Order \(count) Tickets") 使用 Swift 字符串插值,按钮显示时会替换为真实数字。
  • String(localized:) 会处理用户偏好的数字形式、复数和从右到左文本里的变量隔离。
  • session 明确提醒不要把本地化字符串交给 String(format:) 处理。

2. 给翻译人员留下上下文

07:09)翻译人员逐条处理字符串。看不到完整界面时,comment 就是上下文。

import SwiftUI

struct OrderSummaryView: View {
    let ticketCount: Int

    var body: some View {
        VStack(spacing: 12) {
            Text("Order", comment: "Button: confirms concert tickets booking")

            Text("\(ticketCount) Ordered",
                 comment: "Order summary: total number of tickets ordered")
        }
    }
}

关键点:

  • Text("Order", comment: ...) 说明 Order 是确认订票的按钮,避免被误译成名词“订单”或排序动作。
  • comment 的第一部分写出字符串出现的位置,例如 ButtonOrder summary
  • comment 的第二部分写出业务动作,例如 confirms concert tickets booking
  • Text("\(ticketCount) Ordered", comment: ...) 解释变量含义,翻译人员不会看到 Swift 变量名,只会看到占位符。

06:21)相似的英文片段不要为了省代码拼在一起。不同语言可能需要不同词形。

import Foundation

let orderNow = String(localized: "Order Now")
let orderLater = String(localized: "Order Later")

关键点:

  • Order Now 是一个完整可翻译句子。
  • Order Later 也是一个独立句子。
  • session 使用这个例子说明,Order 在“现在下单”和“稍后下单”里可能需要不同翻译。
  • 完整句子给翻译人员更多上下文,也避免运行时拼接造成语序错误。

3. 用 table 和 bundle 管理字符串位置

11:37)字符串默认进入 Localizable.strings。如果项目变大,可以用 table 按功能或页面拆分。

import SwiftUI

struct UserProfileSubtitle: View {
    let ticketCount: Int

    var body: some View {
        Text("\(ticketCount) Ordered",
             tableName: "UserProfile",
             comment: "Profile subtitle: total number of tickets ordered")
    }
}

关键点:

  • Text 仍然负责展示本地化字符串。
  • tableName: "UserProfile" 指定字符串所在表,Xcode 会对应到 UserProfile.strings
  • session 的例子里,法语翻译会放进 fr.lproj/UserProfile.strings
  • comment 继续随字符串导出,帮助翻译人员理解 ticketCount 的含义。

13:49)框架里的字符串应该从框架自己的 bundle 读取。否则宿主 App 会去自己的资源里找,结果找不到。

import SwiftUI

public final class AnyClassInTicketKit {
}

public enum OrderStatus {
    case pending, processing, complete, canceled, invalid(Error)

    var displayName: String {
        switch self {
        case .complete:
            return String(localized: "Complete",
                          bundle: Bundle(for: AnyClassInTicketKit.self),
                          comment: "Standalone ticket status: order finalized")
        case .pending:
            return String(localized: "Pending",
                          bundle: Bundle(for: AnyClassInTicketKit.self),
                          comment: "Standalone ticket status: order waiting to start")
        case .processing:
            return String(localized: "Processing",
                          bundle: Bundle(for: AnyClassInTicketKit.self),
                          comment: "Standalone ticket status: order being processed")
        case .canceled:
            return String(localized: "Canceled",
                          bundle: Bundle(for: AnyClassInTicketKit.self),
                          comment: "Standalone ticket status: order canceled")
        case .invalid:
            return String(localized: "Invalid",
                          bundle: Bundle(for: AnyClassInTicketKit.self),
                          comment: "Standalone ticket status: order invalid")
        }
    }
}

struct OrderStatusLabel: View {
    var body: some View {
        Text(OrderStatus.complete.displayName)
    }
}

关键点:

  • AnyClassInTicketKit 用来定位 TicketKit 框架所在的 bundle。
  • OrderStatus 是框架暴露给宿主 App 的模型。
  • String(localized: "Complete", bundle: ...) 指向框架自己的本地化资源。
  • comment 说明 Complete 是订单状态,避免被误译成按钮动作。
  • Text(OrderStatus.complete.displayName) 让宿主 App 直接使用框架提供的本地化结果。

4. 让 Xcode 13 提取、编辑、导入本地化目录

14:40)Xcode 13 增加 Swift 字符串提取的编译器支持。Export Localizations 会读取 Swift 和 Foundation 方法,提取本地化内容。

import SwiftUI

struct CheckoutView: View {
    let ticketCount: Int
    let price: Decimal

    var body: some View {
        VStack(spacing: 12) {
            Text("\(ticketCount) Ordered",
                 tableName: "UserProfile",
                 comment: "Profile subtitle: total number of tickets ordered")

            Text("Total: \(price, format: .currency(code: "USD"))",
                 comment: "Order subtitle: total price of all tickets")
        }
    }
}

关键点:

  • Text("\(ticketCount) Ordered", tableName: ..., comment: ...) 会被 Xcode 提取。
  • Text("Total: ...", comment: ...) 也会被提取,comment 会一起进入本地化目录。
  • session 说明 Xcode 还会提取 Info.plist 里的 App 名称、隐私描述,以及 inspector 中标记为 localized 的资源。
  • 如果项目用自定义 wrapper 包住本地化 API,默认不会被提取;需要在 Build Settings 的 Localized String Macro Names 中配置。

17:43)导出和导入可以在 Xcode 菜单里完成,也可以放进 CI。

xcodebuild -exportLocalizations -workspace VacationPlanet.xcworkspace -localizationPath ~/Documents
xcodebuild -importLocalizations -workspace VacationPlanet.xcworkspace -localizationPath ~/Documents/de.xcloc

关键点:

  • 第一行导出本地化目录,把项目里的字符串、图片、文件和相关上下文交给翻译流程。
  • -workspace VacationPlanet.xcworkspace 指定工作区,session 提到 Xcode 13 已支持 workspace。
  • -localizationPath ~/Documents 指定导出位置。
  • 第二行导入德语 .xcloc 目录,Xcode 会创建或更新 .strings.stringsdict 和其他本地化资源。
  • 这两个命令可以定期运行,让新增 UI 字符串更快进入翻译流程。

5. 本地化富文本、复数、语法和格式化数据

18:28AttributedString(localized:) 支持 Markdown 语法。开发者可以本地化带样式的文本,不用手动计算字符范围。

import Foundation

let title = AttributedString(localized: "Your order is **complete**!",
                             comment: "Ticket order confirmation title")

关键点:

  • AttributedString(localized:) 创建 Swift 原生富文本。
  • **complete** 使用 Markdown 加粗语法。
  • 整个句子参与本地化,翻译后仍能保留格式信息。
  • comment 告诉翻译人员这是订票确认标题。

19:22)包含数字的复数字符串应该交给 .stringsdict。代码不需要为每种语言写复数分支。

import Foundation

func orderTitle(ticketCount: Int) -> String {
    String(localized: "Order \(ticketCount) Ticket(s)")
}

关键点:

  • ticketCount 是决定复数形式的数字。
  • String(localized:) 读取本地化资源。
  • .stringsdict 负责把这个 key 映射到各语言的复数规则。
  • session 提到 Xcode 导出时会按语言补充需要的复数类别,例如俄语会需要更多规则。

22:46)没有数字的“单个、两个、全部”不能用 .stringsdict 推断。这里应该写清楚三个字符串。

import Foundation

func orderButtonTitle(ticketCount: Int) -> String {
    if ticketCount == 1 {
        return String(localized: "Order This Ticket")
    } else if ticketCount == 2 {
        return String(localized: "Order Both Tickets")
    } else {
        return String(localized: "Order All Tickets")
    }
}

关键点:

  • ticketCount == 1 对应“这张票”。
  • ticketCount == 2 对应“两张票”。
  • else 对应“全部票”。
  • session 用俄语举例说明,.stringsdictone 规则不等于数字只为 1,21、31 也可能落入同一类。

23:31)Foundation 在 iOS 15 和 macOS Monterey 中加入语法一致能力。Markdown 里的 inflect: true 可以让运行时计算正确词形。

import Foundation

let ticketsCount = 3
let title = AttributedString(localized: "Order ^[\(ticketsCount) Ticket](inflect: true)")

关键点:

  • ticketsCount 是数量变量。
  • ^[...] 标记需要语法处理的片段。
  • (inflect: true) 让 Foundation 根据运行时数量调整词形。
  • session 说明该能力在 iOS 15 和 macOS Monterey 中加入,并且当前支持部分语言。

25:45)展示数据时,不要手写列表、货币和数字格式。使用 formatter,让系统按语言和地区生成文本。

import SwiftUI

struct CheckoutTotalView: View {
    let price: Decimal = 9.41
    let genres = ["pop", "rock", "electronic"]

    var body: some View {
        VStack(spacing: 12) {
            Text(genres.formatted(.list(type: .or)))

            Text("Total: \(price, format: .currency(code: "USD"))",
                 comment: "Order subtitle: total price of all tickets")
        }
    }
}

关键点:

  • genres.formatted(.list(type: .or)) 让系统生成符合地区习惯的“或”列表。
  • price 是要展示的金额。
  • .currency(code: "USD") 指定货币代码。
  • Text("Total: ...", comment: ...) 把格式化结果放进本地化字符串插值。
  • session 提醒开发者让 framework 处理数据格式,不要硬编码。

核心启发

  • 做什么:给 App 增加“伪语言本地化”检查页面。 为什么值得做:session 要求无论本地化投入多少,都要测试 App 在各种语言下的运行效果。 怎么开始:把关键 SwiftUI 页面改成 TextButtonString(localized:),在 scheme 或 SwiftUI preview 里切换语言,检查长文本、RTL 和缺失翻译。

  • 做什么:把订单、购物车、订阅页的按钮文案拆成完整句子。 为什么值得做:Apple 明确指出拼接字符串会给翻译带来语序和词形问题。 怎么开始:搜索 + 拼接和局部文案变量,把 Order NowOrder Later 这类内容改成独立 String(localized:) 或 SwiftUI 字符串。

  • 做什么:为 App 的所有短词按钮补 comment。 为什么值得做Order 这种词在不同上下文中含义不同,comment 会随导出目录交给翻译人员。 怎么开始:从 Text("...")Button("...")String(localized:) 开始,优先处理动词、名词相同的英文词,加入位置和业务动作说明。

  • 做什么:把框架或 Swift Package 的用户可见字符串留在框架资源里。 为什么值得做:session 说明框架字符串需要指定 bundle,否则 Foundation 会从宿主 App 查找。 怎么开始:在框架中使用 String(localized:bundle:comment:),bundle 用 Bundle(for:) 定位框架资源,对外暴露 displayName 这类已本地化属性。

  • 做什么:把价格、日期、列表和票数显示改成格式化 API。 为什么值得做:Foundation 的 formatter 覆盖大量语言和地区组合,减少硬编码格式错误。 怎么开始:用 .formatted(.list(type: .or)) 处理列表,用 Text("Total: \(price, format: .currency(code: "USD"))") 处理金额,用 .stringsdictAttributedString(localized:) 处理数量词形。

关联 Session

评论

GitHub Issues · utterances