summaryrefslogtreecommitdiffstats
path: root/intl/l10n/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /intl/l10n/test
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/l10n/test')
-rw-r--r--intl/l10n/test/gtest/TestLocalization.cpp118
-rw-r--r--intl/l10n/test/gtest/moz.build11
-rw-r--r--intl/l10n/test/mochitest/chrome.ini6
-rw-r--r--intl/l10n/test/mochitest/localization/test_formatMessages.html101
-rw-r--r--intl/l10n/test/mochitest/localization/test_formatValue.html81
-rw-r--r--intl/l10n/test/mochitest/localization/test_formatValues.html85
-rw-r--r--intl/l10n/test/test_datetimeformat.js76
-rw-r--r--intl/l10n/test/test_l10nregistry.js563
-rw-r--r--intl/l10n/test/test_l10nregistry_fuzzed.js205
-rw-r--r--intl/l10n/test/test_l10nregistry_sync.js536
-rw-r--r--intl/l10n/test/test_localization.js319
-rw-r--r--intl/l10n/test/test_localization_sync.js289
-rw-r--r--intl/l10n/test/test_messagecontext.js47
-rw-r--r--intl/l10n/test/test_missing_variables.js35
-rw-r--r--intl/l10n/test/test_pseudo.js131
-rw-r--r--intl/l10n/test/xpcshell.ini13
16 files changed, 2616 insertions, 0 deletions
diff --git a/intl/l10n/test/gtest/TestLocalization.cpp b/intl/l10n/test/gtest/TestLocalization.cpp
new file mode 100644
index 0000000000..804c228c98
--- /dev/null
+++ b/intl/l10n/test/gtest/TestLocalization.cpp
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/intl/Localization.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::intl;
+
+TEST(Intl_Localization, FormatValueSyncMissing)
+{
+ nsTArray<nsCString> resIds = {
+ "toolkit/global/handlerDialog.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+
+ auto l10nId = "non-existing-l10n-id"_ns;
+ IgnoredErrorResult rv;
+ nsAutoCString result;
+
+ l10n->FormatValueSync(l10nId, {}, result, rv);
+ ASSERT_FALSE(rv.Failed());
+ ASSERT_TRUE(result.IsEmpty());
+}
+
+TEST(Intl_Localization, FormatValueSync)
+{
+ nsTArray<nsCString> resIds = {
+ "toolkit/global/handlerDialog.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+
+ auto l10nId = "permission-dialog-unset-description"_ns;
+ IgnoredErrorResult rv;
+ nsAutoCString result;
+
+ l10n->FormatValueSync(l10nId, {}, result, rv);
+ ASSERT_FALSE(rv.Failed());
+ ASSERT_FALSE(result.IsEmpty());
+}
+
+TEST(Intl_Localization, FormatValueSyncWithArgs)
+{
+ nsTArray<nsCString> resIds = {
+ "toolkit/global/handlerDialog.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+
+ auto l10nId = "permission-dialog-description"_ns;
+
+ auto l10nArgs = dom::Optional<intl::L10nArgs>();
+ l10nArgs.Construct();
+
+ auto dirArg = l10nArgs.Value().Entries().AppendElement();
+ dirArg->mKey = "scheme"_ns;
+ dirArg->mValue.SetValue().SetAsUTF8String().Assign("Foo"_ns);
+
+ IgnoredErrorResult rv;
+ nsAutoCString result;
+
+ l10n->FormatValueSync(l10nId, l10nArgs, result, rv);
+ ASSERT_FALSE(rv.Failed());
+ ASSERT_TRUE(result.Find("Foo"_ns) > -1);
+}
+
+TEST(Intl_Localization, FormatMessagesSync)
+{
+ nsTArray<nsCString> resIds = {
+ "toolkit/global/handlerDialog.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+
+ dom::Sequence<dom::OwningUTF8StringOrL10nIdArgs> l10nIds;
+ auto* l10nId = l10nIds.AppendElement(fallible);
+ ASSERT_TRUE(l10nId);
+ l10nId->SetAsUTF8String().Assign("permission-dialog-unset-description"_ns);
+
+ IgnoredErrorResult rv;
+ nsTArray<dom::Nullable<dom::L10nMessage>> result;
+
+ l10n->FormatMessagesSync(l10nIds, result, rv);
+ ASSERT_FALSE(rv.Failed());
+ ASSERT_FALSE(result.IsEmpty());
+}
+
+TEST(Intl_Localization, FormatMessagesSyncWithArgs)
+{
+ nsTArray<nsCString> resIds = {
+ "toolkit/global/handlerDialog.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+
+ dom::Sequence<dom::OwningUTF8StringOrL10nIdArgs> l10nIds;
+ L10nIdArgs& key0 = l10nIds.AppendElement(fallible)->SetAsL10nIdArgs();
+ key0.mId.Assign("permission-dialog-description"_ns);
+ auto arg = key0.mArgs.SetValue().Entries().AppendElement();
+ arg->mKey = "scheme"_ns;
+ arg->mValue.SetValue().SetAsUTF8String().Assign("Foo"_ns);
+
+ L10nIdArgs& key1 = l10nIds.AppendElement(fallible)->SetAsL10nIdArgs();
+ key1.mId.Assign("chooser-window"_ns);
+
+ IgnoredErrorResult rv;
+ nsTArray<dom::Nullable<dom::L10nMessage>> result;
+
+ l10n->FormatMessagesSync(l10nIds, result, rv);
+ ASSERT_FALSE(rv.Failed());
+ ASSERT_TRUE(result.Length() == 2);
+ ASSERT_TRUE(result.ElementAt(0).Value().mValue.Find("Foo"_ns) > -1);
+
+ auto fmtAttr = result.ElementAt(1).Value().mAttributes.Value();
+ ASSERT_TRUE(fmtAttr.Length() == 2);
+ ASSERT_FALSE(fmtAttr.ElementAt(0).mName.IsEmpty());
+ ASSERT_FALSE(fmtAttr.ElementAt(0).mValue.IsEmpty());
+}
diff --git a/intl/l10n/test/gtest/moz.build b/intl/l10n/test/gtest/moz.build
new file mode 100644
index 0000000000..6d04077a50
--- /dev/null
+++ b/intl/l10n/test/gtest/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestLocalization.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/intl/l10n/test/mochitest/chrome.ini b/intl/l10n/test/mochitest/chrome.ini
new file mode 100644
index 0000000000..1b18537cc6
--- /dev/null
+++ b/intl/l10n/test/mochitest/chrome.ini
@@ -0,0 +1,6 @@
+[localization/test_formatValue.html]
+skip-if = debug # Intentionally triggers a debug assert for missing Fluent arguments.
+[localization/test_formatValues.html]
+skip-if = debug # Intentionally triggers a debug assert for missing Fluent arguments.
+[localization/test_formatMessages.html]
+skip-if = debug # Intentionally triggers a debug assert for missing Fluent arguments.
diff --git a/intl/l10n/test/mochitest/localization/test_formatMessages.html b/intl/l10n/test/mochitest/localization/test_formatMessages.html
new file mode 100644
index 0000000000..6e5a9f3f41
--- /dev/null
+++ b/intl/l10n/test/mochitest/localization/test_formatMessages.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Localization.prototype.formatMessages API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const mockSource = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}/", [
+ {
+ path: "/localization/en-US/mock.ftl",
+ source: `
+key1 = Value
+ .title = Title 1
+ .accesskey = K
+key2 =
+ .label = This is a label for { $user }
+`
+ }
+ ]);
+ let registry = new L10nRegistry({
+ bundleOptions: {
+ useIsolating: false
+ }
+ });
+ registry.registerSources([mockSource]);
+
+ function getAttributeByName(attributes, name) {
+ return attributes.find(attr => attr.name === name);
+ }
+
+ (async () => {
+ SimpleTest.waitForExplicitFinish();
+
+ const loc = new Localization(
+ ['mock.ftl'],
+ false,
+ registry,
+ );
+
+ {
+ // Simple mix works.
+ let msgs = await loc.formatMessages([
+ {id: "key1"},
+ {id: "key2", args: { user: "Amy"}},
+ ]);
+ {
+ is(msgs[0].value, "Value");
+ let attr0 = getAttributeByName(msgs[0].attributes, "title");
+ is(attr0.name, "title");
+ is(attr0.value, "Title 1");
+ let attr1 = getAttributeByName(msgs[0].attributes, "accesskey");
+ is(attr1.name, "accesskey");
+ is(attr1.value, "K");
+ }
+
+ {
+ is(msgs[1].value, null);
+ let attr0 = getAttributeByName(msgs[1].attributes, "label");
+ is(attr0.name, "label");
+ is(attr0.value, "This is a label for Amy");
+ }
+ }
+
+ {
+ // Missing arguments cause exception in automation.
+ try {
+ let msgs = await loc.formatMessages([
+ {id: "key1"},
+ {id: "key2"},
+ ]);
+ ok(false, "Missing argument didn't cause an exception.");
+ } catch (e) {
+ is(e.message,
+ "[fluent][resolver] errors in en-US/key2: Resolver error: Unknown variable: $user",
+ "Missing key causes an exception.");
+ }
+ }
+
+ {
+ // Missing keys cause exception in automation.
+ try {
+ let msgs = await loc.formatMessages([
+ { id: "key1" },
+ { id: "key4" },
+ ]);
+ ok(false, "Missing key didn't cause an exception.");
+ } catch (e) {
+ is(e.message,
+ "[fluent] Missing message in locale en-US: key4",
+ "Missing key causes an exception.");
+ }
+ }
+ SimpleTest.finish();
+ })();
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/intl/l10n/test/mochitest/localization/test_formatValue.html b/intl/l10n/test/mochitest/localization/test_formatValue.html
new file mode 100644
index 0000000000..e1ee02fa7a
--- /dev/null
+++ b/intl/l10n/test/mochitest/localization/test_formatValue.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Localization.prototype.formatValue API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const mockSource = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}/", [
+ {
+ path: "/localization/en-US/mock.ftl",
+ source: `
+key1 = Value
+key2 = Value { $user }
+key3 = Value { $count }
+`
+ }
+ ]);
+ let registry = new L10nRegistry({
+ bundleOptions: {
+ useIsolating: false
+ }
+ });
+ registry.registerSources([mockSource]);
+
+ (async () => {
+ SimpleTest.waitForExplicitFinish();
+
+ const loc = new Localization(
+ ['mock.ftl'],
+ false,
+ registry,
+ );
+
+ {
+ // Simple value works.
+ let val = await loc.formatValue("key1");
+ is(val, "Value");
+ }
+
+ {
+ // Value with a string argument works.
+ let val = await loc.formatValue("key2", { user: "John" });
+ is(val, "Value John");
+ }
+
+ {
+ // Value with a number argument works.
+ let val = await loc.formatValue("key3", { count: -3.21 });
+ is(val, "Value -3.21");
+ }
+
+ {
+ // Verify that in automation, a missing
+ // argument causes an exception.
+ try {
+ let val = await loc.formatValue("key3");
+ ok(false, "Missing argument didn't cause an exception.");
+ } catch (e) {
+ is(e.message,
+ "[fluent][resolver] errors in en-US/key3: Resolver error: Unknown variable: $count",
+ "Missing key causes an exception.");
+ }
+ }
+
+ {
+ // Incorrect argument type works.
+ // Due to how WebIDL handles union types, it'll convert
+ // the argument to a string `[object Object]`.
+ let val = await loc.formatValue("key2", { user: { name: true } });
+ is(val, "Value [object Object]");
+ }
+
+ SimpleTest.finish();
+ })();
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/intl/l10n/test/mochitest/localization/test_formatValues.html b/intl/l10n/test/mochitest/localization/test_formatValues.html
new file mode 100644
index 0000000000..819d418b2e
--- /dev/null
+++ b/intl/l10n/test/mochitest/localization/test_formatValues.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Localization.prototype.formatValues API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script type="application/javascript">
+ "use strict";
+ const mockSource = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}/", [
+ {
+ path: "/localization/en-US/mock.ftl",
+ source: `
+key1 = Value
+key2 = Value { $user }
+key3 = Value { $count }
+`
+ }
+ ]);
+ let registry = new L10nRegistry({
+ bundleOptions: {
+ useIsolating: false
+ }
+ });
+ registry.registerSources([mockSource]);
+
+ (async () => {
+ SimpleTest.waitForExplicitFinish();
+
+ const loc = new Localization(
+ ['mock.ftl'],
+ false,
+ registry,
+ );
+
+ {
+ // Simple mix works.
+ let vals = await loc.formatValues([
+ {id: "key1"},
+ {id: "key2", args: { user: "Amy"}},
+ {id: "key3", args: { count: -32.12 }},
+ ]);
+ is(vals[0], "Value");
+ is(vals[1], "Value Amy");
+ is(vals[2], "Value -32.12");
+ }
+
+ {
+ // Missing arguments cause exception in automation.
+ try {
+ let vals = await loc.formatValues([
+ {id: "key1"},
+ {id: "key2"},
+ {id: "key3", args: { count: -32.12 }},
+ ]);
+ ok(false, "Missing argument didn't cause an exception.");
+ } catch (e) {
+ is(e.message,
+ "[fluent][resolver] errors in en-US/key2: Resolver error: Unknown variable: $user",
+ "Missing key causes an exception.");
+ }
+ }
+
+ {
+ // Missing keys cause exception in automation.
+ try {
+ let vals = await loc.formatValues([
+ { id: "key1" },
+ { id: "key4", args: { count: -32.12 } },
+ ]);
+ ok(false, "Missing key didn't cause an exception.");
+ } catch (e) {
+ is(e.message,
+ "[fluent] Missing message in locale en-US: key4",
+ "Missing key causes an exception.");
+ }
+ }
+
+ SimpleTest.finish();
+ })();
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/intl/l10n/test/test_datetimeformat.js b/intl/l10n/test/test_datetimeformat.js
new file mode 100644
index 0000000000..4c0794424b
--- /dev/null
+++ b/intl/l10n/test/test_datetimeformat.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const FIREFOX_RELEASE_TIMESTAMP = 1032800850000;
+const FIREFOX_RELEASE_DATE = new Date(FIREFOX_RELEASE_TIMESTAMP);
+
+add_task(function test_date_time_format() {
+ const bundle = new FluentBundle(["en-US"]);
+
+ bundle.addResource(
+ new FluentResource(`
+dt-arg = Just the arg is: {$dateArg}
+dt-bare = The bare date is: { DATETIME($dateArg) }
+dt-month-year = Months and year are not time-zone dependent here: { DATETIME($dateArg, month: "long") }
+dt-bad = This is a bad month: { DATETIME($dateArg, month: "oops") }
+# TODO - Bug 1707728:
+dt-timezone = The timezone: { DATETIME($dateArg, timezone: "America/New_York") }
+dt-unknown = Unknown: { DATETIME($dateArg, unknown: "unknown") }
+dt-style = Style formatting: { DATETIME($dateArg, dateStyle: "short", timeStyle: "short") }
+ `)
+ );
+
+ function testMessage(id, dateArg, expectedMessage) {
+ const message = bundle.formatPattern(bundle.getMessage(id).value, {
+ dateArg,
+ });
+
+ if (typeof expectedMessage === "object") {
+ // Assume regex.
+ ok(
+ expectedMessage.test(message),
+ `"${message}" matches regex: ${expectedMessage.toString()}`
+ );
+ } else {
+ // Assume string.
+ equal(message, expectedMessage);
+ }
+ }
+
+ // TODO - Bug 1707728 - Some of these are implemented as regexes since time zones are not
+ // supported in fluent messages as of yet. They could be simplified if a time zone were
+ // specified.
+ testMessage(
+ "dt-arg",
+ FIREFOX_RELEASE_DATE,
+ /^Just the arg is: (Sun|Mon|Tue) Sep \d+ 2002 \d+:\d+:\d+ .* \(.*\)$/
+ );
+ testMessage(
+ "dt-bare",
+ FIREFOX_RELEASE_TIMESTAMP,
+ /^The bare date is: Sep \d+, 2002, \d+:\d+:\d+ (AM|PM)$/
+ );
+ testMessage(
+ "dt-month-year",
+ FIREFOX_RELEASE_TIMESTAMP,
+ "Months and year are not time-zone dependent here: September"
+ );
+ testMessage(
+ "dt-bad",
+ FIREFOX_RELEASE_TIMESTAMP,
+ /^This is a bad month: Sep \d+, 2002, \d+:\d+:\d+ (AM|PM)$/
+ );
+ testMessage(
+ "dt-unknown",
+ FIREFOX_RELEASE_TIMESTAMP,
+ /^Unknown: Sep \d+, 2002, \d+:\d+:\d+ (AM|PM)$/
+ );
+ testMessage(
+ "dt-style",
+ FIREFOX_RELEASE_TIMESTAMP,
+ /^Style formatting: \d+\/\d+\/\d+, \d+:\d+ (AM|PM)$/
+ );
+
+// TODO - Bug 1707728
+// testMessage("dt-timezone", ...);
+});
diff --git a/intl/l10n/test/test_l10nregistry.js b/intl/l10n/test/test_l10nregistry.js
new file mode 100644
index 0000000000..cbbb1e7316
--- /dev/null
+++ b/intl/l10n/test/test_l10nregistry.js
@@ -0,0 +1,563 @@
+/* Any copyrighequal dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {setTimeout} = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");
+
+const l10nReg = new L10nRegistry();
+
+add_task(function test_methods_presence() {
+ equal(typeof l10nReg.generateBundles, "function");
+ equal(typeof l10nReg.generateBundlesSync, "function");
+ equal(typeof l10nReg.getAvailableLocales, "function");
+ equal(typeof l10nReg.registerSources, "function");
+ equal(typeof l10nReg.removeSources, "function");
+ equal(typeof l10nReg.updateSources, "function");
+});
+
+/**
+ * Test that passing empty resourceIds list works.
+ */
+add_task(async function test_empty_resourceids() {
+ const fs = [];
+
+ const source = L10nFileSource.createMock("test", "", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const bundles = l10nReg.generateBundles(["en-US"], []);
+
+ const done = (await bundles.next()).done;
+
+ equal(done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * Test that passing empty sources list works.
+ */
+add_task(async function test_empty_sources() {
+ const fs = [];
+ const bundles = l10nReg.generateBundlesSync(["en-US"], fs);
+
+ const done = (await bundles.next()).done;
+
+ equal(done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test tests generation of a proper context for a single
+ * source scenario
+ */
+add_task(async function test_methods_calling() {
+ const fs = [
+ { path: "/localization/en-US/browser/menu.ftl", source: "key = Value" }
+ ];
+ const source = L10nFileSource.createMock("test", "", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const bundles = l10nReg.generateBundles(["en-US"], ["/browser/menu.ftl"]);
+
+ const bundle = (await bundles.next()).value;
+
+ equal(bundle.hasMessage("key"), true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that the public methods return expected values
+ * for the single source scenario
+ */
+add_task(async function test_has_one_source() {
+ const fs = [
+ {path: "./app/data/locales/en-US/test.ftl", source: "key = value en-US"}
+ ];
+ let oneSource = L10nFileSource.createMock("app", "", ["en-US"], "./app/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource]);
+
+
+ // has one source
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("app"), true);
+
+
+ // returns a single context
+
+ let bundles = l10nReg.generateBundles(["en-US"], ["test.ftl"]);
+ let bundle0 = (await bundles.next()).value;
+ equal(bundle0.hasMessage("key"), true);
+
+ equal((await bundles.next()).done, true);
+
+
+ // returns no contexts for missing locale
+
+ bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that public methods return expected values
+ * for the dual source scenario.
+ */
+add_task(async function test_has_two_sources() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "key = platform value" },
+ { path: "./app/data/locales/pl/test.ftl", source: "key = app value" }
+ ];
+ let oneSource = L10nFileSource.createMock("platform", "", ["en-US"], "./platform/data/locales/{locale}/", fs);
+ let secondSource = L10nFileSource.createMock("app", "", ["pl"], "./app/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource, secondSource]);
+
+ // has two sources
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("app"), true);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns correct contexts for en-US
+
+ let bundles = l10nReg.generateBundles(["en-US"], ["test.ftl"]);
+ let bundle0 = (await bundles.next()).value;
+
+ equal(bundle0.hasMessage("key"), true);
+ let msg = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg.value), "platform value");
+
+ equal((await bundles.next()).done, true);
+
+
+ // returns correct contexts for [pl, en-US]
+
+ bundles = l10nReg.generateBundles(["pl", "en-US"], ["test.ftl"]);
+ bundle0 = (await bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "app value");
+
+ let bundle1 = (await bundles.next()).value;
+ equal(bundle1.locales[0], "en-US");
+ equal(bundle1.hasMessage("key"), true);
+ let msg1 = bundle1.getMessage("key");
+ equal(bundle1.formatPattern(msg1.value), "platform value");
+
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that behavior specific to the L10nFileSource
+ * works correctly.
+ *
+ * In particular it tests that L10nFileSource correctly returns
+ * missing files as `false` instead of `undefined`.
+ */
+add_task(function test_indexed() {
+ let oneSource = new L10nFileSource("langpack-pl", "app", ["pl"], "/data/locales/{locale}/", {}, [
+ "/data/locales/pl/test.ftl",
+ ]);
+ equal(oneSource.hasFile("pl", "test.ftl"), "present");
+ equal(oneSource.hasFile("pl", "missing.ftl"), "missing");
+});
+
+/**
+ * This test checks if the correct order of contexts is used for
+ * scenarios where a new file source is added on top of the default one.
+ */
+add_task(async function test_override() {
+ const fs = [
+ { path: "/app/data/locales/pl/test.ftl", source: "key = value" },
+ { path: "/data/locales/pl/test.ftl", source: "key = addon value"},
+ ];
+ let fileSource = L10nFileSource.createMock("app", "", ["pl"], "/app/data/locales/{locale}/", fs);
+ let oneSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", fs);
+ l10nReg.registerSources([fileSource, oneSource]);
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("langpack-pl"), true);
+
+ let bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+ let bundle0 = (await bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "addon value");
+
+ let bundle1 = (await bundles.next()).value;
+ equal(bundle1.locales[0], "pl");
+ equal(bundle1.hasMessage("key"), true);
+ let msg1 = bundle1.getMessage("key");
+ equal(bundle1.formatPattern(msg1.value), "value");
+
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that new contexts are returned
+ * after source update.
+ */
+add_task(async function test_updating() {
+ const fs = [
+ { path: "/data/locales/pl/test.ftl", source: "key = value" }
+ ];
+ let oneSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource]);
+
+ let bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+ let bundle0 = (await bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "value");
+
+
+ const newSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", [
+ { path: "/data/locales/pl/test.ftl", source: "key = new value" }
+ ]);
+ l10nReg.updateSources([newSource]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+ bundle0 = (await bundles.next()).value;
+ msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "new value");
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that generated contexts return correct values
+ * after sources are being removed.
+ */
+add_task(async function test_removing() {
+ const fs = [
+ { path: "/app/data/locales/pl/test.ftl", source: "key = value" },
+ { path: "/data/locales/pl/test.ftl", source: "key = addon value" },
+ ];
+
+ let fileSource = L10nFileSource.createMock("app", "", ["pl"], "/app/data/locales/{locale}/", fs);
+ let oneSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", fs);
+ l10nReg.registerSources([fileSource, oneSource]);
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("langpack-pl"), true);
+
+ let bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+ let bundle0 = (await bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "addon value");
+
+ let bundle1 = (await bundles.next()).value;
+ equal(bundle1.locales[0], "pl");
+ equal(bundle1.hasMessage("key"), true);
+ let msg1 = bundle1.getMessage("key");
+ equal(bundle1.formatPattern(msg1.value), "value");
+
+ equal((await bundles.next()).done, true);
+
+ // Remove langpack
+
+ l10nReg.removeSources(["langpack-pl"]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("langpack-pl"), false);
+
+ bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+ bundle0 = (await bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "value");
+
+ equal((await bundles.next()).done, true);
+
+ // Remove app source
+
+ l10nReg.removeSources(["app"]);
+
+ equal(l10nReg.getSourceNames().length, 0);
+
+ bundles = l10nReg.generateBundles(["pl"], ["test.ftl"]);
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that the logic works correctly when there's a missing
+ * file in the FileSource scenario.
+ */
+add_task(async function test_missing_file() {
+ const fs = [
+ { path: "./app/data/locales/en-US/test.ftl", source: "key = value en-US" },
+ { path: "./platform/data/locales/en-US/test.ftl", source: "key = value en-US" },
+ { path: "./platform/data/locales/en-US/test2.ftl", source: "key2 = value2 en-US" },
+ ];
+ let oneSource = L10nFileSource.createMock("app", "", ["en-US"], "./app/data/locales/{locale}/", fs);
+ let twoSource = L10nFileSource.createMock("platform", "", ["en-US"], "./platform/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource, twoSource]);
+
+ // has two sources
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("app"), true);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns a single context
+
+ let bundles = l10nReg.generateBundles(["en-US"], ["test.ftl", "test2.ftl"]);
+
+ // First permutation:
+ // [platform, platform] - both present
+ let bundle1 = (await bundles.next());
+ equal(bundle1.value.hasMessage("key"), true);
+
+ // Second permutation skipped:
+ // [platform, app] - second missing
+ // Third permutation:
+ // [app, platform] - both present
+ let bundle2 = (await bundles.next());
+ equal(bundle2.value.hasMessage("key"), true);
+
+ // Fourth permutation skipped:
+ // [app, app] - second missing
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+add_task(async function test_hasSource() {
+ equal(l10nReg.hasSource("nonsense"), false, "Non-existing source doesn't exist");
+ equal(l10nReg.hasSource("app"), false, "hasSource returns true before registering a source");
+ let oneSource = new L10nFileSource("app", "app", ["en-US"], "/{locale}/");
+ l10nReg.registerSources([oneSource]);
+ equal(l10nReg.hasSource("app"), true, "hasSource returns true after registering a source");
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that we handle correctly a scenario where a source
+ * is being removed while the iterator operates.
+ */
+add_task(async function test_remove_source_mid_iter_cycle() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "key = platform value" },
+ { path: "./app/data/locales/pl/test.ftl", source: "key = app value" },
+ ];
+ let oneSource = L10nFileSource.createMock("platform", "", ["en-US"], "./platform/data/locales/{locale}/", fs);
+ let secondSource = L10nFileSource.createMock("app", "", ["pl"], "./app/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource, secondSource]);
+
+ let bundles = l10nReg.generateBundles(["en-US", "pl"], ["test.ftl"]);
+
+ let bundle0 = await bundles.next();
+
+ // The registry has a copy of the file sources, so it will be unaffected.
+ l10nReg.removeSources(["app"]);
+
+ let bundle1 = await bundles.next();
+
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+add_task(async function test_metasources() {
+ let fs = [
+ { path: "/localization/en-US/browser/menu1.ftl", source: "key1 = Value" },
+ { path: "/localization/en-US/browser/menu2.ftl", source: "key2 = Value" },
+ { path: "/localization/en-US/browser/menu3.ftl", source: "key3 = Value" },
+ { path: "/localization/en-US/browser/menu4.ftl", source: "key4 = Value" },
+ { path: "/localization/en-US/browser/menu5.ftl", source: "key5 = Value" },
+ { path: "/localization/en-US/browser/menu6.ftl", source: "key6 = Value" },
+ { path: "/localization/en-US/browser/menu7.ftl", source: "key7 = Value" },
+ { path: "/localization/en-US/browser/menu8.ftl", source: "key8 = Value" },
+ ];
+
+ const browser = L10nFileSource.createMock("browser", "app", ["en-US"], "/localization/{locale}", fs);
+ const toolkit = L10nFileSource.createMock("toolkit", "app", ["en-US"], "/localization/{locale}", fs);
+ const browser2 = L10nFileSource.createMock("browser2", "langpack", ["en-US"], "/localization/{locale}", fs);
+ const toolkit2 = L10nFileSource.createMock("toolkit2", "langpack", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([toolkit, browser, toolkit2, browser2]);
+
+ let res = [
+ "/browser/menu1.ftl",
+ "/browser/menu2.ftl",
+ "/browser/menu3.ftl",
+ "/browser/menu4.ftl",
+ "/browser/menu5.ftl",
+ "/browser/menu6.ftl",
+ "/browser/menu7.ftl",
+ {path: "/browser/menu8.ftl", optional: false},
+ ];
+
+ const bundles = l10nReg.generateBundles(["en-US"], res);
+
+ let nbundles = 0;
+ while (!(await bundles.next()).done) {
+ nbundles += 1;
+ }
+
+ // If metasources are working properly, we'll generate 2^8 = 256 bundles for
+ // each metasource giving 512 bundles in total. Otherwise, we generate
+ // 4^8 = 65536 bundles.
+ equal(nbundles, 512);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that when a required resource is missing for a locale,
+ * we do not produce a bundle for that locale.
+ */
+add_task(async function test_missing_required_resource() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "test-key = en-US value" },
+ { path: "./platform/data/locales/pl/missing-in-en-US.ftl", source: "missing-key = pl value" },
+ { path: "./platform/data/locales/pl/test.ftl", source: "test-key = pl value" },
+ ];
+ let source = L10nFileSource.createMock("platform", "", ["en-US", "pl"], "./platform/data/locales/{locale}/", fs);
+ l10nReg.registerSources([source]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns correct contexts for [en-US, pl]
+
+ let bundles = l10nReg.generateBundlesSync(["en-US", "pl"], ["test.ftl", "missing-in-en-US.ftl"]);
+ let bundle0 = (await bundles.next()).value;
+
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("test-key"), true);
+ equal(bundle0.hasMessage("missing-key"), true);
+
+ let msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "pl value");
+
+ let msg1 = bundle0.getMessage("missing-key");
+ equal(bundle0.formatPattern(msg1.value), "pl value");
+
+ equal((await bundles.next()).done, true);
+
+
+ // returns correct contexts for [pl, en-US]
+
+ bundles = l10nReg.generateBundlesSync(["pl", "en-US"], ["test.ftl", {path: "missing-in-en-US.ftl", optional: false}]);
+ bundle0 = (await bundles.next()).value;
+
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("test-key"), true);
+
+ msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "pl value");
+
+ msg1 = bundle0.getMessage("missing-key");
+ equal(bundle0.formatPattern(msg1.value), "pl value");
+
+ equal((await bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that when an optional resource is missing, we continue
+ * to produce a bundle for that locale. The bundle will have missing entries
+ * with regard to the missing optional resource.
+ */
+add_task(async function test_missing_optional_resource() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "test-key = en-US value" },
+ { path: "./platform/data/locales/pl/missing-in-en-US.ftl", source: "missing-key = pl value" },
+ { path: "./platform/data/locales/pl/test.ftl", source: "test-key = pl value" },
+ ];
+ let source = L10nFileSource.createMock("platform", "", ["en-US", "pl"], "./platform/data/locales/{locale}/", fs);
+ l10nReg.registerSources([source]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns correct contexts for [en-US, pl]
+
+ let bundles = l10nReg.generateBundlesSync(["en-US", "pl"], ["test.ftl", { path: "missing-in-en-US.ftl", optional: true }]);
+ let bundle0 = (await bundles.next()).value;
+
+ equal(bundle0.locales[0], "en-US");
+ equal(bundle0.hasMessage("test-key"), true);
+ equal(bundle0.hasMessage("missing-key"), false);
+
+ let msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "en-US value");
+
+ equal(bundle0.getMessage("missing-key"), null);
+
+ let bundle1 = (await bundles.next()).value;
+
+ equal(bundle1.locales[0], "pl");
+ equal(bundle1.hasMessage("test-key"), true);
+ equal(bundle1.hasMessage("missing-key"), true);
+
+ msg0 = bundle1.getMessage("test-key");
+ equal(bundle1.formatPattern(msg0.value), "pl value");
+
+ msg1 = bundle1.getMessage("missing-key");
+ equal(bundle1.formatPattern(msg1.value), "pl value");
+
+ equal((await bundles.next()).done, true);
+
+ // returns correct contexts for [pl, en-US]
+
+ bundles = l10nReg.generateBundlesSync(["pl", "en-US"], ["test.ftl", { path: "missing-in-en-US.ftl", optional: true }]);
+ bundle0 = (await bundles.next()).value;
+
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("test-key"), true);
+ equal(bundle0.hasMessage("missing-key"), true);
+
+ msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "pl value");
+
+ msg1 = bundle0.getMessage("missing-key");
+ equal(bundle0.formatPattern(msg1.value), "pl value");
+
+ bundle1 = (await bundles.next()).value;
+
+ equal(bundle1.locales[0], "en-US");
+ equal(bundle1.hasMessage("test-key"), true);
+ equal(bundle1.hasMessage("missing-key"), false);
+
+ msg0 = bundle1.getMessage("test-key");
+ equal(bundle1.formatPattern(msg0.value), "en-US value");
+
+ equal(bundle1.getMessage("missing-key"), null);
+
+ // cleanup
+ l10nReg.clearSources();
+});
diff --git a/intl/l10n/test/test_l10nregistry_fuzzed.js b/intl/l10n/test/test_l10nregistry_fuzzed.js
new file mode 100644
index 0000000000..f99c3b61e5
--- /dev/null
+++ b/intl/l10n/test/test_l10nregistry_fuzzed.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test is a fuzzing test for the L10nRegistry API. It was written to find
+ * a hard to reproduce bug in the L10nRegistry code. If it fails, place the seed
+ * from the failing run in the code directly below to make it consistently reproducible.
+ */
+let seed = Math.floor(Math.random() * 1e9);
+
+console.log(`Starting a fuzzing run with seed: ${seed}.`);
+console.log("To reproduce this test locally, re-run it locally with:");
+console.log(`let seed = ${seed};`);
+
+/**
+ * A simple non-robust psuedo-random number generator.
+ *
+ * It is implemented using a Lehmer random number generator.
+ * https://en.wikipedia.org/wiki/16,807
+ *
+ * @returns {number} Ranged [0, 1)
+ */
+function prng() {
+ const multiplier = 16807;
+ const prime = 2147483647;
+ seed = seed * multiplier % prime
+ return (seed - 1) / prime
+}
+
+/**
+ * Generate a name like "mock-dmsxfodrqboljmxdeayt".
+ * @returns {string}
+ */
+function generateRandomName() {
+ let name = 'mock-'
+ const letters = "abcdefghijklmnopqrstuvwxyz";
+ for (let i = 0; i < 20; i++) {
+ name += letters[Math.floor(prng() * letters.length)];
+ }
+ return name;
+}
+
+/**
+ * Picks one item from an array.
+ *
+ * @param {Array<T>}
+ * @returns {T}
+ */
+function pickOne(list) {
+ return list[Math.floor(prng() * list.length)]
+}
+
+/**
+ * Picks a random subset from an array.
+ *
+ * @param {Array<T>}
+ * @returns {Array<T>}
+ */
+function pickN(list, count) {
+ list = list.slice();
+ const result = [];
+ for (let i = 0; i < count && i < list.length; i++) {
+ // Pick a random item.
+ const index = Math.floor(prng() * list.length);
+
+ // Swap item to the end.
+ const a = list[index];
+ const b = list[list.length - 1];
+ list[index] = b;
+ list[list.length - 1] = a
+
+ // Now that the random item is on the end, pop it off and add it to the results.
+ result.push(list.pop());
+ }
+
+ return result
+}
+
+/**
+ * Generate a random number
+ * @param {number} min
+ * @param {number} max
+ * @returns {number}
+ */
+function random(min, max) {
+ const delta = max - min;
+ return min + delta * prng();
+}
+
+/**
+ * Generate a random number generator with a distribution more towards the lower end.
+ * @param {number} min
+ * @param {number} max
+ * @returns {number}
+ */
+function randomPow(min, max) {
+ const delta = max - min;
+ const r = prng()
+ return min + delta * r * r;
+}
+
+add_task(async function test_fuzzing_sources() {
+ const iterations = 100;
+ const maxSources = 10;
+
+ const metasources = ["app", "langpack", ""];
+ const availableLocales = ["en", "en-US", "pl", "en-CA", "es-AR", "es-ES"];
+
+ const l10nReg = new L10nRegistry();
+
+ for (let i = 0; i < iterations; i++) {
+ console.log("----------------------------------------------------------------------");
+ console.log("Iteration", i);
+ let sourceCount = randomPow(0, maxSources);
+
+ const mocks = [];
+ const fs = [];
+
+ const locales = new Set();
+ const filenames = new Set();
+
+ for (let j = 0; j < sourceCount; j++) {
+ const locale = pickOne(availableLocales);
+ locales.add(locale);
+
+ let metasource = pickOne(metasources);
+ if (metasource === "langpack") {
+ metasource = `${metasource}-${locale}`
+ }
+
+ const dir = generateRandomName();
+ const filename = generateRandomName() + j + ".ftl";
+ const path = `${dir}/${locale}/${filename}`
+ const name = metasource || "app";
+ const source = "key = value";
+
+ filenames.add(filename);
+
+ console.log("Add source", { name, metasource, path, source });
+ fs.push({ path, source });
+
+ mocks.push([
+ name, // name
+ metasource, // metasource,
+ [locale], // locales,
+ dir + "/{locale}/",
+ fs
+ ])
+ }
+
+ l10nReg.registerSources(mocks.map(args => L10nFileSource.createMock(...args)));
+
+ const bundleLocales = pickN([...locales], random(1, 4));
+ const bundleFilenames = pickN([...filenames], random(1, 10));
+
+ console.log("generateBundles", {bundleLocales, bundleFilenames});
+ const bundles = l10nReg.generateBundles(
+ bundleLocales,
+ bundleFilenames
+ );
+
+ function next() {
+ console.log("Getting next bundle");
+ const bundle = bundles.next()
+ console.log("Next bundle obtained", bundle);
+ return bundle;
+ }
+
+ const ops = [
+ // Increase the frequency of next being called.
+ next,
+ next,
+ next,
+ () => {
+ const newMocks = [];
+ for (const mock of pickN(mocks, random(0, 3))) {
+ const newMock = mock.slice();
+ newMocks.push(newMock)
+ }
+ console.log("l10nReg.updateSources");
+ l10nReg.updateSources(newMocks.map(mock => L10nFileSource.createMock(...mock)));
+ },
+ () => {
+ console.log("l10nReg.clearSources");
+ l10nReg.clearSources();
+ }
+ ];
+
+ console.log("Start the operation loop");
+ while (true) {
+ console.log("Next operation");
+ const op = pickOne(ops);
+ const result = await op();
+ if (result?.done) {
+ // The iterator completed.
+ break;
+ }
+ }
+
+ console.log("Clear sources");
+ l10nReg.clearSources();
+ }
+
+ ok(true, "The L10nRegistry fuzzing did not crash.")
+});
diff --git a/intl/l10n/test/test_l10nregistry_sync.js b/intl/l10n/test/test_l10nregistry_sync.js
new file mode 100644
index 0000000000..178a24ff6d
--- /dev/null
+++ b/intl/l10n/test/test_l10nregistry_sync.js
@@ -0,0 +1,536 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {setTimeout} = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");
+
+const l10nReg = new L10nRegistry();
+
+add_task(function test_methods_presence() {
+ equal(typeof l10nReg.generateBundles, "function");
+ equal(typeof l10nReg.generateBundlesSync, "function");
+ equal(typeof l10nReg.getAvailableLocales, "function");
+ equal(typeof l10nReg.registerSources, "function");
+ equal(typeof l10nReg.removeSources, "function");
+ equal(typeof l10nReg.updateSources, "function");
+});
+
+/**
+ * Test that passing empty resourceIds list works.
+ */
+add_task(function test_empty_resourceids() {
+ const fs = [];
+
+ const source = L10nFileSource.createMock("test", "", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const bundles = l10nReg.generateBundlesSync(["en-US"], []);
+
+ const done = (bundles.next()).done;
+
+ equal(done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * Test that passing empty sources list works.
+ */
+add_task(function test_empty_sources() {
+ const fs = [];
+ const bundles = l10nReg.generateBundlesSync(["en-US"], fs);
+
+ const done = (bundles.next()).done;
+
+ equal(done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test tests generation of a proper context for a single
+ * source scenario
+ */
+add_task(function test_methods_calling() {
+ const fs = [
+ { path: "/localization/en-US/browser/menu.ftl", source: "key = Value" }
+ ];
+ const source = L10nFileSource.createMock("test", "", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const bundles = l10nReg.generateBundlesSync(["en-US"], ["/browser/menu.ftl"]);
+
+ const bundle = (bundles.next()).value;
+
+ equal(bundle.hasMessage("key"), true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that the public methods return expected values
+ * for the single source scenario
+ */
+add_task(function test_has_one_source() {
+ const fs = [
+ {path: "./app/data/locales/en-US/test.ftl", source: "key = value en-US"}
+ ];
+ let oneSource = L10nFileSource.createMock("app", "", ["en-US"], "./app/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource]);
+
+
+ // has one source
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("app"), true);
+
+
+ // returns a single context
+
+ let bundles = l10nReg.generateBundlesSync(["en-US"], ["test.ftl"]);
+ let bundle0 = (bundles.next()).value;
+ equal(bundle0.hasMessage("key"), true);
+
+ equal((bundles.next()).done, true);
+
+
+ // returns no contexts for missing locale
+
+ bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that public methods return expected values
+ * for the dual source scenario.
+ */
+add_task(function test_has_two_sources() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "key = platform value" },
+ { path: "./app/data/locales/pl/test.ftl", source: "key = app value" }
+ ];
+ let oneSource = L10nFileSource.createMock("platform", "", ["en-US"], "./platform/data/locales/{locale}/", fs);
+ let secondSource = L10nFileSource.createMock("app", "", ["pl"], "./app/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource, secondSource]);
+
+ // has two sources
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("app"), true);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns correct contexts for en-US
+
+ let bundles = l10nReg.generateBundlesSync(["en-US"], ["test.ftl"]);
+ let bundle0 = (bundles.next()).value;
+
+ equal(bundle0.hasMessage("key"), true);
+ let msg = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg.value), "platform value");
+
+ equal((bundles.next()).done, true);
+
+
+ // returns correct contexts for [pl, en-US]
+
+ bundles = l10nReg.generateBundlesSync(["pl", "en-US"], ["test.ftl"]);
+ bundle0 = (bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "app value");
+
+ let bundle1 = (bundles.next()).value;
+ equal(bundle1.locales[0], "en-US");
+ equal(bundle1.hasMessage("key"), true);
+ let msg1 = bundle1.getMessage("key");
+ equal(bundle1.formatPattern(msg1.value), "platform value");
+
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test checks if the correct order of contexts is used for
+ * scenarios where a new file source is added on top of the default one.
+ */
+add_task(function test_override() {
+ const fs = [
+ { path: "/app/data/locales/pl/test.ftl", source: "key = value" },
+ { path: "/data/locales/pl/test.ftl", source: "key = addon value"},
+ ];
+ let fileSource = L10nFileSource.createMock("app", "", ["pl"], "/app/data/locales/{locale}/", fs);
+ let oneSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", fs);
+ l10nReg.registerSources([fileSource, oneSource]);
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("langpack-pl"), true);
+
+ let bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+ let bundle0 = (bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "addon value");
+
+ let bundle1 = (bundles.next()).value;
+ equal(bundle1.locales[0], "pl");
+ equal(bundle1.hasMessage("key"), true);
+ let msg1 = bundle1.getMessage("key");
+ equal(bundle1.formatPattern(msg1.value), "value");
+
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that new contexts are returned
+ * after source update.
+ */
+add_task(function test_updating() {
+ const fs = [
+ { path: "/data/locales/pl/test.ftl", source: "key = value" }
+ ];
+ let oneSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource]);
+
+ let bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+ let bundle0 = (bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "value");
+
+
+ const newSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", [
+ { path: "/data/locales/pl/test.ftl", source: "key = new value" }
+ ]);
+ l10nReg.updateSources([newSource]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+ bundle0 = (bundles.next()).value;
+ msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "new value");
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that generated contexts return correct values
+ * after sources are being removed.
+ */
+add_task(function test_removing() {
+ const fs = [
+ { path: "/app/data/locales/pl/test.ftl", source: "key = value" },
+ { path: "/data/locales/pl/test.ftl", source: "key = addon value" },
+ ];
+
+ let fileSource = L10nFileSource.createMock("app", "", ["pl"], "/app/data/locales/{locale}/", fs);
+ let oneSource = L10nFileSource.createMock("langpack-pl", "", ["pl"], "/data/locales/{locale}/", fs);
+ l10nReg.registerSources([fileSource, oneSource]);
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("langpack-pl"), true);
+
+ let bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+ let bundle0 = (bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ let msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "addon value");
+
+ let bundle1 = (bundles.next()).value;
+ equal(bundle1.locales[0], "pl");
+ equal(bundle1.hasMessage("key"), true);
+ let msg1 = bundle1.getMessage("key");
+ equal(bundle1.formatPattern(msg1.value), "value");
+
+ equal((bundles.next()).done, true);
+
+ // Remove langpack
+
+ l10nReg.removeSources(["langpack-pl"]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("langpack-pl"), false);
+
+ bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+ bundle0 = (bundles.next()).value;
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("key"), true);
+ msg0 = bundle0.getMessage("key");
+ equal(bundle0.formatPattern(msg0.value), "value");
+
+ equal((bundles.next()).done, true);
+
+ // Remove app source
+
+ l10nReg.removeSources(["app"]);
+
+ equal(l10nReg.getSourceNames().length, 0);
+
+ bundles = l10nReg.generateBundlesSync(["pl"], ["test.ftl"]);
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that the logic works correctly when there's a missing
+ * file in the FileSource scenario.
+ */
+add_task(function test_missing_file() {
+ const fs = [
+ { path: "./app/data/locales/en-US/test.ftl", source: "key = value en-US" },
+ { path: "./platform/data/locales/en-US/test.ftl", source: "key = value en-US" },
+ { path: "./platform/data/locales/en-US/test2.ftl", source: "key2 = value2 en-US" },
+ ];
+ let oneSource = L10nFileSource.createMock("app", "", ["en-US"], "./app/data/locales/{locale}/", fs);
+ let twoSource = L10nFileSource.createMock("platform", "", ["en-US"], "./platform/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource, twoSource]);
+
+ // has two sources
+
+ equal(l10nReg.getSourceNames().length, 2);
+ equal(l10nReg.hasSource("app"), true);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns a single context
+
+ let bundles = l10nReg.generateBundlesSync(["en-US"], ["test.ftl", "test2.ftl"]);
+
+ // First permutation:
+ // [platform, platform] - both present
+ let bundle1 = (bundles.next());
+ equal(bundle1.value.hasMessage("key"), true);
+
+ // Second permutation skipped:
+ // [platform, app] - second missing
+ // Third permutation:
+ // [app, platform] - both present
+ let bundle2 = (bundles.next());
+ equal(bundle2.value.hasMessage("key"), true);
+
+ // Fourth permutation skipped:
+ // [app, app] - second missing
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that we handle correctly a scenario where a source
+ * is being removed while the iterator operates.
+ */
+add_task(function test_remove_source_mid_iter_cycle() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "key = platform value" },
+ { path: "./app/data/locales/pl/test.ftl", source: "key = app value" },
+ ];
+ let oneSource = L10nFileSource.createMock("platform", "", ["en-US"], "./platform/data/locales/{locale}/", fs);
+ let secondSource = L10nFileSource.createMock("app", "", ["pl"], "./app/data/locales/{locale}/", fs);
+ l10nReg.registerSources([oneSource, secondSource]);
+
+ let bundles = l10nReg.generateBundlesSync(["en-US", "pl"], ["test.ftl"]);
+
+ let bundle0 = bundles.next();
+
+ l10nReg.removeSources(["app"]);
+
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+add_task(async function test_metasources() {
+ let fs = [
+ { path: "/localization/en-US/browser/menu1.ftl", source: "key1 = Value" },
+ { path: "/localization/en-US/browser/menu2.ftl", source: "key2 = Value" },
+ { path: "/localization/en-US/browser/menu3.ftl", source: "key3 = Value" },
+ { path: "/localization/en-US/browser/menu4.ftl", source: "key4 = Value" },
+ { path: "/localization/en-US/browser/menu5.ftl", source: "key5 = Value" },
+ { path: "/localization/en-US/browser/menu6.ftl", source: "key6 = Value" },
+ { path: "/localization/en-US/browser/menu7.ftl", source: "key7 = Value" },
+ { path: "/localization/en-US/browser/menu8.ftl", source: "key8 = Value" },
+ ];
+
+ const browser = L10nFileSource.createMock("browser", "app", ["en-US"], "/localization/{locale}", fs);
+ const toolkit = L10nFileSource.createMock("toolkit", "app", ["en-US"], "/localization/{locale}", fs);
+ const browser2 = L10nFileSource.createMock("browser2", "langpack", ["en-US"], "/localization/{locale}", fs);
+ const toolkit2 = L10nFileSource.createMock("toolkit2", "langpack", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([toolkit, browser, toolkit2, browser2]);
+
+ let res = [
+ "/browser/menu1.ftl",
+ "/browser/menu2.ftl",
+ "/browser/menu3.ftl",
+ "/browser/menu4.ftl",
+ "/browser/menu5.ftl",
+ "/browser/menu6.ftl",
+ "/browser/menu7.ftl",
+ {path: "/browser/menu8.ftl", optional: false},
+ ];
+
+ const bundles = l10nReg.generateBundlesSync(["en-US"], res);
+
+ let nbundles = 0;
+ while (!bundles.next().done) {
+ nbundles += 1;
+ }
+
+ // If metasources are working properly, we'll generate 2^8 = 256 bundles for
+ // each metasource giving 512 bundles in total. Otherwise, we generate
+ // 4^8 = 65536 bundles.
+ equal(nbundles, 512);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that when a required resource is missing for a locale,
+ * we do not produce a bundle for that locale.
+ */
+add_task(function test_missing_required_resource() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "test-key = en-US value" },
+ { path: "./platform/data/locales/pl/missing-in-en-US.ftl", source: "missing-key = pl value" },
+ { path: "./platform/data/locales/pl/test.ftl", source: "test-key = pl value" },
+ ];
+ let source = L10nFileSource.createMock("platform", "", ["en-US", "pl"], "./platform/data/locales/{locale}/", fs);
+ l10nReg.registerSources([source]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns correct contexts for [en-US, pl]
+
+ let bundles = l10nReg.generateBundlesSync(["en-US", "pl"], ["test.ftl", "missing-in-en-US.ftl"]);
+ let bundle0 = (bundles.next()).value;
+
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("test-key"), true);
+ equal(bundle0.hasMessage("missing-key"), true);
+
+ let msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "pl value");
+
+ let msg1 = bundle0.getMessage("missing-key");
+ equal(bundle0.formatPattern(msg1.value), "pl value");
+
+ equal((bundles.next()).done, true);
+
+
+ // returns correct contexts for [pl, en-US]
+
+ bundles = l10nReg.generateBundlesSync(["pl", "en-US"], ["test.ftl", {path: "missing-in-en-US.ftl", optional: false}]);
+ bundle0 = (bundles.next()).value;
+
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("test-key"), true);
+
+ msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "pl value");
+
+ msg1 = bundle0.getMessage("missing-key");
+ equal(bundle0.formatPattern(msg1.value), "pl value");
+
+ equal((bundles.next()).done, true);
+
+ // cleanup
+ l10nReg.clearSources();
+});
+
+/**
+ * This test verifies that when an optional resource is missing, we continue
+ * to produce a bundle for that locale. The bundle will have missing entries
+ * with regard to the missing optional resource.
+ */
+add_task(function test_missing_optional_resource() {
+ const fs = [
+ { path: "./platform/data/locales/en-US/test.ftl", source: "test-key = en-US value" },
+ { path: "./platform/data/locales/pl/missing-in-en-US.ftl", source: "missing-key = pl value" },
+ { path: "./platform/data/locales/pl/test.ftl", source: "test-key = pl value" },
+ ];
+ let source = L10nFileSource.createMock("platform", "", ["en-US", "pl"], "./platform/data/locales/{locale}/", fs);
+ l10nReg.registerSources([source]);
+
+ equal(l10nReg.getSourceNames().length, 1);
+ equal(l10nReg.hasSource("platform"), true);
+
+
+ // returns correct contexts for [en-US, pl]
+
+ let bundles = l10nReg.generateBundlesSync(["en-US", "pl"], ["test.ftl", { path: "missing-in-en-US.ftl", optional: true }]);
+ let bundle0 = (bundles.next()).value;
+
+ equal(bundle0.locales[0], "en-US");
+ equal(bundle0.hasMessage("test-key"), true);
+ equal(bundle0.hasMessage("missing-key"), false);
+
+ let msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "en-US value");
+
+ equal(bundle0.getMessage("missing-key"), null);
+
+ let bundle1 = (bundles.next()).value;
+
+ equal(bundle1.locales[0], "pl");
+ equal(bundle1.hasMessage("test-key"), true);
+ equal(bundle1.hasMessage("missing-key"), true);
+
+ msg0 = bundle1.getMessage("test-key");
+ equal(bundle1.formatPattern(msg0.value), "pl value");
+
+ msg1 = bundle1.getMessage("missing-key");
+ equal(bundle1.formatPattern(msg1.value), "pl value");
+
+ equal((bundles.next()).done, true);
+
+ // returns correct contexts for [pl, en-US]
+
+ bundles = l10nReg.generateBundlesSync(["pl", "en-US"], ["test.ftl", { path: "missing-in-en-US.ftl", optional: true }]);
+ bundle0 = (bundles.next()).value;
+
+ equal(bundle0.locales[0], "pl");
+ equal(bundle0.hasMessage("test-key"), true);
+ equal(bundle0.hasMessage("missing-key"), true);
+
+ msg0 = bundle0.getMessage("test-key");
+ equal(bundle0.formatPattern(msg0.value), "pl value");
+
+ msg1 = bundle0.getMessage("missing-key");
+ equal(bundle0.formatPattern(msg1.value), "pl value");
+
+ bundle1 = (bundles.next()).value;
+
+ equal(bundle1.locales[0], "en-US");
+ equal(bundle1.hasMessage("test-key"), true);
+ equal(bundle1.hasMessage("missing-key"), false);
+
+ msg0 = bundle1.getMessage("test-key");
+ equal(bundle1.formatPattern(msg0.value), "en-US value");
+
+ equal(bundle1.getMessage("missing-key"), null);
+
+ // cleanup
+ l10nReg.clearSources();
+});
diff --git a/intl/l10n/test/test_localization.js b/intl/l10n/test/test_localization.js
new file mode 100644
index 0000000000..1b89a0549a
--- /dev/null
+++ b/intl/l10n/test/test_localization.js
@@ -0,0 +1,319 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
+
+// Disable `xpc::IsInAutomation()` so incomplete locales do not generate
+// errors.
+Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+);
+
+add_task(function test_methods_presence() {
+ strictEqual(typeof Localization.prototype.formatValues, "function");
+ strictEqual(typeof Localization.prototype.formatMessages, "function");
+ strictEqual(typeof Localization.prototype.formatValue, "function");
+});
+
+add_task(async function test_methods_calling() {
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/de/browser/menu.ftl", source: `
+key-value1 = [de] Value2
+` },
+ { path: "/localization/en-US/browser/menu.ftl", source: `
+key-value1 = [en] Value2
+key-value2 = [en] Value3
+key-attr =
+ .label = [en] Label 3
+` },
+ ];
+ const originalRequested = Services.locale.requestedLocales;
+
+ const source = L10nFileSource.createMock("test", "app", ["de", "en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization([
+ "/browser/menu.ftl",
+ ], false, l10nReg, ["de", "en-US"]);
+
+ {
+ let values = await l10n.formatValues([
+ {id: "key-value1"},
+ {id: "key-value2"},
+ {id: "key-missing"},
+ {id: "key-attr"}
+ ]);
+
+ strictEqual(values[0], "[de] Value2");
+ strictEqual(values[1], "[en] Value3");
+ strictEqual(values[2], null);
+ strictEqual(values[3], null);
+ }
+
+ {
+ let values = await l10n.formatValues([
+ "key-value1",
+ "key-value2",
+ "key-missing",
+ "key-attr"
+ ]);
+
+ strictEqual(values[0], "[de] Value2");
+ strictEqual(values[1], "[en] Value3");
+ strictEqual(values[2], null);
+ strictEqual(values[3], null);
+ }
+
+ {
+ strictEqual(await l10n.formatValue("key-missing"), null);
+ strictEqual(await l10n.formatValue("key-value1"), "[de] Value2");
+ strictEqual(await l10n.formatValue("key-value2"), "[en] Value3");
+ strictEqual(await l10n.formatValue("key-attr"), null);
+ }
+
+ {
+ let messages = await l10n.formatMessages([
+ {id: "key-value1"},
+ {id: "key-missing"},
+ {id: "key-value2"},
+ {id: "key-attr"},
+ ]);
+
+ strictEqual(messages[0].value, "[de] Value2");
+ strictEqual(messages[1], null);
+ strictEqual(messages[2].value, "[en] Value3");
+ strictEqual(messages[3].value, null);
+ }
+});
+
+add_task(async function test_builtins() {
+ const l10nReg = new L10nRegistry();
+ const known_platforms = {
+ "linux": "linux",
+ "win": "windows",
+ "macosx": "macos",
+ "android": "android",
+ };
+
+ const fs = [
+ { path: "/localization/en-US/test.ftl", source: `
+key = { PLATFORM() ->
+ ${ Object.values(known_platforms).map(
+ name => ` [${ name }] ${ name.toUpperCase() } Value\n`).join("") }
+ *[other] OTHER Value
+ }` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization([
+ "/test.ftl",
+ ], false, l10nReg, ["en-US"]);
+
+ let values = await l10n.formatValues([{id: "key"}]);
+
+ ok(values[0].includes(
+ `${ known_platforms[AppConstants.platform].toUpperCase() } Value`));
+});
+
+add_task(async function test_add_remove_resourceIds() {
+ const l10nReg = new L10nRegistry();
+ const fs = [
+ { path: "/localization/en-US/browser/menu.ftl", source: "key1 = Value1" },
+ { path: "/localization/en-US/toolkit/menu.ftl", source: "key2 = Value2" },
+ ];
+
+
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization(["/browser/menu.ftl"], false, l10nReg, ["en-US"]);
+
+ let values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], null);
+
+ l10n.addResourceIds(["/toolkit/menu.ftl"]);
+
+ values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ values = await l10n.formatValues(["key1", {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ values = await l10n.formatValues([{id: "key1"}, "key2"]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ l10n.removeResourceIds(["/browser/menu.ftl"]);
+
+ values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], null);
+ strictEqual(values[1], "Value2");
+});
+
+add_task(async function test_switch_to_async() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/en-US/browser/menu.ftl", source: "key1 = Value1" },
+ { path: "/localization/en-US/toolkit/menu.ftl", source: "key2 = Value2" },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization(["/browser/menu.ftl"], true, l10nReg, ["en-US"]);
+
+ let values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], null);
+
+ l10n.setAsync();
+
+ Assert.throws(() => {
+ l10n.formatValuesSync([{ id: "key1" }, { id: "key2" }]);
+ }, /Can't use formatValuesSync when state is async./);
+
+ l10n.addResourceIds(["/toolkit/menu.ftl"]);
+
+ values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
+ let values2 = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
+
+ deepEqual(values, values2);
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ l10n.removeResourceIds(["/browser/menu.ftl"]);
+
+ values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], null);
+ strictEqual(values[1], "Value2");
+});
+
+/**
+ * This test verifies that when a required resource is missing,
+ * we fallback entirely to the next locale for all entries.
+ */
+add_task(async function test_format_from_missing_required_resource() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/de/browser/menu.ftl", source: `
+key-value = [de] Value1
+` },
+ { path: "/localization/de/browser/missing-in-en-US.ftl", source: `
+key-missing = [de] MissingValue
+` },
+ { path: "/localization/en-US/browser/menu.ftl", source: `
+key-value = [en] Value1
+` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["de", "en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ // returns correct contexts for [en-US, de]
+
+ let l10n = new Localization([
+ "/browser/menu.ftl",
+ "/browser/missing-in-en-US.ftl",
+ ], false, l10nReg, ["en-US", "de"]);
+
+ {
+ let values = await l10n.formatValues([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[de] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+
+ // returns correct contexts for [de, en-US]
+
+ l10n = new Localization([
+ "/browser/menu.ftl",
+ {path: "/browser/missing-in-en-US.ftl", optional: false},
+ ], false, l10nReg, ["de", "en-US"]);
+
+ {
+ let values = await l10n.formatValues([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[de] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+});
+
+/**
+ * This test verifies that when an optional resource is missing, we continue
+ * to populate entires from other resources in the same locale, and we only
+ * fallback entries from the missing optional resource to the next locale.
+ */
+add_task(async function test_format_from_missing_optional_resource() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/de/browser/menu.ftl", source: `
+key-value = [de] Value1
+` },
+ { path: "/localization/de/browser/missing-in-en-US.ftl", source: `
+key-missing = [de] MissingValue
+` },
+ { path: "/localization/en-US/browser/menu.ftl", source: `
+key-value = [en] Value1
+` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["de", "en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ // returns correct contexts for [en-US, de]
+
+ let l10n = new Localization([
+ {path: "/browser/menu.ftl", optional: false},
+ {path: "/browser/missing-in-en-US.ftl", optional: true},
+ ], false, l10nReg, ["en-US", "de"]);
+
+ {
+ let values = await l10n.formatValues([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[en] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+
+ // returns correct contexts for [de, en-US]
+
+ l10n = new Localization([
+ {path: "/browser/menu.ftl", optional: false},
+ {path: "/browser/missing-in-en-US.ftl", optional: true},
+ ], false, l10nReg, ["de", "en-US"]);
+
+ {
+ let values = await l10n.formatValues([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[de] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+});
diff --git a/intl/l10n/test/test_localization_sync.js b/intl/l10n/test/test_localization_sync.js
new file mode 100644
index 0000000000..fa2186bc07
--- /dev/null
+++ b/intl/l10n/test/test_localization_sync.js
@@ -0,0 +1,289 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
+
+// Disable `xpc::IsInAutomation()` so incomplete locales do not generate
+// errors.
+Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+);
+
+add_task(function test_methods_calling() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/de/browser/menu.ftl", source: `
+key-value1 = [de] Value2
+` },
+ { path: "/localization/en-US/browser/menu.ftl", source: `
+key-value1 = [en] Value2
+key-value2 = [en] Value3
+key-attr =
+ .label = [en] Label 3
+` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["de", "en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization([
+ "/browser/menu.ftl",
+ ], true, l10nReg, ["de", "en-US"]);
+
+
+ {
+ let values = l10n.formatValuesSync([
+ {id: "key-value1"},
+ {id: "key-value2"},
+ {id: "key-missing"},
+ {id: "key-attr"}
+ ]);
+
+ strictEqual(values[0], "[de] Value2");
+ strictEqual(values[1], "[en] Value3");
+ strictEqual(values[2], null);
+ strictEqual(values[3], null);
+ }
+
+ {
+ let values = l10n.formatValuesSync([
+ "key-value1",
+ "key-value2",
+ "key-missing",
+ "key-attr"
+ ]);
+
+ strictEqual(values[0], "[de] Value2");
+ strictEqual(values[1], "[en] Value3");
+ strictEqual(values[2], null);
+ strictEqual(values[3], null);
+ }
+
+ {
+ strictEqual(l10n.formatValueSync("key-missing"), null);
+ strictEqual(l10n.formatValueSync("key-value1"), "[de] Value2");
+ strictEqual(l10n.formatValueSync("key-value2"), "[en] Value3");
+ strictEqual(l10n.formatValueSync("key-attr"), null);
+ }
+
+ {
+ let messages = l10n.formatMessagesSync([
+ {id: "key-value1"},
+ {id: "key-missing"},
+ {id: "key-value2"},
+ {id: "key-attr"},
+ ]);
+
+ strictEqual(messages[0].value, "[de] Value2");
+ strictEqual(messages[1], null);
+ strictEqual(messages[2].value, "[en] Value3");
+ strictEqual(messages[3].value, null);
+ }
+});
+
+add_task(function test_builtins() {
+ const known_platforms = {
+ "linux": "linux",
+ "win": "windows",
+ "macosx": "macos",
+ "android": "android",
+ };
+
+ const fs = [
+ { path: "/localization/en-US/test.ftl", source: `
+key = { PLATFORM() ->
+ ${ Object.values(known_platforms).map(
+ name => ` [${ name }] ${ name.toUpperCase() } Value\n`).join("") }
+ *[other] OTHER Value
+ }` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ const l10nReg = new L10nRegistry();
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization([
+ "/test.ftl",
+ ], true, l10nReg, ["en-US"]);
+
+ let values = l10n.formatValuesSync([{id: "key"}]);
+
+ ok(values[0].includes(
+ `${ known_platforms[AppConstants.platform].toUpperCase() } Value`));
+});
+
+add_task(function test_add_remove_resourceIds() {
+ const fs = [
+ { path: "/localization/en-US/browser/menu.ftl", source: "key1 = Value1" },
+ { path: "/localization/en-US/toolkit/menu.ftl", source: "key2 = Value2" },
+ ];
+ const originalRequested = Services.locale.requestedLocales;
+
+ const source = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}", fs);
+ const l10nReg = new L10nRegistry();
+ l10nReg.registerSources([source]);
+
+ const l10n = new Localization(["/browser/menu.ftl"], true, l10nReg, ["en-US"]);
+
+ let values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], null);
+
+ l10n.addResourceIds(["/toolkit/menu.ftl"]);
+
+ values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ values = l10n.formatValuesSync(["key1", {id: "key2"}]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ values = l10n.formatValuesSync([{id: "key1"}, "key2"]);
+
+ strictEqual(values[0], "Value1");
+ strictEqual(values[1], "Value2");
+
+ l10n.removeResourceIds(["/browser/menu.ftl"]);
+
+ values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]);
+
+ strictEqual(values[0], null);
+ strictEqual(values[1], "Value2");
+});
+
+add_task(function test_calling_sync_methods_in_async_mode_fails() {
+ const l10n = new Localization(["/browser/menu.ftl"], false);
+
+ Assert.throws(() => {
+ l10n.formatValuesSync([{ id: "key1" }, { id: "key2" }]);
+ }, /Can't use formatValuesSync when state is async./);
+
+ Assert.throws(() => {
+ l10n.formatValueSync("key1");
+ }, /Can't use formatValueSync when state is async./);
+
+ Assert.throws(() => {
+ l10n.formatMessagesSync([{ id: "key1"}]);
+ }, /Can't use formatMessagesSync when state is async./);
+});
+
+/**
+ * This test verifies that when a required resource is missing,
+ * we fallback entirely to the next locale for all entries.
+ */
+add_task(function test_format_from_missing_required_resource() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/de/browser/menu.ftl", source: `
+key-value = [de] Value1
+` },
+ { path: "/localization/de/browser/missing-in-en-US.ftl", source: `
+key-missing = [de] MissingValue
+` },
+ { path: "/localization/en-US/browser/menu.ftl", source: `
+key-value = [en] Value1
+` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["de", "en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ // returns correct contexts for [en-US, de]
+
+ let l10n = new Localization([
+ "/browser/menu.ftl",
+ "/browser/missing-in-en-US.ftl",
+ ], true, l10nReg, ["en-US", "de"]);
+
+ {
+ let values = l10n.formatValuesSync([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[de] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+
+ // returns correct contexts for [de, en-US]
+
+ l10n = new Localization([
+ "/browser/menu.ftl",
+ {path: "/browser/missing-in-en-US.ftl", optional: false},
+ ], true, l10nReg, ["de", "en-US"]);
+
+ {
+ let values = l10n.formatValuesSync([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[de] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+});
+
+/**
+ * This test verifies that when an optional resource is missing
+ * we continue to populate entires from other resources in the same locale
+ * and only fallback entries from the optional resource to the next locale.
+ */
+add_task(function test_format_from_missing_optional_resource() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/de/browser/menu.ftl", source: `
+key-value = [de] Value1
+` },
+ { path: "/localization/de/browser/missing-in-en-US.ftl", source: `
+key-missing = [de] MissingValue
+` },
+ { path: "/localization/en-US/browser/menu.ftl", source: `
+key-value = [en] Value1
+` },
+ ];
+
+ const source = L10nFileSource.createMock("test", "app", ["de", "en-US"], "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+
+ // returns correct contexts for [en-US, de]
+
+ let l10n = new Localization([
+ {path: "/browser/menu.ftl", optional: false},
+ {path: "/browser/missing-in-en-US.ftl", optional: true},
+ ], true, l10nReg, ["en-US", "de"]);
+
+ {
+ let values = l10n.formatValuesSync([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[en] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+
+ // returns correct contexts for [de, en-US]
+
+ l10n = new Localization([
+ {path: "/browser/menu.ftl", optional: false},
+ {path: "/browser/missing-in-en-US.ftl", optional: true},
+ ], true, l10nReg, ["de", "en-US"]);
+
+ {
+ let values = l10n.formatValuesSync([
+ {id: "key-value"},
+ {id: "key-missing"},
+ ]);
+
+ strictEqual(values[0], "[de] Value1");
+ strictEqual(values[1], "[de] MissingValue");
+ }
+});
diff --git a/intl/l10n/test/test_messagecontext.js b/intl/l10n/test/test_messagecontext.js
new file mode 100644
index 0000000000..7ab17dea90
--- /dev/null
+++ b/intl/l10n/test/test_messagecontext.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ test_methods_presence(FluentBundle);
+ test_methods_calling(FluentBundle, FluentResource);
+ test_number_options(FluentBundle, FluentResource);
+
+ ok(true);
+}
+
+function test_methods_presence(FluentBundle) {
+ const bundle = new FluentBundle(["en-US", "pl"]);
+ equal(typeof bundle.addResource, "function");
+ equal(typeof bundle.formatPattern, "function");
+}
+
+function test_methods_calling(FluentBundle, FluentResource) {
+ const bundle = new FluentBundle(["en-US", "pl"], {
+ useIsolating: false,
+ });
+ bundle.addResource(new FluentResource("key = Value"));
+
+ const msg = bundle.getMessage("key");
+ equal(bundle.formatPattern(msg.value), "Value");
+
+ bundle.addResource(new FluentResource("key2 = Hello { $name }"));
+
+ const msg2 = bundle.getMessage("key2");
+ equal(bundle.formatPattern(msg2.value, { name: "Amy" }), "Hello Amy");
+ ok(true);
+}
+
+function test_number_options(FluentBundle, FluentResource) {
+ const bundle = new FluentBundle(["en-US", "pl"], {
+ useIsolating: false,
+ });
+ bundle.addResource(new FluentResource(`
+key = { NUMBER(0.53, style: "percent") } { NUMBER(0.12, style: "percent", minimumFractionDigits: 0) }
+ { NUMBER(-2.5, style: "percent") } { NUMBER(2.91, style: "percent") } { NUMBER("wrong", style: "percent") }
+`));
+
+ const msg = bundle.getMessage("key");
+ equal(bundle.formatPattern(msg.value), "53.00% 12%\n-250.0% 291.00% ");
+
+ ok(true);
+}
diff --git a/intl/l10n/test/test_missing_variables.js b/intl/l10n/test/test_missing_variables.js
new file mode 100644
index 0000000000..8c05f7f94f
--- /dev/null
+++ b/intl/l10n/test/test_missing_variables.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Disable `xpc::IsInAutomation()` so that missing variables don't throw
+// errors.
+Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ false
+);
+
+/**
+ * The following test demonstrates crashing behavior.
+ */
+add_task(function test_missing_variables() {
+ const l10nReg = new L10nRegistry();
+
+ const fs = [
+ { path: "/localization/en-US/browser/test.ftl", source: "welcome-message = Welcome { $user }\n" }
+ ]
+ const locales = ["en-US"];
+ const source = L10nFileSource.createMock("test", "app", locales, "/localization/{locale}", fs);
+ l10nReg.registerSources([source]);
+ const l10n = new Localization(["/browser/test.ftl"], true, l10nReg, locales);
+
+ {
+ const [message] = l10n.formatValuesSync([{ id: "welcome-message", args: { user: "Greg" } }]);
+ equal(message, "Welcome Greg");
+ }
+
+ {
+ // This will crash in debug builds.
+ const [message] = l10n.formatValuesSync([{ id: "welcome-message" }]);
+ equal(message, "Welcome {$user}");
+ }
+});
diff --git a/intl/l10n/test/test_pseudo.js b/intl/l10n/test/test_pseudo.js
new file mode 100644
index 0000000000..23404aab12
--- /dev/null
+++ b/intl/l10n/test/test_pseudo.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+const originalValues = {
+ requested: Services.locale.requestedLocales,
+};
+
+const l10nReg = new L10nRegistry();
+
+function getMockRegistry() {
+ const mockSource = L10nFileSource.createMock("test", "app", ["en-US"], "/localization/{locale}/", [
+ {
+ path: "/localization/en-US/mock.ftl",
+ source: `
+key = This is a single message
+ .tooltip = This is a tooltip
+ .accesskey = f
+`
+ }
+ ]);
+ let registry = new L10nRegistry();
+ registry.registerSources([mockSource]);
+ return registry;
+}
+
+function getAttributeByName(attributes, name) {
+ return attributes.find(attr => attr.name === name);
+}
+
+/**
+ * This test verifies that as we switch between
+ * different pseudo strategies, the Localization object
+ * follows and formats using the given strategy.
+ *
+ * We test values and attributes and make sure that
+ * a single-character attributes, commonly used for access keys
+ * don't get transformed.
+ */
+add_task(async function test_pseudo_works() {
+ Services.prefs.setStringPref("intl.l10n.pseudo", "");
+
+ let mockRegistry = getMockRegistry();
+
+ const l10n = new Localization([
+ "mock.ftl",
+ ], false, mockRegistry);
+
+ {
+ // 1. Start with no pseudo
+
+ let message = (await l10n.formatMessages([{id: "key"}]))[0];
+
+ ok(message.value.includes("This is a single message"));
+ let attr0 = getAttributeByName(message.attributes, "tooltip");
+ ok(attr0.value.includes("This is a tooltip"));
+ let attr1 = getAttributeByName(message.attributes, "accesskey");
+ equal(attr1.value, "f");
+ }
+
+ {
+ // 2. Set Accented Pseudo
+
+ Services.prefs.setStringPref("intl.l10n.pseudo", "accented");
+ let message = (await l10n.formatMessages([{id: "key"}]))[0];
+
+ ok(message.value.includes("[Ŧħiş iş aa şiƞɠŀee ḿeeşşaaɠee]"));
+ let attr0 = getAttributeByName(message.attributes, "tooltip");
+ ok(attr0.value.includes("[Ŧħiş iş aa ŧooooŀŧiƥ]"));
+ let attr1 = getAttributeByName(message.attributes, "accesskey");
+ equal(attr1.value, "f");
+ }
+
+ {
+ // 3. Set Bidi Pseudo
+
+ Services.prefs.setStringPref("intl.l10n.pseudo", "bidi");
+ let message = (await l10n.formatMessages([{id: "key"}]))[0];
+
+ ok(message.value.includes("iş a şiƞɠŀe ḿeşşaɠe"));
+ let attr0 = getAttributeByName(message.attributes, "tooltip");
+ ok(attr0.value.includes("Ŧħiş iş a ŧooŀŧiƥ"));
+ let attr1 = getAttributeByName(message.attributes, "accesskey");
+ equal(attr1.value, "f");
+ }
+
+ {
+ // 4. Remove pseudo
+
+ Services.prefs.setStringPref("intl.l10n.pseudo", "");
+ let message = (await l10n.formatMessages([{id: "key"}]))[0];
+
+ ok(message.value.includes("This is a single message"));
+ let attr0 = getAttributeByName(message.attributes, "tooltip");
+ ok(attr0.value.includes("This is a tooltip"));
+ let attr1 = getAttributeByName(message.attributes, "accesskey");
+ equal(attr1.value, "f");
+ }
+
+ Services.locale.requestedLocales = originalValues.requested;
+});
+
+/**
+ * This test verifies that setting a bogus pseudo locale
+ * strategy doesn't break anything.
+ */
+add_task(async function test_unavailable_strategy_works() {
+ Services.prefs.setStringPref("intl.l10n.pseudo", "");
+
+ let mockRegistry = getMockRegistry();
+
+ const l10n = new Localization([
+ "mock.ftl",
+ ], false, mockRegistry);
+
+ {
+ // 1. Set unavailable pseudo strategy
+ Services.prefs.setStringPref("intl.l10n.pseudo", "unknown-strategy");
+
+ let message = (await l10n.formatMessages([{id: "key"}]))[0];
+
+ ok(message.value.includes("This is a single message"));
+ let attr0 = getAttributeByName(message.attributes, "tooltip");
+ ok(attr0.value.includes("This is a tooltip"));
+ let attr1 = getAttributeByName(message.attributes, "accesskey");
+ equal(attr1.value, "f");
+ }
+
+ Services.prefs.setStringPref("intl.l10n.pseudo", "");
+ Services.locale.requestedLocales = originalValues.requested;
+});
diff --git a/intl/l10n/test/xpcshell.ini b/intl/l10n/test/xpcshell.ini
new file mode 100644
index 0000000000..4b5073ebe0
--- /dev/null
+++ b/intl/l10n/test/xpcshell.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+head =
+
+[test_datetimeformat.js]
+[test_l10nregistry.js]
+[test_l10nregistry_fuzzed.js]
+[test_l10nregistry_sync.js]
+[test_localization.js]
+[test_localization_sync.js]
+[test_messagecontext.js]
+[test_missing_variables.js]
+skip-if = debug # Intentionally triggers a debug assert for missing Fluent arguments.
+[test_pseudo.js]