summaryrefslogtreecommitdiffstats
path: root/python/l10n
diff options
context:
space:
mode:
Diffstat (limited to 'python/l10n')
-rw-r--r--python/l10n/convert_xul_to_fluent/convert.py118
-rw-r--r--python/l10n/convert_xul_to_fluent/lib/__init__.py7
-rw-r--r--python/l10n/convert_xul_to_fluent/lib/dtd.py28
-rw-r--r--python/l10n/convert_xul_to_fluent/lib/fluent.py34
-rw-r--r--python/l10n/convert_xul_to_fluent/lib/migration.py77
-rw-r--r--python/l10n/convert_xul_to_fluent/lib/utils.py23
-rw-r--r--python/l10n/convert_xul_to_fluent/lib/xul.py117
-rw-r--r--python/l10n/fluent_migrations/__init__.py0
-rw-r--r--python/l10n/fluent_migrations/bug_1552333_aboutCertError.py41
-rw-r--r--python/l10n/fluent_migrations/bug_1565574_protocol_handler_dialog.py37
-rw-r--r--python/l10n/fluent_migrations/bug_1568133_menubar.py141
-rw-r--r--python/l10n/fluent_migrations/bug_1634042_page_action_menu.py112
-rw-r--r--python/l10n/fluent_migrations/bug_1658629_migration_urlbar_actions.py60
-rw-r--r--python/l10n/fluent_migrations/bug_1667781_preferences_dialogs.py102
-rw-r--r--python/l10n/fluent_migrations/bug_1668284_settings_change.py66
-rw-r--r--python/l10n/fluent_migrations/bug_1682022_bookmarks_and_menubar_strings.py83
-rw-r--r--python/l10n/fluent_migrations/bug_1682022_context_menu_and_browser_strings.py38
-rw-r--r--python/l10n/fluent_migrations/bug_1683419_forking_help_menu.py55
-rw-r--r--python/l10n/fluent_migrations/bug_1686331_library_recent_activity.py23
-rw-r--r--python/l10n/test_fluent_migrations/__init__.py0
-rw-r--r--python/l10n/test_fluent_migrations/fmt.py190
21 files changed, 1352 insertions, 0 deletions
diff --git a/python/l10n/convert_xul_to_fluent/convert.py b/python/l10n/convert_xul_to_fluent/convert.py
new file mode 100644
index 0000000000..5185b3d5de
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/convert.py
@@ -0,0 +1,118 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+from __future__ import print_function
+from lib.xul import collect_messages
+from lib.dtd import get_dtds
+from lib.utils import read_file, write_file
+from lib.migration import build_migration
+from lib.fluent import build_ftl
+import json
+import argparse
+import sys
+
+# import re
+
+# To run this, you'll need to have lxml installed:
+# `pip install lxml`
+
+# default migration directions
+data = {
+ "migration": "python/l10n/fluent_migrations",
+ "description": "Migrate l10n strings",
+ "prefix": "",
+}
+
+
+def parse_inputs():
+ sys_args = sys.argv[1:]
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("bug_id", type=str, help="Id number for bug tracking")
+ parser.add_argument(
+ "xul", type=str, help="POSIX path from mozilla-central to the XML to be updated"
+ )
+ parser.add_argument(
+ "ftl",
+ type=str,
+ help="Case sensitive POSIX path from mozilla-central to desired ftl file",
+ )
+ parser.add_argument(
+ "mozilla_central",
+ type=str,
+ help="Case sensitive absolute POSIX path to current mozilla-central repo",
+ )
+ parser.add_argument(
+ "dtd",
+ type=str,
+ help="comma delimited list of case sensitive POSIX dtd file paths",
+ )
+ parser.add_argument("description", type=str, help="string enclosed in quotes")
+ parser.add_argument(
+ "--dry-run", action="store_true", help="Tell if running dry run or not"
+ )
+ parser.add_argument(
+ "--prefix", type=str, help="a keyword string to be added to all l10n ids"
+ )
+
+ parsed_args = parser.parse_args(sys_args)
+
+ data["description"] = parsed_args.description
+ data["bug_id"] = parsed_args.bug_id
+ data["xul"] = parsed_args.xul
+ data["ftl"] = parsed_args.ftl
+ data["mozilla-central"] = parsed_args.mozilla_central
+ data["dtd"] = parsed_args.dtd.split(",")
+ data["dry-run"] = parsed_args.dry_run
+ data["prefix"] = parsed_args.prefix
+ data["recipe"] = "bug_{}_{}.py".format(
+ data["bug_id"], data["xul"].split("/")[-1].split(".")[0]
+ )
+
+ main()
+
+
+def main():
+ dry_run = data["dry-run"]
+ dtds = get_dtds(data["dtd"], data["mozilla-central"])
+
+ print("======== DTDs ========")
+ print(json.dumps(dtds, sort_keys=True, indent=2))
+
+ s = read_file(data["xul"], data["mozilla-central"])
+
+ print("======== INPUT ========")
+ print(s)
+
+ print("======== OUTPUT ========")
+ (new_xul, messages) = collect_messages(s, data["prefix"])
+ print(new_xul)
+ if not dry_run:
+ write_file(data["xul"], new_xul, data["mozilla-central"])
+
+ print("======== L10N ========")
+
+ print(json.dumps(messages, sort_keys=True, indent=2))
+
+ migration = build_migration(messages, dtds, data)
+
+ print("======== MIGRATION ========")
+ print(migration)
+ recipe_path = "{}/{}".format(data["migration"], data["recipe"])
+ if not dry_run:
+ write_file(recipe_path, migration, data["mozilla-central"])
+
+ ftl = build_ftl(messages, dtds, data)
+
+ print("======== Fluent ========")
+ print(ftl.encode("utf-8"))
+ if not dry_run:
+ write_file(
+ data["ftl"], ftl.encode("utf-8"), data["mozilla-central"], append=True
+ )
+
+
+if __name__ == "__main__":
+ parse_inputs()
diff --git a/python/l10n/convert_xul_to_fluent/lib/__init__.py b/python/l10n/convert_xul_to_fluent/lib/__init__.py
new file mode 100644
index 0000000000..443d56533f
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/lib/__init__.py
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+
+# from .xul import collect_messages
diff --git a/python/l10n/convert_xul_to_fluent/lib/dtd.py b/python/l10n/convert_xul_to_fluent/lib/dtd.py
new file mode 100644
index 0000000000..85348e62ff
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/lib/dtd.py
@@ -0,0 +1,28 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+from io import StringIO
+from lxml import etree
+from .utils import read_file
+
+
+def get_dtds(sources, base_path):
+ entries = {}
+ for source in sources:
+ dtd = get_dtd(source, base_path)
+ for entry in dtd:
+ entries[entry] = {"value": dtd[entry], "file": source}
+ return entries
+
+
+def get_dtd(dtd_source, base_path):
+ entries = {}
+
+ source = read_file(dtd_source, base_path)
+
+ dtd = etree.DTD(StringIO(source.decode("utf-8")))
+ for entity in dtd.entities():
+ entries[entity.name] = entity.content
+ return entries
diff --git a/python/l10n/convert_xul_to_fluent/lib/fluent.py b/python/l10n/convert_xul_to_fluent/lib/fluent.py
new file mode 100644
index 0000000000..ecab538579
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/lib/fluent.py
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+from fluent.syntax import ast
+from fluent.syntax.serializer import FluentSerializer
+
+
+def get_value_from_dtd(name, dtd):
+ return dtd[name[1:-1]]["value"]
+
+
+def build_ftl(messages, dtd, data):
+ res = ast.Resource()
+
+ for id_str in messages:
+ msg = messages[id_str]
+ l10n_id = ast.Identifier(id_str)
+ val = None
+ attrs = []
+ if msg["value"]:
+ dtd_val = get_value_from_dtd(msg["value"], dtd)
+ val = ast.Pattern([ast.TextElement(dtd_val)])
+ for attr_name in msg["attrs"]:
+ dtd_val = get_value_from_dtd(msg["attrs"][attr_name], dtd)
+ attr_val = ast.Pattern([ast.TextElement(dtd_val)])
+ attrs.append(ast.Attribute(ast.Identifier(attr_name), attr_val))
+
+ m = ast.Message(l10n_id, val, attrs)
+ res.body.append(m)
+
+ serializer = FluentSerializer()
+ return serializer.serialize(res)
diff --git a/python/l10n/convert_xul_to_fluent/lib/migration.py b/python/l10n/convert_xul_to_fluent/lib/migration.py
new file mode 100644
index 0000000000..efe32c5178
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/lib/migration.py
@@ -0,0 +1,77 @@
+from __future__ import absolute_import
+
+
+def to_chrome_path(path):
+ path = path.replace("/locales/en-US", "")
+ if path.startswith("./"):
+ path = path[2:]
+ return path
+
+
+def get_dtd_path(name, dtds):
+ return dtds[name[1:-1]]["file"]
+
+
+def get_entity_name(s):
+ return s[1:-1]
+
+
+def ind(n=0):
+ return " " * 4 * n
+
+
+def add_copy(dtd, entity_id):
+ result = "{ " + 'COPY("{0}", "{1}")'.format(dtd, entity_id) + " }\n"
+ return result
+
+
+def make_header():
+ res = "# coding=utf8\n\n"
+ res += "# Any copyright is dedicated to the Public Domain.\n"
+ res += "# http://creativecommons.org/publicdomain/zero/1.0/\n\n"
+ res += "from __future__ import absolute_import\n"
+ res += "import fluent.syntax.ast as FTL\n"
+ res += "from fluent.migrate.helpers import transforms_from\n"
+ res += "from fluent.migrate.helpers import MESSAGE_REFERENCE, "
+ res += "TERM_REFERENCE, VARIABLE_REFERENCE\n"
+ res += "from fluent.migrate import COPY, CONCAT, REPLACE\n"
+
+ return res
+
+
+def build_migration(messages, dtds, data):
+ res = make_header()
+ desc = "Bug {0} - {1}, part {{index}}.".format(data["bug_id"], data["description"])
+ res += '\n\ndef migrate(ctx):\n """{0}"""\n\n'.format(desc)
+
+ for dtd_path in data["dtd"]:
+ res += "{0}ctx.maybe_add_localization('{1}')\n".format(
+ ind(1), to_chrome_path(dtd_path)
+ )
+
+ res += "\n"
+ res += ind(1) + "ctx.add_transforms(\n"
+ res += ind(2) + "'{0}',\n".format(to_chrome_path(data["ftl"]))
+ res += ind(2) + "'{0}',\n".format(to_chrome_path(data["ftl"]))
+ res += ind(2) + "transforms_from(\n"
+ res += '"""\n'
+ for l10n_id in messages:
+ msg = messages[l10n_id]
+ if not msg["attrs"]:
+ entity = get_entity_name(msg["value"])
+ entity_path = to_chrome_path(get_dtd_path(msg["value"], dtds))
+ res += "{0} = {1}".format(l10n_id, add_copy(entity_path, entity))
+ else:
+ res += "{0} = \n".format(l10n_id)
+ for name in msg["attrs"]:
+ attr = msg["attrs"][name]
+ attr_name = get_entity_name(attr)
+ entity_path = to_chrome_path(get_dtd_path(attr, dtds))
+ res += "{0}.{1} = {2}".format(
+ ind(1), name, add_copy(entity_path, attr_name)
+ )
+ res += '"""\n'
+ res += ind(2) + ")\n"
+ res += ind(1) + ")"
+
+ return res
diff --git a/python/l10n/convert_xul_to_fluent/lib/utils.py b/python/l10n/convert_xul_to_fluent/lib/utils.py
new file mode 100644
index 0000000000..f90b002c2e
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/lib/utils.py
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+import os
+
+
+def read_file(path, base_path=None):
+ if base_path is not None:
+ path = os.path.join(base_path, path)
+ path = path.replace("\\", "/")
+ with open(path) as fptr:
+ return fptr.read()
+
+
+def write_file(path, text, base_path=None, append=False):
+ if base_path is not None:
+ path = os.path.join(base_path, path)
+ path = path.replace("\\", "/")
+ mode = "a" if append and os.path.exists(path) else "w"
+ with open(path, mode) as text_file:
+ text_file.write(text)
diff --git a/python/l10n/convert_xul_to_fluent/lib/xul.py b/python/l10n/convert_xul_to_fluent/lib/xul.py
new file mode 100644
index 0000000000..cb940c6c17
--- /dev/null
+++ b/python/l10n/convert_xul_to_fluent/lib/xul.py
@@ -0,0 +1,117 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+import re
+import string
+
+tag_re = r"<([a-z]+[^>/]*)(/?>)([^<]*)(?:</[a-z]+>)?"
+attr_re = r'\s+([a-z]+)="([\&\;\.a-zA-Z0-9]+)"'
+prefix = ""
+
+messages = {}
+
+
+def is_entity(s):
+ return s.startswith("&") and s.endswith(";")
+
+
+def convert_camel_case(name):
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name)
+ return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower()
+
+
+def construct_l10n_id(val, attrs):
+ global prefix
+ id = None
+ if val:
+ id = val[1:-1].strip(string.digits)
+ else:
+ core = None
+ for k in attrs:
+ v = attrs[k][1:-1].strip(string.digits).split(".")
+ if not core:
+ if v[-1].lower() == k:
+ core = ".".join(v[:-1])
+ else:
+ core = ".".join(v)
+ if core:
+ id = core
+ id = id.replace(".", "-")
+ id = convert_camel_case(id)
+ if prefix:
+ id = "{}-{}".format(prefix, id)
+ return id
+
+
+vector = 0
+is_l10n = False
+
+
+def get_indent(pre_tag):
+ if "\n" not in pre_tag:
+ return " "
+ last_bl = pre_tag.rindex("\n")
+ indent = 0
+ for ch in pre_tag[last_bl:]:
+ if ch == " ":
+ indent += 1
+ return "\n" + " " * indent
+
+
+def tagrepl(m):
+ global vector
+ global is_l10n
+ vector = 0
+
+ is_l10n = False
+ l10n_val = None
+ l10n_attrs = {}
+ if is_entity(m.group(3)):
+ is_l10n = True
+ l10n_val = m.group(3)
+
+ def attrrepl(m2):
+ global vector
+ global is_l10n
+ attr_l10n = False
+ if is_entity(m2.group(2)):
+ attr_l10n = True
+ l10n_attrs[m2.group(1)] = m2.group(2)
+ if attr_l10n:
+ is_l10n = True
+ vector = vector + len(m2.group(0))
+ return ""
+ return m2.group(0)
+
+ tag = re.sub(attr_re, attrrepl, m.group(0))
+ if is_l10n:
+ l10n_id = construct_l10n_id(l10n_val, l10n_attrs)
+ messages[l10n_id] = {"value": l10n_val, "attrs": l10n_attrs}
+ indent = get_indent(tag[0 : len(m.group(1)) + 1 - vector])
+ tag = (
+ tag[0 : len(m.group(1)) + 1 - vector]
+ + indent
+ + 'data-l10n-id="'
+ + l10n_id
+ + '"'
+ + m.group(2)
+ + (m.group(3) if not l10n_val else "")
+ + tag[len(m.group(1)) + 1 + len(m.group(2)) + len(m.group(3)) - vector :]
+ )
+ return tag
+
+
+def collect_messages(xul_source, in_prefix):
+ global messages
+ global prefix
+ messages = {}
+ prefix = in_prefix
+
+ new_source = re.sub(tag_re, tagrepl, xul_source)
+ return (new_source, messages)
+
+
+if __name__ == "__main__":
+ pass
diff --git a/python/l10n/fluent_migrations/__init__.py b/python/l10n/fluent_migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/l10n/fluent_migrations/__init__.py
diff --git a/python/l10n/fluent_migrations/bug_1552333_aboutCertError.py b/python/l10n/fluent_migrations/bug_1552333_aboutCertError.py
new file mode 100644
index 0000000000..eb395d44a9
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1552333_aboutCertError.py
@@ -0,0 +1,41 @@
+# coding=utf8
+
+# 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
+from fluent.migrate.helpers import VARIABLE_REFERENCE
+from fluent.migrate import COPY, REPLACE
+
+
+def migrate(ctx):
+ """Bug 1552333 - Migrate strings from pipnss.properties to aboutCertError.ftl"""
+ ctx.add_transforms(
+ "browser/browser/aboutCertError.ftl",
+ "browser/browser/aboutCertError.ftl",
+ transforms_from(
+ """
+cert-error-symantec-distrust-admin = { COPY(from_path, "certErrorSymantecDistrustAdministrator") }
+""",
+ from_path="security/manager/chrome/pipnss/pipnss.properties",
+ ),
+ )
+ ctx.add_transforms(
+ "browser/browser/aboutCertError.ftl",
+ "browser/browser/aboutCertError.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("cert-error-symantec-distrust-description"),
+ value=REPLACE(
+ "security/manager/chrome/pipnss/pipnss.properties",
+ "certErrorSymantecDistrustDescription1",
+ {
+ "%1$S": VARIABLE_REFERENCE("hostname"),
+ },
+ normalize_printf=True,
+ ),
+ ),
+ ],
+ )
diff --git a/python/l10n/fluent_migrations/bug_1565574_protocol_handler_dialog.py b/python/l10n/fluent_migrations/bug_1565574_protocol_handler_dialog.py
new file mode 100644
index 0000000000..f793221f50
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1565574_protocol_handler_dialog.py
@@ -0,0 +1,37 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+ """Bug 1565574 - Migrate protocol handler dialog strings to fluent, part {index}."""
+
+ ctx.add_transforms(
+ "toolkit/toolkit/global/handlerDialog.ftl",
+ "toolkit/toolkit/global/handlerDialog.ftl",
+ transforms_from(
+ """
+choose-other-app-description = { COPY(from_path, "ChooseOtherApp.description") }
+choose-app-btn =
+ .label = { COPY(from_path, "ChooseApp.label") }
+ .accessKey = { COPY(from_path, "ChooseApp.accessKey") }
+""",
+ from_path="toolkit/chrome/mozapps/handling/handling.dtd",
+ ),
+ )
+
+ ctx.add_transforms(
+ "toolkit/toolkit/global/handlerDialog.ftl",
+ "toolkit/toolkit/global/handlerDialog.ftl",
+ transforms_from(
+ """
+choose-dialog-privatebrowsing-disabled = { COPY(from_path, "privatebrowsing.disabled.label") }
+choose-other-app-window-title = { COPY(from_path, "choose.application.title") }
+""",
+ from_path="toolkit/chrome/mozapps/handling/handling.properties",
+ ),
+ )
diff --git a/python/l10n/fluent_migrations/bug_1568133_menubar.py b/python/l10n/fluent_migrations/bug_1568133_menubar.py
new file mode 100644
index 0000000000..44fc0af2f7
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1568133_menubar.py
@@ -0,0 +1,141 @@
+# 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
+from fluent.migrate import COPY, CONCAT, REPLACE
+from fluent.migrate.helpers import TERM_REFERENCE
+
+
+def migrate(ctx):
+ """Bug 1568133 - Migrate remaining menubar from dtd to ftl, part {index}"""
+
+ ctx.add_transforms(
+ "browser/browser/menubar.ftl",
+ "browser/browser/menubar.ftl",
+ transforms_from(
+ """
+menu-application-services =
+ .label = { COPY(base_path, "servicesMenuMac.label") }
+menu-application-hide-other =
+ .label = { COPY(base_path, "hideOtherAppsCmdMac.label") }
+menu-application-show-all =
+ .label = { COPY(base_path, "showAllAppsCmdMac.label") }
+menu-application-touch-bar =
+ .label = { COPY(base_path, "touchBarCmdMac.label") }
+
+menu-quit =
+ .label =
+ { PLATFORM() ->
+ [windows] { COPY(browser_path, "quitApplicationCmdWin2.label") }
+ *[other] { COPY(browser_path, "quitApplicationCmd.label") }
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [windows] { COPY(browser_path, "quitApplicationCmdWin2.accesskey") }
+ *[other] { COPY(browser_path, "quitApplicationCmd.accesskey") }
+ }
+
+menu-quit-button =
+ .label = { menu-quit.label }
+""",
+ base_path="browser/chrome/browser/baseMenuOverlay.dtd",
+ browser_path="browser/chrome/browser/browser.dtd",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/menubar.ftl",
+ "browser/browser/menubar.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("menu-application-hide-this"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=REPLACE(
+ "browser/chrome/browser/baseMenuOverlay.dtd",
+ "hideThisAppCmdMac2.label",
+ {
+ "&brandShorterName;": TERM_REFERENCE(
+ "brand-shorter-name"
+ ),
+ },
+ ),
+ )
+ ],
+ ),
+ FTL.Message(
+ id=FTL.Identifier("menu-quit-mac"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=REPLACE(
+ "browser/chrome/browser/browser.dtd",
+ "quitApplicationCmdMac2.label",
+ {
+ "&brandShorterName;": TERM_REFERENCE(
+ "brand-shorter-name"
+ ),
+ },
+ ),
+ )
+ ],
+ ),
+ FTL.Message(
+ id=FTL.Identifier("menu-quit-button-win"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=FTL.Pattern(
+ elements=[
+ FTL.Placeable(
+ expression=FTL.MessageReference(
+ id=FTL.Identifier("menu-quit"),
+ attribute=FTL.Identifier("label"),
+ )
+ )
+ ]
+ ),
+ ),
+ FTL.Attribute(
+ id=FTL.Identifier("tooltip"),
+ value=REPLACE(
+ "browser/chrome/browser/browser.dtd",
+ "quitApplicationCmdWin2.tooltip",
+ {
+ "&brandShorterName;": TERM_REFERENCE(
+ "brand-shorter-name"
+ ),
+ },
+ ),
+ ),
+ ],
+ ),
+ FTL.Message(
+ id=FTL.Identifier("menu-about"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=REPLACE(
+ "browser/chrome/browser/baseMenuOverlay.dtd",
+ "aboutProduct2.label",
+ {
+ "&brandShorterName;": TERM_REFERENCE(
+ "brand-shorter-name"
+ ),
+ },
+ ),
+ ),
+ FTL.Attribute(
+ id=FTL.Identifier("accesskey"),
+ value=COPY(
+ "browser/chrome/browser/baseMenuOverlay.dtd",
+ "aboutProduct2.accesskey",
+ ),
+ ),
+ ],
+ ),
+ ],
+ )
diff --git a/python/l10n/fluent_migrations/bug_1634042_page_action_menu.py b/python/l10n/fluent_migrations/bug_1634042_page_action_menu.py
new file mode 100644
index 0000000000..d4ade307d8
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1634042_page_action_menu.py
@@ -0,0 +1,112 @@
+# coding=utf8
+
+# 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 import COPY, PLURALS, REPLACE, REPLACE_IN_TEXT
+from fluent.migrate.helpers import TERM_REFERENCE, VARIABLE_REFERENCE, transforms_from
+
+
+def migrate(ctx):
+ """Bug 1634042 - Adding page action labels to fluent, part {index}."""
+
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ transforms_from(
+ """
+page-action-pin-tab-panel =
+ .label = { COPY(from_path, "pinTab.label") }
+page-action-pin-tab-urlbar =
+ .tooltiptext = { COPY(from_path, "pinTab.label") }
+page-action-unpin-tab-panel =
+ .label = { COPY(from_path, "unpinTab.label") }
+page-action-unpin-tab-urlbar =
+ .tooltiptext = { COPY(from_path, "unpinTab.label") }
+page-action-copy-url-panel =
+ .label = { COPY(from_path, "pageAction.copyLink.label") }
+page-action-copy-url-urlbar =
+ .tooltiptext = { COPY(from_path, "pageAction.copyLink.label") }
+page-action-email-link-panel =
+ .label = { COPY(from_path, "emailPageCmd.label") }
+page-action-email-link-urlbar =
+ .tooltiptext = { COPY(from_path, "emailPageCmd.label") }
+page-action-share-url-panel =
+ .label = { COPY(from_path, "pageAction.shareUrl.label") }
+page-action-share-url-urlbar =
+ .tooltiptext = { COPY(from_path, "pageAction.shareUrl.label") }
+page-action-share-more-panel =
+ .label = { COPY(from_path, "pageAction.shareMore.label") }
+page-action-send-tab-not-ready =
+ .label = { COPY(from_path, "sendToDevice.syncNotReady.label") }
+""",
+ from_path="browser/chrome/browser/browser.dtd",
+ ),
+ )
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("page-action-pocket-panel"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=REPLACE(
+ "browser/chrome/browser/browser.dtd",
+ "saveToPocketCmd.label",
+ {
+ "Pocket": TERM_REFERENCE("pocket-brand-name"),
+ },
+ ),
+ )
+ ],
+ )
+ ],
+ )
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("page-action-send-tabs-panel"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=PLURALS(
+ "browser/chrome/browser/browser.properties",
+ "pageAction.sendTabsToDevice.label",
+ VARIABLE_REFERENCE("tabCount"),
+ lambda text: REPLACE_IN_TEXT(
+ text, {"#1": VARIABLE_REFERENCE("tabCount")}
+ ),
+ ),
+ )
+ ],
+ )
+ ],
+ )
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("page-action-send-tabs-urlbar"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("tooltiptext"),
+ value=PLURALS(
+ "browser/chrome/browser/browser.properties",
+ "pageAction.sendTabsToDevice.label",
+ VARIABLE_REFERENCE("tabCount"),
+ lambda text: REPLACE_IN_TEXT(
+ text, {"#1": VARIABLE_REFERENCE("tabCount")}
+ ),
+ ),
+ )
+ ],
+ )
+ ],
+ )
diff --git a/python/l10n/fluent_migrations/bug_1658629_migration_urlbar_actions.py b/python/l10n/fluent_migrations/bug_1658629_migration_urlbar_actions.py
new file mode 100644
index 0000000000..e7fecad144
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1658629_migration_urlbar_actions.py
@@ -0,0 +1,60 @@
+# coding=utf8
+
+# 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
+from fluent.migrate import REPLACE
+from fluent.migrate.helpers import VARIABLE_REFERENCE
+
+
+def migrate(ctx):
+ """Bug 1658629 - Show proper action text when moving through local one-off search buttons, part {index}."""
+
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("urlbar-result-action-search-w-engine"),
+ value=REPLACE(
+ "toolkit/chrome/global/autocomplete.properties",
+ "searchWithEngine",
+ {"%1$S": VARIABLE_REFERENCE("engine")},
+ normalize_printf=True,
+ ),
+ )
+ ],
+ )
+
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("urlbar-result-action-search-in-private-w-engine"),
+ value=REPLACE(
+ "toolkit/chrome/global/autocomplete.properties",
+ "searchInPrivateWindowWithEngine",
+ {"%1$S": VARIABLE_REFERENCE("engine")},
+ normalize_printf=True,
+ ),
+ )
+ ],
+ )
+
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ transforms_from(
+ """
+urlbar-result-action-switch-tab = { COPY(from_path, "switchToTab2") }
+urlbar-result-action-search-in-private = { COPY(from_path, "searchInPrivateWindow") }
+urlbar-result-action-visit = { COPY(from_path, "visit") }
+
+""",
+ from_path="toolkit/chrome/global/autocomplete.properties",
+ ),
+ )
diff --git a/python/l10n/fluent_migrations/bug_1667781_preferences_dialogs.py b/python/l10n/fluent_migrations/bug_1667781_preferences_dialogs.py
new file mode 100644
index 0000000000..b66d4ccf90
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1667781_preferences_dialogs.py
@@ -0,0 +1,102 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+ """Bug 1667781 - Refactor preferences dialogs to use dialog element, part {index}."""
+
+ ctx.add_transforms(
+ "browser/browser/preferences/addEngine.ftl",
+ "browser/browser/preferences/addEngine.ftl",
+ transforms_from(
+ """
+add-engine-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "add-engine-ok.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "add-engine-ok.accesskey") }
+""",
+ from_path="browser/browser/preferences/addEngine.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/preferences/clearSiteData.ftl",
+ "browser/browser/preferences/clearSiteData.ftl",
+ transforms_from(
+ """
+clear-site-data-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "clear-site-data-clear.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "clear-site-data-clear.accesskey") }
+""",
+ from_path="browser/browser/preferences/clearSiteData.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/preferences/containers.ftl",
+ "browser/browser/preferences/containers.ftl",
+ transforms_from(
+ """
+containers-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "containers-button-done.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "containers-button-done.accesskey") }
+""",
+ from_path="browser/browser/preferences/containers.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/preferences/permissions.ftl",
+ "browser/browser/preferences/permissions.ftl",
+ transforms_from(
+ """
+permission-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "permissions-button-ok.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "permissions-button-ok.accesskey") }
+""",
+ from_path="browser/browser/preferences/permissions.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/preferences/siteDataSettings.ftl",
+ "browser/browser/preferences/siteDataSettings.ftl",
+ transforms_from(
+ """
+site-data-settings-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "site-data-button-save.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "site-data-button-save.accesskey") }
+""",
+ from_path="browser/browser/preferences/siteDataSettings.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/preferences/translation.ftl",
+ "browser/browser/preferences/translation.ftl",
+ transforms_from(
+ """
+translation-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "translation-button-close.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "translation-button-close.accesskey") }
+""",
+ from_path="browser/browser/preferences/translation.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/preferences/blocklists.ftl",
+ "browser/browser/preferences/blocklists.ftl",
+ transforms_from(
+ """
+blocklist-dialog =
+ .buttonlabelaccept = { COPY_PATTERN(from_path, "blocklist-button-ok.label") }
+ .buttonaccesskeyaccept = { COPY_PATTERN(from_path, "blocklist-button-ok.accesskey") }
+""",
+ from_path="browser/browser/preferences/blocklists.ftl",
+ ),
+ )
diff --git a/python/l10n/fluent_migrations/bug_1668284_settings_change.py b/python/l10n/fluent_migrations/bug_1668284_settings_change.py
new file mode 100644
index 0000000000..71611b16a6
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1668284_settings_change.py
@@ -0,0 +1,66 @@
+# coding=utf8
+
+# 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
+from fluent.migrate import REPLACE, COPY
+from fluent.migrate.helpers import MESSAGE_REFERENCE, TERM_REFERENCE
+
+
+def migrate(ctx):
+ """Bug 1668284 - Unknown content type change settings label is no longer accurate, part {index}."""
+
+ ctx.add_transforms(
+ "toolkit/toolkit/global/unknownContentType.ftl",
+ "toolkit/toolkit/global/unknownContentType.ftl",
+ [
+ FTL.Message(
+ id=FTL.Identifier("unknowncontenttype-settingschange"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("value"),
+ value=FTL.Pattern(
+ elements=[
+ FTL.Placeable(
+ expression=FTL.SelectExpression(
+ selector=MESSAGE_REFERENCE("PLATFORM()"),
+ variants=[
+ FTL.Variant(
+ key=FTL.Identifier("windows"),
+ default=False,
+ value=REPLACE(
+ "toolkit/chrome/mozapps/downloads/settingsChange.dtd",
+ "settingsChangeOptions.label",
+ {
+ "&brandShortName;": TERM_REFERENCE(
+ "brand-short-name"
+ ),
+ },
+ ),
+ ),
+ FTL.Variant(
+ key=FTL.Identifier("other"),
+ default=True,
+ value=REPLACE(
+ "toolkit/chrome/mozapps/downloads/settingsChange.dtd",
+ "settingsChangePreferences.label",
+ {
+ "&brandShortName;": TERM_REFERENCE(
+ "brand-short-name"
+ ),
+ },
+ ),
+ ),
+ ],
+ )
+ )
+ ]
+ ),
+ )
+ ],
+ )
+ ],
+ )
diff --git a/python/l10n/fluent_migrations/bug_1682022_bookmarks_and_menubar_strings.py b/python/l10n/fluent_migrations/bug_1682022_bookmarks_and_menubar_strings.py
new file mode 100644
index 0000000000..74ab8a248f
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1682022_bookmarks_and_menubar_strings.py
@@ -0,0 +1,83 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+ """Bug 1682022 - Convert some bookmarking strings to Fluent, and copy over some menubar strings, part {index}."""
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ transforms_from(
+ """
+bookmarks-show-all-bookmarks =
+ .label = { COPY(from_path, "showAllBookmarks2.label") }
+bookmarks-recent-bookmarks =
+ .value = { COPY(from_path, "recentBookmarks.label") }
+bookmarks-toolbar-chevron =
+ .tooltiptext = { COPY(from_path, "bookmarksToolbarChevron.tooltip") }
+bookmarks-sidebar-content =
+ .aria-label = { COPY(from_path, "bookmarksButton.label") }
+bookmarks-menu-button =
+ .label = { COPY(from_path, "bookmarksMenuButton2.label") }
+bookmarks-other-bookmarks-menu =
+ .label = { COPY(from_path, "bookmarksMenuButton.other.label") }
+bookmarks-mobile-bookmarks-menu =
+ .label = { COPY(from_path, "bookmarksMenuButton.mobile.label") }
+bookmarks-tools-sidebar-visibility =
+ .label = { $isVisible ->
+ [true] { COPY(from_path, "hideBookmarksSidebar.label") }
+ *[other] { COPY(from_path, "viewBookmarksSidebar2.label") }
+ }
+bookmarks-tools-toolbar-visibility =
+ .label = { $isVisible ->
+ [true] { COPY(from_path, "hideBookmarksToolbar.label") }
+ *[other] { COPY(from_path, "viewBookmarksToolbar.label") }
+ }
+bookmarks-tools-menu-button-visibility =
+ .label = { $isVisible ->
+ [true] { COPY(from_path, "removeBookmarksMenu.label") }
+ *[other] { COPY(from_path, "addBookmarksMenu.label") }
+ }
+bookmarks-search =
+ .label = { COPY(from_path, "searchBookmarks.label") }
+bookmarks-tools =
+ .label = { COPY(from_path, "bookmarkingTools.label") }
+bookmarks-toolbar =
+ .toolbarname = { COPY(from_path, "personalbarCmd.label") }
+ .accesskey = { COPY(from_path, "personalbarCmd.accesskey") }
+ .aria-label = { COPY(from_path, "personalbar.accessibleLabel") }
+bookmarks-toolbar-menu =
+ .label = { COPY(from_path, "personalbarCmd.label") }
+bookmarks-toolbar-placeholder =
+ .title = { COPY(from_path, "bookmarksToolbarItem.label") }
+bookmarks-toolbar-placeholder-button =
+ .label = { COPY(from_path, "bookmarksToolbarItem.label") }
+library-bookmarks-menu =
+ .label = { COPY(from_path, "bookmarksSubview.label") }
+""",
+ from_path="browser/chrome/browser/browser.dtd",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ transforms_from(
+ """
+library-bookmarks-bookmark-this-page =
+ .label = { COPY_PATTERN(from_path, "menu-bookmark-this-page.label") }
+library-bookmarks-bookmark-edit =
+ .label = { COPY_PATTERN(from_path, "menu-bookmark-edit.label") }
+
+more-menu-go-offline =
+ .label = { COPY_PATTERN(from_path, "menu-file-go-offline.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-file-go-offline.accesskey") }
+""",
+ from_path="browser/browser/menubar.ftl",
+ ),
+ )
diff --git a/python/l10n/fluent_migrations/bug_1682022_context_menu_and_browser_strings.py b/python/l10n/fluent_migrations/bug_1682022_context_menu_and_browser_strings.py
new file mode 100644
index 0000000000..0b0b68874c
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1682022_context_menu_and_browser_strings.py
@@ -0,0 +1,38 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+ """Bug 1682022 - Fork more strings from the context menu and browser.dtd for the AppMenu, part {index}."""
+ ctx.add_transforms(
+ "browser/browser/appmenu.ftl",
+ "browser/browser/appmenu.ftl",
+ transforms_from(
+ """
+appmenuitem-save-page =
+ .label = { COPY_PATTERN(from_path, "main-context-menu-page-save.label") }
+""",
+ from_path="browser/browser/browserContext.ftl",
+ ),
+ )
+
+ ctx.add_transforms(
+ "browser/browser/appmenu.ftl",
+ "browser/browser/appmenu.ftl",
+ transforms_from(
+ """
+appmenuitem-new-window =
+ .label = { COPY(from_path, "newNavigatorCmd.label") }
+appmenuitem-new-private-window =
+ .label = { COPY(from_path, "newPrivateWindow.label") }
+appmenuitem-fullscreen =
+ .label = { COPY(from_path, "fullScreenCmd.label") }
+""",
+ from_path="browser/chrome/browser/browser.dtd",
+ ),
+ )
diff --git a/python/l10n/fluent_migrations/bug_1683419_forking_help_menu.py b/python/l10n/fluent_migrations/bug_1683419_forking_help_menu.py
new file mode 100644
index 0000000000..65e6edbe47
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1683419_forking_help_menu.py
@@ -0,0 +1,55 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+ """Bug 1683419 - Fork the Help menu strings for use in the AppMenu, part {index}."""
+ ctx.add_transforms(
+ "browser/browser/appmenu.ftl",
+ "browser/browser/appmenu.ftl",
+ transforms_from(
+ """
+appmenu-about =
+ .label = { COPY_PATTERN(from_path, "menu-about.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-about.accesskey") }
+appmenu-help-product =
+ .label = { COPY_PATTERN(from_path, "menu-help-product.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-product.accesskey") }
+appmenu-help-show-tour =
+ .label = { COPY_PATTERN(from_path, "menu-help-show-tour.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-show-tour.accesskey") }
+appmenu-help-import-from-another-browser =
+ .label = { COPY_PATTERN(from_path, "menu-help-import-from-another-browser.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-import-from-another-browser.accesskey") }
+appmenu-help-keyboard-shortcuts =
+ .label = { COPY_PATTERN(from_path, "menu-help-keyboard-shortcuts.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-keyboard-shortcuts.accesskey") }
+appmenu-help-troubleshooting-info =
+ .label = { COPY_PATTERN(from_path, "menu-help-troubleshooting-info.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-troubleshooting-info.accesskey") }
+appmenu-help-feedback-page =
+ .label = { COPY_PATTERN(from_path, "menu-help-feedback-page.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-feedback-page.accesskey") }
+appmenu-help-safe-mode-without-addons =
+ .label = { COPY_PATTERN(from_path, "menu-help-safe-mode-without-addons.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-safe-mode-without-addons.accesskey") }
+appmenu-help-safe-mode-with-addons =
+ .label = { COPY_PATTERN(from_path, "menu-help-safe-mode-with-addons.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-safe-mode-with-addons.accesskey") }
+appmenu-help-report-deceptive-site =
+ .label = { COPY_PATTERN(from_path, "menu-help-report-deceptive-site.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-report-deceptive-site.accesskey") }
+appmenu-help-not-deceptive =
+ .label = { COPY_PATTERN(from_path, "menu-help-not-deceptive.label") }
+ .accesskey = { COPY_PATTERN(from_path, "menu-help-not-deceptive.accesskey") }
+appmenu-help-check-for-update =
+ .label = { COPY_PATTERN(from_path, "menu-help-check-for-update.label") }
+""",
+ from_path="browser/browser/menubar.ftl",
+ ),
+ )
diff --git a/python/l10n/fluent_migrations/bug_1686331_library_recent_activity.py b/python/l10n/fluent_migrations/bug_1686331_library_recent_activity.py
new file mode 100644
index 0000000000..baaad1eec8
--- /dev/null
+++ b/python/l10n/fluent_migrations/bug_1686331_library_recent_activity.py
@@ -0,0 +1,23 @@
+# coding=utf8
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from __future__ import absolute_import
+from fluent.migrate.helpers import transforms_from
+from fluent.migrate import COPY_PATTERN
+
+
+def migrate(ctx):
+ """Bug 1686331 - Library menu should not have a scroll bar part {index}."""
+ ctx.add_transforms(
+ "browser/browser/browser.ftl",
+ "browser/browser/browser.ftl",
+ transforms_from(
+ """
+library-recent-activity-title =
+ .value = {COPY_PATTERN(from_path, "library-recent-activity-label")}
+""",
+ from_path="browser/browser/browser.ftl",
+ ),
+ )
diff --git a/python/l10n/test_fluent_migrations/__init__.py b/python/l10n/test_fluent_migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/l10n/test_fluent_migrations/__init__.py
diff --git a/python/l10n/test_fluent_migrations/fmt.py b/python/l10n/test_fluent_migrations/fmt.py
new file mode 100644
index 0000000000..98c41f2ab8
--- /dev/null
+++ b/python/l10n/test_fluent_migrations/fmt.py
@@ -0,0 +1,190 @@
+from __future__ import absolute_import, print_function
+import codecs
+from difflib import unified_diff
+import logging
+import os
+import re
+import shutil
+import sys
+
+import hglib
+from mozboot.util import get_state_dir
+import mozpack.path as mozpath
+
+from compare_locales.merge import merge_channels
+from compare_locales.paths.files import ProjectFiles
+from compare_locales.paths.configparser import TOMLParser
+from fluent.migrate import validator
+from fluent.syntax import FluentParser, FluentSerializer
+
+
+def inspect_migration(path):
+ """Validate recipe and extract some metadata."""
+ return validator.Validator.validate(path)
+
+
+def prepare_object_dir(cmd):
+ """Prepare object dir to have an up-to-date clone of gecko-strings.
+
+ We run this once per mach invocation, for all tested migrations.
+ """
+ obj_dir = mozpath.join(cmd.topobjdir, "python", "l10n")
+ if not os.path.exists(obj_dir):
+ os.makedirs(obj_dir)
+ state_dir = get_state_dir()
+ if os.path.exists(mozpath.join(state_dir, "gecko-strings")):
+ cmd.run_process(
+ ["hg", "pull", "-u"], cwd=mozpath.join(state_dir, "gecko-strings")
+ )
+ else:
+ cmd.run_process(
+ ["hg", "clone", "https://hg.mozilla.org/l10n/gecko-strings"],
+ cwd=state_dir,
+ )
+ return obj_dir
+
+
+def diff_resources(left_path, right_path):
+ parser = FluentParser(with_spans=False)
+ serializer = FluentSerializer(with_junk=True)
+ lines = []
+ for p in (left_path, right_path):
+ with codecs.open(p, encoding="utf-8") as fh:
+ res = parser.parse(fh.read())
+ lines.append(serializer.serialize(res).splitlines(True))
+ sys.stdout.writelines(
+ chunk for chunk in unified_diff(lines[0], lines[1], left_path, right_path)
+ )
+
+
+def test_migration(cmd, obj_dir, to_test, references):
+ """Test the given recipe.
+
+ This creates a workdir by l10n-merging gecko-strings and the m-c source,
+ to mimmic gecko-strings after the patch to test landed.
+ It then runs the recipe with a gecko-strings clone as localization, both
+ dry and wet.
+ It inspects the generated commits, and shows a diff between the merged
+ reference and the generated content.
+ The diff is intended to be visually inspected. Some changes might be
+ expected, in particular when formatting of the en-US strings is different.
+ """
+ rv = 0
+ migration_name = os.path.splitext(os.path.split(to_test)[1])[0]
+ work_dir = mozpath.join(obj_dir, migration_name)
+
+ paths = os.path.normpath(to_test).split(os.sep)
+ # Migration modules should be in a sub-folder of l10n.
+ migration_module = (
+ ".".join(paths[paths.index("l10n") + 1 : -1]) + "." + migration_name
+ )
+
+ if os.path.exists(work_dir):
+ shutil.rmtree(work_dir)
+ os.makedirs(mozpath.join(work_dir, "reference"))
+ l10n_toml = mozpath.join(
+ cmd.topsrcdir, cmd.substs["MOZ_BUILD_APP"], "locales", "l10n.toml"
+ )
+ pc = TOMLParser().parse(l10n_toml, env={"l10n_base": work_dir})
+ pc.set_locales(["reference"])
+ files = ProjectFiles("reference", [pc])
+ for ref in references:
+ if ref != mozpath.normpath(ref):
+ cmd.log(
+ logging.ERROR,
+ "fluent-migration-test",
+ {
+ "file": to_test,
+ "ref": ref,
+ },
+ 'Reference path "{ref}" needs to be normalized for {file}',
+ )
+ rv = 1
+ continue
+ full_ref = mozpath.join(work_dir, "reference", ref)
+ m = files.match(full_ref)
+ if m is None:
+ raise ValueError("Bad reference path: " + ref)
+ m_c_path = m[1]
+ g_s_path = mozpath.join(work_dir, "gecko-strings", ref)
+ resources = [
+ b"" if not os.path.exists(f) else open(f, "rb").read()
+ for f in (g_s_path, m_c_path)
+ ]
+ ref_dir = os.path.dirname(full_ref)
+ if not os.path.exists(ref_dir):
+ os.makedirs(ref_dir)
+ open(full_ref, "wb").write(merge_channels(ref, resources))
+ client = hglib.clone(
+ source=mozpath.join(get_state_dir(), "gecko-strings"),
+ dest=mozpath.join(work_dir, "en-US"),
+ )
+ client.open()
+ old_tip = client.tip().node
+ run_migration = [
+ cmd._virtualenv_manager.python_path,
+ "-m",
+ "fluent.migrate.tool",
+ "--lang",
+ "en-US",
+ "--reference-dir",
+ mozpath.join(work_dir, "reference"),
+ "--localization-dir",
+ mozpath.join(work_dir, "en-US"),
+ "--dry-run",
+ migration_module,
+ ]
+ cmd.run_process(
+ run_migration,
+ cwd=work_dir,
+ line_handler=print,
+ )
+ # drop --dry-run
+ run_migration.pop(-2)
+ cmd.run_process(
+ run_migration,
+ cwd=work_dir,
+ line_handler=print,
+ )
+ tip = client.tip().node
+ if old_tip == tip:
+ cmd.log(
+ logging.WARN,
+ "fluent-migration-test",
+ {
+ "file": to_test,
+ },
+ "No migration applied for {file}",
+ )
+ return rv
+ for ref in references:
+ diff_resources(
+ mozpath.join(work_dir, "reference", ref),
+ mozpath.join(work_dir, "en-US", ref),
+ )
+ messages = [
+ l.desc.decode("utf-8") for l in client.log(b"::%s - ::%s" % (tip, old_tip))
+ ]
+ bug = re.search("[0-9]{5,}", migration_name).group()
+ # Just check first message for bug number, they're all following the same pattern
+ if bug not in messages[0]:
+ rv = 1
+ cmd.log(
+ logging.ERROR,
+ "fluent-migration-test",
+ {
+ "file": to_test,
+ },
+ "Missing or wrong bug number for {file}",
+ )
+ if any("part {}".format(n + 1) not in msg for n, msg in enumerate(messages)):
+ rv = 1
+ cmd.log(
+ logging.ERROR,
+ "fluent-migration-test",
+ {
+ "file": to_test,
+ },
+ 'Commit messages should have "part {{index}}" for {file}',
+ )
+ return rv