遗留格式迁移

从遗留格式(.dtd、.properties)迁移不同于 Fluent 到 Fluent 的迁移。在迁移遗留代码路径时,您需要根据 Mozilla 在遗留代码路径中使用的特殊情况调整 Fluent 字符串。您将在此处找到许多专门的功能。

遗留迁移工具

为了帮助进行遗留格式迁移,提供了一些脚本工具

创建迁移时,其中一个或两个工具可以通过自动化迁移的至少一部分(包括方案生成和重构调用代码)为手动工作提供良好的起点。

基本迁移

让我们考虑一个基本的示例:一个字符串需要从 DTD 文件迁移到 Fluent,无需任何其他更改。

遗留字符串存储在 toolkit/locales/en-US/chrome/global/findbar.dtd

<!ENTITY next.tooltip "Find the next occurrence of the phrase">

新的 Fluent 字符串存储在 toolkit/locales/en-US/toolkit/main-window/findbar.ftl

findbar-next =
    .tooltiptext = Find the next occurrence of the phrase

迁移方案如下所示

# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/

from __future__ import absolute_import
import fluent.syntax.ast as FTL
from fluent.migrate.helpers import transforms_from

def migrate(ctx):
    """Bug 1411707 - Migrate the findbar XBL binding to a Custom Element, part {index}."""

    ctx.add_transforms(
        "toolkit/toolkit/main-window/findbar.ftl",
        "toolkit/toolkit/main-window/findbar.ftl",
        transforms_from(
"""
findbar-next =
    .tooltiptext = { COPY(from_path, "next.tooltip") }
""", from_path="toolkit/chrome/global/findbar.dtd"))

首先要注意的是,迁移方案需要相对于本地化存储库的文件路径,丢失 locales/en-US/

  • toolkit/locales/en-US/chrome/global/findbar.dtd 变成 toolkit/chrome/global/findbar.dtd

  • toolkit/locales/en-US/toolkit/main-window/findbar.ftl 变成 toolkit/toolkit/main-window/findbar.ftl

context.add_transforms 函数接受 3 个参数

  • 目标 l10n 文件的路径。

  • 参考(en-US)文件的路径。

  • 转换数组。转换是 AST 节点,描述了如何迁移遗留翻译。

注意

对于 Firefox 本地化的迁移,目标路径和参考路径相同。这对于所有使用 Fluent 的项目并不适用,因此需要这两个参数。

在本例中,只有一个转换将 ID 为 next.tooltip 的字符串从 toolkit/chrome/global/findbar.dtd 迁移,并将其注入 FTL 片段中。 COPY 转换允许按原样从现有文件复制字符串,而 from_path 用于避免多次重复相同的路径,使方案更具可读性。如果没有 from_path,则可以写成

ctx.add_transforms(
    "toolkit/toolkit/main-window/findbar.ftl",
    "toolkit/toolkit/main-window/findbar.ftl",
    transforms_from(
"""
findbar-next =
    .tooltiptext = { COPY("toolkit/chrome/global/findbar.dtd", "next.tooltip") }
"""))

这种编写迁移方案的方法允许获取原始 FTL 字符串,并简单地用 COPY 转换替换每个消息的值。 transforms_from 负责将 FTL 语法转换为描述如何迁移遗留翻译的转换数组。这种定义迁移的方式仅适用于复制操作足够简单的简单字符串。对于需要在 Python 中使用一些额外逻辑的更复杂用例,有必要使用原始 AST。

上面的示例等效于以下语法,它公开了底层的 AST 结构

ctx.add_transforms(
    "toolkit/toolkit/main-window/findbar.ftl",
    "toolkit/toolkit/main-window/findbar.ftl",
    [
        FTL.Message(
            id=FTL.Identifier("findbar-next"),
            attributes=[
                FTL.Attribute(
                    id=FTL.Identifier("tooltiptext"),
                    value=COPY(
                        "toolkit/chrome/global/findbar.dtd",
                        "next.tooltip"
                    )
                )
            ]
        )
    ]
)

这会创建一个 Message,从遗留字符串 findbar-next 中获取值。消息可以有一个属性数组,每个属性都有一个 ID 和一个值:在本例中,只有一个属性,ID 为 tooltiptextvalue 从遗留字符串复制。

