Firefox 开发者指南

本教程面向已经熟悉 Gecko 提供的先前本地化系统(DTD 和 StringBundle)的 Firefox 工程师,并假设读者具备使用这些系统的经验。

要更深入地了解 Fluent,可以尝试学习 Fluent DOMLocalization 教程,该教程提供了 Fluent 工作原理的一些背景信息,并指导您逐步创建一个使用 Fluent 进行本地化的基本 Web 项目。

在 Gecko 中使用 Fluent

Fluent 是一个引入 Gecko 平台的现代本地化系统,专注于质量、性能、维护和完整性。

旧版 DTD 系统已弃用,应尽可能使用 Fluent。

获取审阅

如果您处理任何涉及 FTL 文件的补丁,则需要获得 fluent-reviewers 的审阅。有一个 Herald 钩子会自动将该组设置为阻塞审阅者。

审阅流程指南可 在此处 获取。

为了减轻审阅者的负担,请在提交补丁以供审阅之前花点时间查看一些最佳实践。

主要优势

Fluent 紧密结合 国际化领域,通过 UnicodeCLDRICU

更具体地说,每组用户的最显著优势是

开发者

  • 支持 XUL、XHTML、HTML、Web Components、React、JS、Python 和 Rust

  • 字符串在一个统一的本地化上下文中可用,可供 DOM 和运行时代码使用

  • 完整的国际化 (i18n) 支持:日期和时间格式、数字格式、复数、性别等。

  • 强烈关注 通过 DOM 属性进行声明式 API

  • 可扩展自定义格式化程序、Mozilla 特定 API 等。

  • 关注点分离:本地化细节以及某些语言带来的额外复杂性不会泄露到源代码中,开发者无需关心。

  • 复合消息将单个翻译单元链接到单个 UI 元素

  • DOM 覆盖 允许本地化 DOM 片段

  • 简化的构建系统模型

  • 无需预处理指令

  • 支持伪本地化

产品质量

  • 强大的多级 错误回退系统 可防止 XML 错误和运行时错误

  • 简化的 l10n API 减少了 l10n 特定代码的数量以及由此产生的 bug

  • 运行时本地化允许动态更改语言并通过无线方式更新

  • DOM 覆盖提高了本地化安全性

Fluent 翻译列表 - FTL

Fluent 引入了一种专门为易读性和系统提供的本地化功能而设计的格式。

乍一看,该格式是一个简单的键值存储。它可能看起来像这样

home-page-header = Home Page

# The label of a button opening a new tab
new-tab-open = Open New Tab

但是 FTL 文件格式的功能要强大得多,并且功能很快就会增加。为了熟悉基本功能,请考虑阅读 Fluent 语法指南 以了解更复杂的示例,例如

### These messages correspond to security and privacy user interface.
###
### Please choose simple and non-threatening language when localizing
### to help user feel in control when interacting with the UI.

## General Section

-brand-short-name = Firefox
    .gender = masculine

pref-pane =
    .title =
        { PLATFORM() ->
            [windows] Options
           *[other] Preferences
        }
    .accesskey = C

# Variables:
#   $tabCount (Number) - number of container tabs to be closed
containers-disable-alert-ok-button =
    { $tabCount ->
        [one] Close { $tabCount } Container Tab
       *[other] Close { $tabCount } Container Tabs
    }

update-application-info =
    You are using { -brand-short-name } Version: { $version }.
    Please read the <a>privacy policy</a>.

当然,以上是旨在举例说明 Fluent 引入的新功能和概念的特定复杂字符串选择。

重要

虽然在 Fluent 中可以使用消息标识符中的小写和大写字符,但 Gecko 中的命名约定是使用小写字母和连字符,避免使用驼峰式大小写和下划线。例如,应优先使用 allow-button 而不是 allow_buttonallowButton,除非存在技术约束(例如从外部来源运行时生成的标识符)使得这样做不切实际。

为了确保输出质量,构建系统中包含了许多检查和工具。 Pontoon 是用于翻译 Firefox 的主要本地化工具,它也支持 Fluent 及其功能,以帮助本地化人员开展工作。

社会契约

Fluent 使用开发人员和本地化人员之间的 社会契约 概念。该契约是通过选择一个唯一的标识符(称为 l10n-id)建立的,该标识符承诺在特定位置用于传达特定含义。

