i18n & l10n

在使用国际化和本地化时,经常遇到两个术语:“i18n” 和 “l10n”,它们分别是“国际化”和“本地化”的缩写。为什么?嗯,这很简单:

这两者都旨在使键入更容易,并且在存储文件目录时尤为常见。例如,您可能会看到存储在 i18n 目录中的语言文件。这是最容易的部分。稍微令人困惑的部分是这两个词之间的区别:


Internationalization 国际化

<aside> 💡 Xcode 中的一个重要设置:转到【Product】菜单,按住 Option 键,然后选择【Run】。然后转到弹窗中的【Option】选项卡,然后选中 “本地化调试 Localization Debugging ” 旁边的框【显示非本地化字符串 Show non-localized strings】。

</aside>

此选项将自动调整应用程序在模拟器中的外观,以便任何未本地化的文本都将以大写字母显示。这一开始可能会让人困惑,尤其是前面所说的“国际化和本地化”是两回事。但是,一旦完成了应用程序的国际化,其实也已经完成了本地化(因为你已经添加了原始语言)。

该选项开启后:


本地化为英文

第一步是要本地化英文 (Localizing to English) ,要从应用程序中提取英文字符串,并将它们存储在其他地方。这些将保留为英文,但一旦完成,我们的应用程序将国际化,它将准备好与我们想要添加的任何其他语言一起使用。

1. 手动创建 Localizable 文件

现在,查看本地化文件可以看到:

2. 自动创建 Localizable 文件

前期,在初始本地化中,相当一部分工作是要把相同的字符串作为键和值。也就是先把要本地化的词全部摘出来,方便后续修改。查找和提取代码中的所有字符串看似非常麻烦,所以 Xcode 附带了一个非常有用的命令行工具,使这个过程变得容易。

3. 解决自动创建的报错

在执行 genstrings 命令时,可能无法识别带有 formatter 的 Text,例如 Text(issue.formatted(date: .numeric, time: .omitted))

  1. 最简单的方法,创建计算属性:

    // 解决这类问题的一种简单方法是:将代码改成没有问题的简单的东西。把视图中的逻辑移动到视图模型中,尽量让数据和视图分离
    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)
    
  2. 更结构化的方法 - 创建扩展:

    extension Date {
        func localizedString(using formatter: DateFormatter) -> String {
            NSLocalizedString(
                formatter.string(from: self),
                comment: "Formatted date"
            )
        }
    }
    
    // 使用
    Text(project.projectCreationDate.localizedString(using: dateFormatter))
    
  3. 如果需要在多处使用相同格式的日期:

    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))
    
  4. 如果你的日期格式需要本地化:

    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)
    
  5. 完整的解决方案示例:

    // 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日"
                ))
            }
        }
    }
    
    
  6. 如果需要在 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)
        }
    }
    

4. 本地化导致的编译报错

生成本地化文本后,有时会导致编译出错,这时可以排查以下常见原因:

5. 整理 Localizable 文件

完成本地化英文后,现在的 Localizable.strings 文件存在一些问题:

后面的其他语言的本地化,都需要基于现有的 Localizable.strings 文件,所以应该先整理好该文件再创建其他语言,可按以下步骤清理:

  1. 删除所有“No comment provided by engineer”注释
  2. 重新排列字符串行,并且将相近的字符串,合理地归组在一起
  3. 在这些组上方添加注释,描述它们的使用文件位置
//示例

/* 
- 添加注释:告诉翻译人员这些字符串的使用位置。这就是分组非常有用的地方
- 完成操作后,最后一步是添加空行,在 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";

6. 语言字符压力测试

另一个有用的 Xcode 功能是:返回【Product】菜单,按住 Option,然后再次选择【Run】。在【Option】选项里,将 “App Language” 从 “System Language” 更改为 “Double-Length Pseudolanguage 双倍长度伪语言”,它在长列表靠近底部的位置。

设置完成后,再次运行该应用程序。应该看到的是,应用程序的大多数字符串都是两次编写的:例如,Filters 标题现在是 Filters Filters。此设置旨在通过生成人为的长字符串来对布局进行压力测试,其想法是,如果 UI 在长度为两倍时仍能正常工作,则应适用于任何数据。


本地化为其他语言

完成了英语本地化后,下一步是将应用程序本地化为不同的语言,不管是什么语言,其处理步骤完全相同。

处理 Localizable.strings

处理 Localizable.stringsdict

修复了 Localizable.strings 文件后,需要对 Localizable.stringsdict 文件进行同样的操作:选择该文件,在文件检查器中点击 Localize 按钮,从英语复制,然后勾选文件检查器中的其他语言。在 Localizable.stringsdict 文件上右键单击新语言版本,然后选择“在源代码中打开”。

<aside> 💡 现在应用程序大部分都本地化为其他语言了!您可以尝试一下,转到“产品”菜单,按住 Option,然后单击“运行”。现在将“应用语言”更改为“其他语言”,然后单击“运行”。这将启动应用程序,就好像用户特别要求匈牙利语一样。

</aside>

处理 JSON 等其他文件

除了本地化字符串文件,还可以本地化任何类型的资源,例如一些承载数据的 JSON 文件,实际上操作过程是完全相同的步骤。也是选择该文件,在文件检查器中点击 Localize 按钮,从英语复制,然后勾选文件检查器中的其他语言。本地化完成后,应该确认这些文件整理到一个文件夹中。


字符无法本地化的问题