diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /intl/l10n/test | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.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.cpp | 118 | ||||
-rw-r--r-- | intl/l10n/test/gtest/moz.build | 11 | ||||
-rw-r--r-- | intl/l10n/test/mochitest/chrome.ini | 6 | ||||
-rw-r--r-- | intl/l10n/test/mochitest/localization/test_formatMessages.html | 101 | ||||
-rw-r--r-- | intl/l10n/test/mochitest/localization/test_formatValue.html | 81 | ||||
-rw-r--r-- | intl/l10n/test/mochitest/localization/test_formatValues.html | 85 | ||||
-rw-r--r-- | intl/l10n/test/test_datetimeformat.js | 76 | ||||
-rw-r--r-- | intl/l10n/test/test_l10nregistry.js | 563 | ||||
-rw-r--r-- | intl/l10n/test/test_l10nregistry_fuzzed.js | 205 | ||||
-rw-r--r-- | intl/l10n/test/test_l10nregistry_sync.js | 536 | ||||
-rw-r--r-- | intl/l10n/test/test_localization.js | 319 | ||||
-rw-r--r-- | intl/l10n/test/test_localization_sync.js | 289 | ||||
-rw-r--r-- | intl/l10n/test/test_messagecontext.js | 47 | ||||
-rw-r--r-- | intl/l10n/test/test_missing_variables.js | 35 | ||||
-rw-r--r-- | intl/l10n/test/test_pseudo.js | 131 | ||||
-rw-r--r-- | intl/l10n/test/xpcshell.ini | 13 |
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] |