唯一标识符的使用与 Firefox 中的旧版本地化系统共享。

重要

契约的一个重要部分是开发人员承诺将本地化输出视为 不透明。这意味着在翻译完成后,不应进行任何连接、替换或拆分操作来生成所需的输出。

作为回报,本地化人员通过承诺提供与请求相匹配的消息的准确且干净的翻译来参与社会契约。

在 Fluent 中,开发人员无需担心本地化将用于构建响应的内部逻辑和复杂性。是否使用 词形变化 或其他变体选择技术取决于本地化人员及其特定的翻译。从开发人员的角度来看,Fluent 返回一个要呈现给用户的最终字符串,运行代码中不需要任何 l10n 逻辑。

标记本地化

要使用 Fluent 本地化元素,开发人员需要将新消息添加到 FTL 文件中,然后通过定义 data-l10n-id 属性将 l10n-id 与元素关联

<h1 data-l10n-id="home-page-header" />

<button data-l10n-id="pref-pane" />

Fluent 将负责其余工作,在其内容和所有可本地化的属性(如果定义)中使用消息值填充元素。

开发人员仅提供一条消息来本地化整个元素,包括值和选定的属性。

该值可以是 DOM 的整个片段

<p data-l10n-id="update-application-info" data-l10n-args='{"version": "60.0"}'>
  <a data-l10n-name="privacy-url" href="http://www.mozilla.org/privacy" />
</p>
-brand-short-name = Firefox
update-application-info =
    You are using { -brand-short-name } Version: { $version }.
    Please read the <a data-l10n-name="privacy-url">privacy policy</a>.

Fluent 将翻译覆盖到源片段上,保留源中的 classhref 等属性,并为内部元素添加翻译。生成的本地化内容将如下所示

<p data-l10n-id="update-application-info" data-l10n-args='{"version": "60.0"}'">
  You are using Firefox Version: 60.0.
  Please read the <a href="http://www.mozilla.org/privacy">privacy policy</a>.
</p>

此操作已进行消毒,Fluent 会负责选择本地化可以安全提供哪些元素和属性。允许的元素和属性列表由 W3C 维护,如果开发人员需要允许本地化其他属性,则可以使用 data-l10n-attrs 列表

<label data-l10n-id="search-input" data-l10n-attrs="style" />

上面的示例添加了一个属性 style,以允许在该特定 label 元素上使用。

外部参数

请注意,在前面的示例中,属性 data-l10n-args 是一个 JSON 对象,用于存储开发人员公开给本地化人员的变量。

这是开发人员提供要用于本地化的其他变量的主要通道。

值得注意的是,当在运行时代码中设置 l10n-args 时,它们实际上会被编码为 JSON 并与 l10n-id 一起作为元素的属性存储。

运行时本地化

几乎在所有情况下,JS 运行时代码都将在特定文档(XUL、XHTML 或 HTML)上运行。

如果文档的标记已本地化,则 Fluent 会在 document 元素上公开一个新属性 - document.l10n

此属性是 DOMLocalization 类型的对象,它维护此文档的主要本地化上下文并将其公开给运行时代码。

专注于 声明式本地化,本地化的主要方法是更改 DOM 中的本地化属性。Fluent 提供了一种方法来促进此操作

document.l10n.setAttributes(element, "new-panel-header");

这将在元素上设置 data-l10n-id 并在下一个动画帧之前进行翻译。

此 API 可用于同时设置 ID 和参数。

document.l10n.setAttributes(element, "containers-disable-alert-ok-button", {
  tabCount: 5
});

如果只需要更新参数,则可以使用 setArgs 方法。

document.l10n.setArgs(element, {
  tabCount: 5
});

在调试版本中,如果未提供 Fluent 参数,则 Firefox 将崩溃。这样做是为了在 CI 中捕获这些错误。在极少数情况下,可能需要通过提供空字符串作为参数值来解决此崩溃。

非标记化本地化

在极少数情况下,当运行时代码需要检索翻译而不将其应用于 DOM 时,Fluent 提供了一个 API 来检索它。

let [ msg ] = await document.l10n.formatValues([
  {id: "remove-containers-description"}
]);

alert(msg);

强烈不建议使用此模型,仅应在 DOM 注释不可行的情况下使用。

注意

