Migration Recipes and Their Lifecycle

The actual migrations are performed running Python modules called migration recipes, which contain directives on how to migrate strings, which files are involved, transformations to apply, etc. These recipes are stored in mozilla-central.

When part of Firefox’s UI is migrated to Fluent, a migration recipe should be attached to the same patch that adds new strings to .ftl files.

Migration recipes can quickly become obsolete, because the referenced strings and files are removed from repositories as part of ongoing development. For these reasons, l10n-drivers periodically clean up the fluent_migrations folder in mozilla-central, keeping only recipes for 2 shipping versions (Nightly and Beta).

Hint

As a developer you don’t need to bother about updating migration recipes already in mozilla-central: if a new patch removes a string or file that is used in a migration recipe, simply ignore it, since the entire recipe will be removed within a couple of cycles.

How to Write Migration Recipes

The migration recipe’s filename should start with a reference to the associated bug number, and include a brief description of the bug, e.g. bug_1451992_preferences_applicationManager.py is the migration recipe used to migrate the Application Manager window in preferences. It’s also possible to look at existing recipes in mozilla-central for inspiration.

General Recipe Structure

A migration recipe is a Python module, implementing the migrate() function, which takes a MigrationContext as input. The API provided by the context is

class MigrationContext:
    def add_transforms(self, target, reference, transforms):
        """Define transforms for target using reference as template.

        `target` is a path of the destination FTL file relative to the
        localization directory. `reference` is a path to the template FTL
        file relative to the reference directory.

        Each transform is an extended FTL node with `Transform` nodes as some
        values.

        For transforms that merely copy legacy messages or Fluent patterns,
        using `fluent.migrate.helpers.transforms_from` is recommended.
        """

The skeleton of a migration recipe just implements the migrate() function calling into ctx.add_transforms(), and looks like

# coding=utf8

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

from __future__ import absolute_import


def migrate(ctx):
    """Bug 1552333 - Migrate feature to Fluent, part {index}"""
    target = 'browser/browser/feature.ftl'
    reference = 'browser/browser/feature.ftl'
    ctx.add_transforms(
        target,
        reference,
        [],  # Actual transforms go here.
    )

One can call into ctx.add_transforms() multiple times. In particular, one can create migrated content in multiple files as part of a single migration recipe by calling ctx.add_transforms() with different target-reference pairs.

The docstring for this function will be used as a commit message in VCS, that’s why it’s important to make sure the bug reference is correct, and to keep the part {index} section: multiple strings could have multiple authors, and would be migrated in distinct commits (part 1, part 2, etc.).

Transforms

The work of the migrations is done by the transforms that are passed as last argument to ctx.add_transforms(). They’re instances of either Fluent fluent.syntax.ast.Message or Term, and their content can depend on existing translation sources. The skeleton of a Message looks like

FTL.Message(
    id=FTL.Identifier(
        name="msg",
    ),
    value=FTL.Pattern(
        elements=[
            FTL.TextElement(
                value="A string",
            ),
        ],
    ),
)

When migrating existing legacy translations, you’ll replace an FTL.TextElement with a COPY(legacy_path, "old_id"), or one of its variations we detail next. When migrating existing Fluent translations, an FTL.Pattern is replaced with a COPY_PATTERN(old_path, "old-id").