在使用国际化和本地化时,经常遇到两个术语:“i18n” 和 “l10n”,它们分别是“国际化”和“本地化”的缩写。为什么?嗯,这很简单:
这两者都旨在使键入更容易,并且在存储文件目录时尤为常见。例如,您可能会看到存储在 i18n 目录中的语言文件。这是最容易的部分。稍微令人困惑的部分是这两个词之间的区别:
<aside> 💡 Xcode 中的一个重要设置:转到【Product】菜单,按住 Option 键,然后选择【Run】。然后转到弹窗中的【Option】选项卡,然后选中 “本地化调试 Localization Debugging ” 旁边的框【显示非本地化字符串 Show non-localized strings】。
</aside>
此选项将自动调整应用程序在模拟器中的外观,以便任何未本地化的文本都将以大写字母显示。这一开始可能会让人困惑,尤其是前面所说的“国际化和本地化”是两回事。但是,一旦完成了应用程序的国际化,其实也已经完成了本地化(因为你已经添加了原始语言)。
该选项开启后:
第一步是要本地化英文 (Localizing to English) ,要从应用程序中提取英文字符串,并将它们存储在其他地方。这些将保留为英文,但一旦完成,我们的应用程序将国际化,它将准备好与我们想要添加的任何其他语言一起使用。
Command + N
创建新文件,从选项列表中选择 “Strings File
” ,将其命名为 Localizable.strings
,Xcode 将用它来进行本地化"Filters" = "Fish";
添加到文件末尾;这时再次运行程序,就可以看到应用中的 Filters
已替换为 "Fish"
Localizable.strings
文件中编写 Filters
时,Swift 其实是将其解释为 LocalizedStringKey
对象,然后输入到 SwiftUI 中,后者会在文件中查找它后面的实际值。之后每次您在代码中输入新的字符串,其实 SwiftUI 都会自动尝试在 Localizable.strings
中查找它现在,查看本地化文件可以看到:
Filters
”,这是我们在 SwiftUI 代码中使用的字符串名称Fish
” ,选择了一个特别的词只是为了看起来效果明显前期,在初始本地化中,相当一部分工作是要把相同的字符串作为键和值。也就是先把要本地化的词全部摘出来,方便后续修改。查找和提取代码中的所有字符串看似非常麻烦,所以 Xcode 附带了一个非常有用的命令行工具,使这个过程变得容易。
启动终端应用程序,并切换到 Swift 代码目录,例如 cd ~/Desktop/UltimatePortfolio/UltimatePortfolio
运行 Xcode 的 genstrings 命令:genstrings -SwiftUI *.swift
,在代码中查找和提取字符串;虽然它只会跟踪存储在 Text
视图中的文本,但也算是给了一个良好的开端
该命令只能处理当前文件夹下的 swift 后缀文件,子文件夹内的文件无法被找到。如果想一次性处理所有子文件夹里的文件,可以输入:
// 这个命令会递归地查找当前目录及其子目录中的所有 Swift 文件,并对每个文件执行 genstrings -SwiftUI 命令
// 需要注意的是,如果你的文件名或路径中包含空格或其他特殊字符,使用 find 和 xargs 的组合命令会更加安全可靠
find . -name "*.swift" -print0 | xargs -0 genstrings -SwiftUI
// find . -name "*.swift" 会在当前目录及其子目录中查找所有扩展名为 .swift 的文件
// print0 可以使命令在输出每个文件名时使用 null 字符 (\\0) 作为分隔符,而不是默认的换行符。这可以避免文件名中包含空格等特殊字符时的问题
// | xargs -0 会将 find 命令的输出作为 `xargs` 命令的参数。`0` 选项告诉 `xargs` 使用 null 字符作为参数分隔符
// genstrings -SwiftUI 是你想要执行的命令。`xargs` 会将 `find` 命令找到的所有 Swift 文件作为参数传递给 `genstrings` 命令
如果终端提示 “xcode-select: error” 出错,可以运行以下指令进行修复
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
genstrings
正常运行后,它将自动覆盖现有的 Localizable.strings
文件。打开 Localizable.strings
,会发现里面有各种各样的字符串,键和值总是相同的,因为 genstrings
不能翻译东西。另请注意,它在每个字符串之前都放置了一个注释,这是您描述如何使用密钥来帮助翻译人员提供足够信息以进行良好翻译的机会。更重要的是,我们的 Swift 代码没有变化:我们的 Swift 代码中仍然有 “Filters”、“Issues” 等,作为我们应用程序的本地化字符串键。
最后,尽管用了 genstrings
,还有相当多的 string
由于某种原因没有被添加。需要人为将遗漏的添加到 Localizable.strings
中
在执行 genstrings
命令时,可能无法识别带有 formatter 的 Text,例如 Text(issue.formatted(date: .numeric, time: .omitted))
最简单的方法,创建计算属性:
// 解决这类问题的一种简单方法是:将代码改成没有问题的简单的东西。把视图中的逻辑移动到视图模型中,尽量让数据和视图分离
var issueFormattedCreationDate: String {
issueCreationDate.formatted(date: .numeric, time: .omitted)
}
// 挪过去以后,视图中的代码就比较简单了
Text(issue.issueFormattedCreationDate)
var formattedDate: String {
NSLocalizedString(
dateFormatter.string(from: project.projectCreationDate),
comment: "Project creation date"
)
}
// 使用
Text(formattedDate)
更结构化的方法 - 创建扩展:
extension Date {
func localizedString(using formatter: DateFormatter) -> String {
NSLocalizedString(
formatter.string(from: self),
comment: "Formatted date"
)
}
}
// 使用
Text(project.projectCreationDate.localizedString(using: dateFormatter))
如果需要在多处使用相同格式的日期:
struct LocalizedDateFormat {
static func format(_ date: Date, style: DateFormatter.Style = .medium) -> String {
let formatter = DateFormatter()
formatter.dateStyle = style
return NSLocalizedString(
formatter.string(from: date),
comment: "Date formatted with \\\\(style) style"
)
}
}
// 使用
Text(LocalizedDateFormat.format(project.projectCreationDate))
如果你的日期格式需要本地化:
extension DateFormatter {
static var localizedProjectDate: DateFormatter = {
let formatter = DateFormatter()
// 设置日期格式的本地化键
formatter.dateFormat = NSLocalizedString(
"yyyy-MM-dd",
comment: "Project date format"
)
return formatter
}()
}
// 使用
Text(project.projectCreationDate, formatter: .localizedProjectDate)
完整的解决方案示例:
// DateFormatting.swift
enum DateFormatting {
// 日期格式化器
static let projectDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
// 格式化方法
static func localizedProjectDate(_ date: Date) -> String {
NSLocalizedString(
projectDateFormatter.string(from: date),
comment: "Project creation date format"
)
}
// 带有自定义格式的方法
static func localizedCustomDate(_ date: Date, format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = NSLocalizedString(
format,
comment: "Custom date format"
)
return NSLocalizedString(
formatter.string(from: date),
comment: "Formatted date with custom format"
)
}
}
// 在视图中使用
struct ProjectView: View {
let project: Project
var body: some View {
VStack {
// 基本使用
Text(DateFormatting.localizedProjectDate(project.projectCreationDate))
// 自定义格式使用
Text(DateFormatting.localizedCustomDate(
project.projectCreationDate,
format: "yyyy年MM月dd日"
))
}
}
}
如果需要在 Localizable.strings 中定义不同的日期格式:
// Localizable.strings (English)
"DATE_FORMAT" = "MMM d, yyyy";
"PROJECT_DATE_%@" = "Created on %@";
// Localizable.strings (Chinese)
"DATE_FORMAT" = "yyyy年MM月dd日";
"PROJECT_DATE_%@" = "创建于 %@";
// 在代码中使用
struct ProjectDateView: View {
let date: Date
var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = NSLocalizedString("DATE_FORMAT", comment: "Date format")
let dateString = formatter.string(from: date)
return String(format: NSLocalizedString("PROJECT_DATE_%@", comment: "Project creation date"), dateString)
}
var body: some View {
Text(formattedDate)
}
}
生成本地化文本后,有时会导致编译出错,这时可以排查以下常见原因:
检查文件是否正确添加到 Target:选中 Localizable.strings 文件,在右侧 File Inspector 中确认该文件已添加到正确的 Target membership
检查文件位置和引用:确保 Localizable.strings 文件实际存在于项目目录中,确认文件路径是否正确
检查 Build Phases:选择项目 target,进入 Build Phases,检查 Copy Bundle Resources
中是否有重复的 Localizable.strings 文件。有些我们在不同文件夹下调用过前面的终端指令,就会产生多个文件。如果有重复,删除其中一个
如果 Localizable.strings
文件成功生成后,运行 Xcode 项目时提示报错。可能是提示编码不正确,可以选中文件后,在 xcode 的侧边栏 Text Encoding 选项中进行修改。当切换到 UTF-16 时,Xcode 会询问您是要 Convert
还是 Reinterpret
。老实说不知道,我尝试了 Reinterpret
到目前为止它运行良好。
清理项目和构建文件夹:Command + Shift + K 和 Command + Option + Shift + K
尝试以下操作:关闭 Xcode,输入以下命令删除 DerivedData 文件夹,再重新打开 Xcode 并构建项目
rm -rf ~/Library/Developer/Xcode/DerivedData
完成本地化英文后,现在的 Localizable.strings
文件存在一些问题:
genstrings
字母顺序出现,这几乎可以肯定没有意义后面的其他语言的本地化,都需要基于现有的 Localizable.strings
文件,所以应该先整理好该文件再创建其他语言,可按以下步骤清理:
SidebarView
、ContentView
、DetailView
、IssueView
…//示例
/*
- 添加注释:告诉翻译人员这些字符串的使用位置。这就是分组非常有用的地方
- 完成操作后,最后一步是添加空行,在 Localizable.strings 文件中添加空行,以帮助直观地分解不同的部分
- 一般会在每个主要组(例如视图)之间放置三个换行符,然后在每个次要组之间放置一个换行符,例如上面的小过滤器菜单组
- 字符串文件将一直被人类读取,所以要让它可读——添加换行符、添加注释、用大写字母写东西来制作标题等等
- 如果有帮助,您甚至可以使用 ASCII 艺术!
*/
/* CONTENTVIEW */
"Issues" = "Issues";
/* A button that shows a menu with the following options */
"Filter" = "Filter";
"Turn Filter On" = "Turn Filter On"; /* Enables filtering */
"Turn Filter Off" = "Turn Filter Off"; /* Disables filtering */
"Sort By" = "Sort By"; /* Adjust the order issues are shown */
"Date Created" = "Date Created"; /* Sort issues by the date they were created */
"Date Modified" = "Date Modified"; /* Sort issues by the date they were last changed */
"Newest to Oldest" = "Newest to Oldest"; /* Place newer issues before older issues */
"Oldest to Newest" = "Oldest to Newest"; /* Place older issues before newer issues */
"All" = "All";
"Open" = "Open";
"Closed" = "Closed";
另一个有用的 Xcode 功能是:返回【Product】菜单,按住 Option,然后再次选择【Run】。在【Option】选项里,将 “App Language” 从 “System Language” 更改为 “Double-Length Pseudolanguage 双倍长度伪语言”,它在长列表靠近底部的位置。
设置完成后,再次运行该应用程序。应该看到的是,应用程序的大多数字符串都是两次编写的:例如,Filters 标题现在是 Filters Filters。此设置旨在通过生成人为的长字符串来对布局进行压力测试,其想法是,如果 UI 在长度为两倍时仍能正常工作,则应适用于任何数据。
完成了英语本地化后,下一步是将应用程序本地化为不同的语言,不管是什么语言,其处理步骤完全相同。
hu.lproj
目录中修复了 Localizable.strings 文件后,需要对 Localizable.stringsdict
文件进行同样的操作:选择该文件,在文件检查器中点击 Localize
按钮,从英语复制,然后勾选文件检查器中的其他语言。在 Localizable.stringsdict
文件上右键单击新语言版本,然后选择“在源代码中打开”。
<aside> 💡 现在应用程序大部分都本地化为其他语言了!您可以尝试一下,转到“产品”菜单,按住 Option,然后单击“运行”。现在将“应用语言”更改为“其他语言”,然后单击“运行”。这将启动应用程序,就好像用户特别要求匈牙利语一样。
</aside>
除了本地化字符串文件,还可以本地化任何类型的资源,例如一些承载数据的 JSON 文件,实际上操作过程是完全相同的步骤。也是选择该文件,在文件检查器中点击 Localize
按钮,从英语复制,然后勾选文件检查器中的其他语言。本地化完成后,应该确认这些文件整理到一个文件夹中。