此 API 作为异步方式提供。在 Firefox 中,唯一非 DOM 可本地化的调用用于输出到第三方(如蓝牙、通知等)的情况。所有这些情况都应该已经是异步的。如果您无法避免同步访问,可以使用 mozILocalization.formatMessagesSync 进行同步 IO。

国际化

大多数国际化问题都由 Fluent 隐式处理,无需任何额外要求。完整的 Unicode 支持、双向性 和正确的数字格式化无需开发人员或本地化人员执行任何操作即可正常工作。

document.l10n.setAttributes(element, "welcome-message", {
  userName: "اليسع",
  count: 5
});

例如,本地化为美式英语的消息将正确地将用户名包装在方向标记中,允许布局引擎确定如何显示双向文本。

另一方面,本地化为阿拉伯语的相同消息将使用东阿拉伯数字表示数字“5”。

复数规则

最常见的本地化功能是能够根据复数类别提供同一字符串的不同变体。Fluent 关联到 Unicode CLDR 标准,称为 复数规则

为了允许本地化人员使用它,开发人员需要做的就是传递一个外部参数数字。

document.l10n.setAttributes(element, "unread-warning", { unreadCount: 5 });

本地化人员可以使用该参数构建多变体消息,如果他们的语言需要的话。

unread-warning =
    { $unreadCount ->
        [one] You have { $unreadCount } unread message
       *[other] You have { $unreadCount } unread messages
    }

如果变体选择是基于数字进行的,Fluent 会将该数字与文字数字及其 复数类别 进行匹配。

如果给定的翻译不需要对字符串进行复数化(例如日语通常不需要),本地化人员可以用

unread-warning = You have { $unreadCount } unread messages

替换它,并且消息将保留社交契约。

另一个功能是本地化人员可以通过为特定值指定变体来进一步改进消息。

unread-warning =
    { $unreadCount ->
        [0] You have no unread messages
        [1] You have one unread message
       *[other] You have { $unreadCount } unread messages
    }

这里的优势在于,特定于语言环境的选择不会泄露到源代码中,并且开发人员不受影响。

注意

基于复数类别 one 和数字 1 的变体之间存在重要区别。尽管在英语中两者是同义词,但在其他语言中,类别 one 可能用于其他数字。例如,在 波斯尼亚语 中,类别 one 用于像 12131 等等的数字,以及像 0.1 这样的分数。

部分格式化的变量

在格式化数据方面,Fluent 允许开发人员为格式化程序提供一组参数,并且本地化人员可以微调其中的一些参数。这种技术称为 部分格式化的变量

例如,在格式化日期时,开发人员只需传递一个 JS Date 对象,但其默认格式将非常具有表现力。在大多数情况下,开发人员可能希望使用一些 Intl.DateTimeFormat 选项来选择字符串中日期的默认表示形式。

document.l10n.formatValue("welcome-message", {
startDate: FluentDateTime(new Date(), {
    year: "numeric",
    month: "long",
    day: "numeric"
  })
});
welcome-message = Your session will start date: { $startDate }

在大多数情况下,这将足够,并且日期将在当前 Firefox 中格式化为 February 28, 2018

但是,如果在某些其他语言环境中字符串变得太长,本地化人员也可以微调选项。

welcome-message = Początek Twojej sesji: { DATETIME($startDate, month: "short") }

这将调整消息中月份标记的长度为短,并在波兰语中格式化为 28 lut 2018

目前 Fluent 支持两个与 JS Intl API 对应的格式化程序。

随着时间的推移,将添加更多格式化程序。此外,此功能目前不会公开到 setAttributes,因为这会序列化为 JSON。

注册新的 L10n 文件

Fluent 使用通配符语句,将所有本地化资源打包到其组件的 /localization/ 目录中。

这意味着,如果将新文件添加到 Firefox 的一个已由 Fluent 覆盖的组件(如 browser)中,只需将新文件添加到存储库中,路径类似于 browser/locales/en-US/browser/component/file.ftl,工具链将将其打包到 browser/localization/browser/component/file.ftl 中。

在运行时,Firefox 对所有本地化数据使用一个特殊的注册表。它将注册浏览器的 /localization/ 目录,并使其中的所有文件都可供引用。

要使文档使用 Fluent 进行本地化,开发人员需要做的就是添加 Fluent API 可使用的可本地化资源。