请注意,消息的 ID 和属性的 ID 如何都定义为 FTL.Identifier,而不仅仅是字符串。

提示

可以使用 + 运算符将手动定义的转换数组(如最后一个示例中)与来自 transforms_from 的转换数组连接起来。或者,可以使用多个 add_transforms

方案中提供的转换顺序无关紧要,参考文件用于对消息进行排序。

替换遗留字符串中的内容

虽然 COPY 允许按原样复制遗留字符串,但 REPLACE(来自 fluent.migrate)允许在执行迁移时替换内容。例如,当迁移包含需要替换以适应 Fluent 语法的占位符或实体的字符串时,这很有必要。

例如,考虑以下字符串

<!ENTITY aboutSupport.featuresTitle "&brandShortName; Features">

需要迁移到

features-title = { -brand-short-name } Features

实体 &brandShortName; 需要替换为术语引用

FTL.Message(
    id=FTL.Identifier("features-title"),
    value=REPLACE(
        "toolkit/chrome/global/aboutSupport.dtd",
        "aboutSupport.featuresTitle",
        {
            "&brandShortName;": TERM_REFERENCE("brand-short-name"),
        },
    )
),

这会创建一个 FTL.Message,从遗留字符串 aboutSupport.featuresTitle 中获取值,但用 Fluent 术语引用替换指定的文本。

注意

REPLACE 替换指定文本的所有出现。

也可以用特定文本替换内容:在这种情况下,需要将其定义为 TextElement。例如,要将 example.com 替换为 HTML 标记

value=REPLACE(
    "browser/chrome/browser/preferences/preferences.properties",
    "searchResults.sorryMessageWin",
    {
        "example.com": FTL.TextElement('<span data-l10n-name="example"></span>')
    }
)

当迁移方案需要替换 printf 参数(如 %S)时,情况会更加复杂。实际上,本地化字符串和源字符串使用的格式不需要匹配,以下两个使用无序和有序参数的字符串完全等效

btn-quit = Quit %S
btn-quit = Quit %1$S

在这种情况下,替换 %S 将在第一个版本上起作用,但在第二个版本上不起作用,并且不能保证本地化字符串使用与源字符串相同的格式。

还要考虑以下使用 %S 表示两个不同变量的字符串,隐式依赖于参数出现的顺序

updateFullName = %S (%S)

以及目标 Fluent 字符串

update-full-name = { $name } ({ $buildID })

如上所述, REPLACE 将替换 %S 的所有出现,因此只能设置一个变量。字符串需要被规范化并像这样处理

updateFullName = %1$S (%2$S)

这可以通过调用 REPLACE 并设置 normalize_printf=True 来实现

FTL.Message(
    id=FTL.Identifier("update-full-name"),
    value=REPLACE(
        "toolkit/chrome/mozapps/update/updates.properties",
        "updateFullName",
        {
            "%1$S": VARIABLE_REFERENCE("name"),
            "%2$S": VARIABLE_REFERENCE("buildID"),
        },
        normalize_printf=True
    )
)

注意

为了避免任何问题,在替换 printf 参数时,应始终使用 normalize_printf=True。这是处理 .properties 文件时的默认行为。

注意

VARIABLE_REFERENCEMESSAGE_REFERENCETERM_REFERENCE 是可以在常见情况下使用的辅助转换,在这些情况下,使用原始 AST 太冗长。

VARIABLE_REFERENCE 用于创建对变量的引用,例如 { $variable }

MESSAGE_REFERENCE 用于创建对另一个消息的引用,例如 { another-string }

TERM_REFERENCE 用于创建对 术语 的引用,例如 { -brand-short-name }

这两个转换都需要在方案开头导入,例如 from fluent.migrate.helpers import VARIABLE_REFERENCE

修剪翻译中不必要的空格

注意

本节已于 2020 年 5 月更新,以反映对默认行为的更改:现在会修剪遗留翻译,除非显式设置 trim 参数。

