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 的 Text、Button 默认会把字符串作为本地化内容。普通 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 的第一部分写出字符串出现的位置,例如
Button或Order 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:28)AttributedString(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 用俄语举例说明,
.stringsdict的one规则不等于数字只为 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 页面改成
Text、Button、String(localized:),在 scheme 或 SwiftUI preview 里切换语言,检查长文本、RTL 和缺失翻译。 -
做什么:把订单、购物车、订阅页的按钮文案拆成完整句子。 为什么值得做:Apple 明确指出拼接字符串会给翻译带来语序和词形问题。 怎么开始:搜索
+拼接和局部文案变量,把Order Now、Order 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"))")处理金额,用.stringsdict或AttributedString(localized:)处理数量词形。
关联 Session
- Localize your SwiftUI app — 从 SwiftUI 视角讲字符串、样式文本、图片和布局的本地化流程。
- What’s new in Foundation — 深入介绍
AttributedString、格式化 API 和语法一致引擎。 - What’s new in SwiftUI — 覆盖 SwiftUI 2021 的文本、格式化、搜索、列表和跨平台 UI 更新。
- SwiftUI Accessibility: Beyond the basics — 用预览和辅助功能 API 检查复杂界面,适合和多语言测试一起做。
评论
GitHub Issues · utterances