<link rel="localization" href="branding/brand.ftl"/>
<link rel="localization" href="browser/preferences/preferences.ftl"/>

提供给 <link/> 元素的 URI 是本地化系统中的相对路径。

自定义本地化

上述方法为每个文档创建一个单一的本地化上下文。在几乎所有情况下,这都是足够的。

在极少数情况下,开发人员需要获取其他资源或以另一种语言获取相同资源,可以使用 Localization 类手动创建其他本地化对象。

const myL10n = new Localization([
  "branding/brand.ftl",
  "browser/preferences/preferences.ftl"
]);


let [isDefaultMsg, isNotDefaultMsg] =
  await myL10n.formatValues({id: "is-default"}, {id: "is-not-default"});

示例

一个用例的示例是 Firefox 中的偏好设置 UI,它使用主上下文来本地化 UI,但也用于构建搜索索引。

通常会以当前语言和英语构建此类搜索索引,因为许多文档和在线帮助仅以英语存在。

开发人员可以手动创建一个新的上下文,其资源与主上下文相同,但将其硬编码为 en-US,然后使用这两个上下文构建搜索索引。

默认情况下,所有 Localization 上下文都是异步的。可以通过将 sync = false 参数传递给构造函数或在类上调用 SetIsSync(bool) 方法来创建一个同步上下文。

const myL10n = new Localization([
  "branding/brand.ftl",
  "browser/preferences/preferences.ftl"
], false);


let [isDefaultMsg, isNotDefaultMsg] =
  myL10n.formatValuesSync({id: "is-default"}, {id: "is-not-default"});

应始终避免使用同步上下文,因为它们需要同步 I/O。如果您认为您的用例需要同步本地化上下文,请咨询 Gecko、性能和本地化团队。

设计可本地化 API

在设计可本地化 API 时,最重要的规则是尽可能晚地解析本地化。这意味着,与其在代码库的某个深处解析字符串然后传递它们,甚至缓存它们,强烈建议传递 l10n-id[l10n-id, l10n-args] 对,直到最顶层的代码解析它们或将其应用到 DOM 元素上。

测试

在编写涉及 I18n 和 L10n 的测试时,一般规则是结果字符串是不透明的。这意味着开发人员不应该假设任何特定值,并且永远不应该针对它进行测试。

在原始 i18n 的情况下,所有 Intl.* 格式化程序上的 resolvedOptions 方法使其相对容易。在本地化的情况下,推荐的方法是测试代码是否设置了正确的 l10n-id/l10n-args 属性,如下所示。

testedFunction();

const l10nAttrs = document.l10n.getAttributes(element);

deepEquals(l10nAttrs, {
  id: "my-expected-id",
  args: {
    unreadCount: 5
  }
});

如果代码确实必须测试本地化 UI 中的特定值,最好始终扫描变量。

testedFunction();

equals(element.textContent.contains("John"));

重要

针对整个值进行测试很脆弱,当我们在结果字符串中插入 Unicode 双向标记或以其他方式调整输出时,它将中断。

使用伪本地化手动测试 UI

在使用 Fluent 支持的 UI 时,开发人员会获得一个新工具来测试其 UI 是否存在几类问题。

伪本地化是一种机制,它可以动态转换消息,使用特定逻辑来帮助模拟 UI 本地化后的外观。

这可以帮助解决三类潜在问题。

  • 硬编码字符串。

    启用伪本地化应该会显示源代码中任何硬编码的字符串,因为它们不会被转换。

  • UI 空间无法适应更长的文本。

    许多语言使用的字符串比英语更长。例如,德语字符串可能长 30%(或更多)。启用伪本地化是测试布局如何处理此类语言环境的快速方法。不适合可用空间的字符串会被截断,伪本地化也可以帮助检测它们。

  • 双向适应。

    对于许多开发人员来说,在从右到左模式下测试 UI 很难。伪本地化显示从右到左的语言环境的外观。

要启用伪本地化,请打开 浏览器工具箱,单击右上角的三个点菜单,然后选择以下选项之一。

  • 启用“带重音符号”的语言环境 - [Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ]

    此策略将所有拉丁字符替换为其带重音符号的等效字符,并复制一些元音以创建大约 30% 更长的字符串。字符串包装在标记(方括号)中,这有助于检测截断。

    此选项将 intl.l10n.pseudo 首选项设置为 accented

  • 启用双向语言环境 - ɥsıʅƃuƎ ıpıԐ

    此策略将所有拉丁字符替换为其旋转 180 度的版本,并使用 Unicode UAX#9 显式方向嵌入 强制从右到左的文本流。在此模式下,UI 的方向性也将设置为从右到左。

    此选项将 intl.l10n.pseudo 首选项设置为 bidi