在遗留翻译中,字符串包含不必要的前导或尾随空格的情况并不少见。这些没有意义,对字符串在产品中显示方式没有实际影响,并且主要出于格式原因而添加。例如,考虑以下 DTD 字符串

<!ENTITY aboutAbout.note   "This is a list of “about” pages for your convenience.<br/>
                            Some of them might be confusing. Some are for diagnostic purposes only.<br/>
                            And some are omitted because they require query strings.">

默认情况下, COPYREPLACEPLURALS 转换将从翻译的每一行的开头和结尾去除空格,以及开头和结尾的空行。尽管原始字符串的第二行和第三行有大量的缩进,但上述字符串将迁移为以下 Fluent 消息:

about-about-note =
    This is a list of “about” pages for your convenience.<br/>
    Some of them might be confusing. Some are for diagnostic purposes only.<br/>
    And some are omitted because they require query strings.

要禁用默认的修剪行为,请根据上下文设置 trim:"False"trim=False

transforms_from(
"""
about-about-note = { COPY("toolkit/chrome/global/aboutAbout.dtd", "aboutAbout.note", trim:"False") }
""")

FTL.Message(
    id=FTL.Identifier("discover-description"),
    value=REPLACE(
        "toolkit/chrome/mozapps/extensions/extensions.dtd",
        "discover.description2",
        {
            "&brandShortName;": TERM_REFERENCE("-brand-short-name")
        },
        trim=False
    )
),

字符串拼接

最佳实践是仅将完整的短语暴露给本地化,并避免在代码中将本地化的字符串拼接在一起。使用 DTDproperties 时,选项很少。因此,在迁移到 Fluent 时,您会发现拼接来自 DTDproperties 的多个字符串的情况非常普遍,例如创建带有 HTML 标记的句子。可以使用 CONCAT 变换在迁移方案中拼接字符串和文本元素。

请注意,在使用 transforms_from 进行简单迁移的情况下,拼接是通过使用与 Fluent 语法交织的 COPY() 变换调用来定义迁移方案隐式执行的。

考虑以下示例

# %S is replaced by a link, using searchResults.needHelpSupportLink as text
searchResults.needHelp = Need help? Visit %S

# %S is replaced by "Firefox"
searchResults.needHelpSupportLink = %S Support

在 Fluent 中

search-results-need-help-support-link = Need help? Visit <a data-l10n-name="url">{ -brand-short-name } Support</a>

这是一个相当复杂的迁移:它需要获取 2 个旧字符串,并将它们的值与 HTML 标记拼接起来。以下是变换的定义方式

FTL.Message(
    id=FTL.Identifier("search-results-help-link"),
    value=REPLACE(
        "browser/chrome/browser/preferences/preferences.properties",
        "searchResults.needHelp",
        {
            "%S": CONCAT(
                FTL.TextElement('<a data-l10n-name="url">'),
                REPLACE(
                    "browser/chrome/browser/preferences/preferences.properties",
                    "searchResults.needHelpSupportLink",
                    {
                        "%1$S": TERM_REFERENCE("brand-short-name"),
                    },
                    normalize_printf=True
                ),
                FTL.TextElement("</a>")
            )
        }
    )
),

%SsearchResults.needHelpSupportLink 中被替换为对术语 -brand-short-name 的引用,从 %S Support 迁移到 { -brand-short-name } Support。然后将此操作的结果插入两个文本元素之间以创建锚标记。最终使用生成的文本替换 %SsearchResults.needHelp 中,并用作 FTL 消息的值。

重要

拼接现有字符串时,避免对原始文本进行更改,例如添加空格或标点符号。每种语言都有自己的规则,这可能会导致迁移的字符串质量低下。如有疑问,请务必征求反馈。

当传入多个元素进行拼接时,CONCAT 会禁用上述部分中描述的所有传入的旧变换(COPYREPLACEPLURALS)的空格修剪,除非在这些变换上显式设置了 trim 参数。这有助于确保在拼接过程中不会丢失段落周围的空格。

但是,当仅将单个元素传入 CONCAT 时,修剪行为不会改变,并遵循上一节中描述的规则。这样做的目的是使 CONCAT(COPY()) 等效于裸 COPY()

复数字符串