测试其他语言环境

重要

对于 Firefox 工程工作,您应该优先使用伪语言环境。尤其是在 Nightly 版本中,本地化可能不完整(因为我们一直在添加/删除本地化内容),并且由于回退的工作方式而导致令人困惑的行为。

在不同的语言环境中安装 Nightly

本地化的 Nightly 版本在 mozilla.org 网站上列出

在本地版本上安装语言包

要修复仅在特定语言环境中才能重现的错误,您可能需要使用该语言环境运行开发或 nightly 版本。设置中的 UI 语言切换器在 Nightly 版本中默认禁用,因为语言包可能变得不完整并导致 UI 中出现错误——对于使用旧格式(如 .properties)的字符串,没有回退到英语。

但是,如果您确实需要使用它,您可以。

  1. 打开 about:config 并将 intl.multilingual.enabledintl.multilingual.liveReload 首选项翻转为 true

  2. 打开 langpack 的 FTP 列表 并单击与您的语言和 nightly 版本相对应的 XPI 文件(请注意,尤其是在合并日期前后,可能存在多个版本)。

    注意

    这是 Linux 列表,因为这是我们运行 l10n 作业的平台,但 XPIs 也应该在 macOS 和 Windows 上运行。唯一的例外是“特殊”的日语(适用于 mac)语言环境,它位于 mac/xpi 子目录下,而不是 latest-mozilla-central-l10n 下。(ja-JP-macja 都将在跨平台“工作”,但在某些地方使用不同的术语。)

  3. 单击提示以安装语言包。

  4. 打开 Firefox 设置 UI。

  5. 切换到您选择的语言。

在本地化版本中查找回归

您可以使用本地化版本运行 mozregression

在命令行中,如果您想在荷兰语 (nl) 版本中查找回归,您可以运行类似以下内容:

mozregression --app firefox-l10n --lang nl --good 2024-01-01

这应该运行本地化的 nightly 版本。

Fluent 的内部结构

Gecko 中 Fluent 的内部结构超出了本教程的范围,但由于类和文件名可能会在调试或分析期间显示,以下列出了主要组件,每个组件在 Gecko 的 /intl/l10n 模块中都有一个对应的文件。

有关以下某些概念的更多实践经验,请尝试遵循 Fluent DOMLocalization 教程,该教程提供了一些关于 Fluent 如何工作的背景信息,并指导您逐步创建一个从头开始使用 Fluent 进行本地化的基本 Web 项目。

FluentBundle

FluentBundle 是最低级别的 API。它是完全同步的,包含 FTL 文件格式的解析器和逻辑解析器。它不适用于直接使用。

未来,我们打算提供这一层用于标准化,它可能会成为mozIntl.*甚至Intl.* API 集的一部分。

代码库的这一部分也是我们将首先考虑移植到 Rust 的部分。

本地化

本地化是一个更高级别的 API,它在内部使用FluentBundle,但提供了一整套复合消息格式化和强大的错误回退功能。

它旨在用于运行时代码,并包含所有基本本地化方法。

DOM本地化

DOM本地化扩展了Localization,增加了直接操作 HTML、XUL 和 DOM(包括 DOM 覆盖层和 Mutation Observers)的功能。

DocumentL10n

DocumentL10n 实现 DocumentL10n WebIDL API,并允许 Document 与 DOM本地化进行通信。

事件

DOM 翻译是异步的(例如,设置data-l10n-id 属性不会立即在 DOM 中反映本地化内容)。

我们公开了一个Document.hasPendingL10nMutations成员,它反映了是否存在任何挂起的异步操作。当它们完成后,L10nMutationsFinished事件将在文档上触发,以便 Chrome 代码可以确定所有异步操作都已完成。

L10nRegistry

L10nRegistry 是我们的资源管理服务。它维护打包到构建和语言包中的资源的状态,为给定的语言环境集和Localization类使用的资源提供FluentBundle对象的异步迭代器。