.properties 文件迁移复数字符串通常涉及来自 fluent.migrate.transforms 的两个变换:REPLACE_IN_TEXT 变换将 TextElements 作为输入,使其可以作为 PLURALS 变换的 foreach 函数传递。

考虑以下旧字符串

# LOCALIZATION NOTE (disableContainersOkButton): Semi-colon list of plural forms.
# See: https://mdn.org.cn/en/docs/Localization_and_Plurals
# #1 is the number of container tabs
disableContainersOkButton = Close #1 Container Tab;Close #1 Container Tabs

在 Fluent 中

containers-disable-alert-ok-button =
    { $tabCount ->
        [one] Close { $tabCount } Container Tab
       *[other] Close { $tabCount } Container Tabs
    }

以下是此字符串变换的定义方式

FTL.Message(
    id=FTL.Identifier("containers-disable-alert-ok-button"),
    value=PLURALS(
        "browser/chrome/browser/preferences/preferences.properties",
        "disableContainersOkButton",
        VARIABLE_REFERENCE("tabCount"),
        lambda text: REPLACE_IN_TEXT(
            text,
            {
                "#1": VARIABLE_REFERENCE("tabCount")
            }
        )
    )
)

PLURALS 变换将负责为每种语言创建正确的复数类别数量。请注意,如何使用 REPLACE_IN_TEXTVARIABLE_REFERENCE("tabCount") 将每个变体的 #1 替换为 { $tabCount }

在这种情况下,无法使用 REPLACE,因为它将文件路径和消息 ID 作为参数,而此处方案需要对常规文本进行操作。替换是在原始字符串的每个复数形式上执行的,其中复数形式以分号分隔。

显式变体

显式创建字符串的变体对于平台相关的术语很有用,但也适用于您希望将字符串进行一对多拆分的情况。始终可以通过手动创建底层 AST 结构来迁移字符串。考虑以下复杂的 Fluent 字符串

use-current-pages =
    .label =
        { $tabCount ->
            [1] Use Current Page
           *[other] Use Current Pages
        }
    .accesskey = C

此字符串的迁移非常复杂:label 属性由 2 个不同的旧字符串创建,它不是正确的复数形式。请注意,第一个字符串与 1 案例关联,而不是复数形式中使用的 one 类别。由于这些原因,无法使用 PLURALS,需要制作变换以重新创建 AST。

FTL.Message(
    id=FTL.Identifier("use-current-pages"),
    attributes=[
        FTL.Attribute(
            id=FTL.Identifier("label"),
            value=FTL.Pattern(
                elements=[
                    FTL.Placeable(
                        expression=FTL.SelectExpression(
                            selector=VARIABLE_REFERENCE("tabCount"),
                            variants=[
                                FTL.Variant(
                                    key=FTL.NumberLiteral("1"),
                                    default=False,
                                    value=COPY(
                                        "browser/chrome/browser/preferences/main.dtd",
                                        "useCurrentPage.label",
                                    )
                                ),
                                FTL.Variant(
                                    key=FTL.Identifier("other"),
                                    default=True,
                                    value=COPY(
                                        "browser/chrome/browser/preferences/main.dtd",
                                        "useMultiple.label",
                                    )
                                )
                            ]
                        )
                    )
                ]
            )
        ),
        FTL.Attribute(
            id=FTL.Identifier("accesskey"),
            value=COPY(
                "browser/chrome/browser/preferences/main.dtd",
                "useCurrentPage.accesskey",
            )
        ),
    ],
),

此变换使用了本文档中已经描述的一些概念。值得注意的是 SelectExpressionPlaceable 内,带有 Variant 对象数组。这些变体中只有一个需要具有 default=True

此示例仍然可以使用 transforms_from()`(),因为现有字符串在没有插值的情况下被复制。

transforms_from(
"""
use-current-pages =
    .label =
        { $tabCount ->
            [1] { COPY(main_dtd, "useCurrentPage.label") }
           *[other] { COPY(main_dtd, "useMultiple.label") }
        }
    .accesskey = { COPY(main_dtd, "useCurrentPage.accesskey") }
""", main_dtd="browser/chrome/browser/preferences/main.dtd"
)