diff --git a/browser/components/aboutlogins/AboutLoginsParent.jsm b/browser/components/aboutlogins/AboutLoginsParent.jsm index 8ab14ebbf126..6023605e3e20 100644 --- a/browser/components/aboutlogins/AboutLoginsParent.jsm +++ b/browser/components/aboutlogins/AboutLoginsParent.jsm @@ -9,8 +9,6 @@ var EXPORTED_SYMBOLS = ["AboutLoginsParent"]; const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ChromeUtils.defineModuleGetter(this, "E10SUtils", "resource://gre/modules/E10SUtils.jsm"); -ChromeUtils.defineModuleGetter(this, "Localization", - "resource://gre/modules/Localization.jsm"); ChromeUtils.defineModuleGetter(this, "LoginHelper", "resource://gre/modules/LoginHelper.jsm"); ChromeUtils.defineModuleGetter(this, "MigrationUtils", diff --git a/browser/components/newtab/lib/OnboardingMessageProvider.jsm b/browser/components/newtab/lib/OnboardingMessageProvider.jsm index 9d376eb7027f..1f8abd1785af 100644 --- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm +++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm @@ -2,7 +2,6 @@ * 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/. */ "use strict"; -const {Localization} = ChromeUtils.import("resource://gre/modules/Localization.jsm"); const {FxAccountsConfig} = ChromeUtils.import("resource://gre/modules/FxAccountsConfig.jsm"); const {AttributionCode} = ChromeUtils.import("resource:///modules/AttributionCode.jsm"); const {AddonRepository} = ChromeUtils.import("resource://gre/modules/addons/AddonRepository.jsm"); diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js index df566926a62c..1b95129830cc 100644 --- a/browser/components/preferences/in-content/main.js +++ b/browser/components/preferences/in-content/main.js @@ -13,7 +13,6 @@ var {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); var {TransientPrefs} = ChromeUtils.import("resource:///modules/TransientPrefs.jsm"); var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); var {L10nRegistry} = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm"); -var {Localization} = ChromeUtils.import("resource://gre/modules/Localization.jsm"); var {HomePage} = ChromeUtils.import("resource:///modules/HomePage.jsm"); ChromeUtils.defineModuleGetter(this, "CloudStorage", "resource://gre/modules/CloudStorage.jsm"); diff --git a/browser/components/touchbar/MacTouchBar.js b/browser/components/touchbar/MacTouchBar.js index 6754f4fc612b..2f5bc1b9bc8a 100644 --- a/browser/components/touchbar/MacTouchBar.js +++ b/browser/components/touchbar/MacTouchBar.js @@ -9,7 +9,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", Services: "resource://gre/modules/Services.jsm", AppConstants: "resource://gre/modules/AppConstants.jsm", - Localization: "resource://gre/modules/Localization.jsm", }); /** diff --git a/dom/l10n/DOMLocalization.cpp b/dom/l10n/DOMLocalization.cpp index 77438ac532a3..69ab861d7555 100644 --- a/dom/l10n/DOMLocalization.cpp +++ b/dom/l10n/DOMLocalization.cpp @@ -309,13 +309,54 @@ already_AddRefed DOMLocalization::TranslateElements( return nullptr; } - RefPtr callbackResult = FormatMessages(cx, l10nKeys, aRv); - if (NS_WARN_IF(aRv.Failed())) { - return nullptr; - } + if (mIsSync) { + nsTArray jsKeys; + SequenceRooter keysRooter(cx, &jsKeys); + for (auto& key : l10nKeys) { + JS::RootedValue jsKey(cx); + if (!ToJSValue(cx, key, &jsKey)) { + aRv.NoteJSContextException(cx); + return nullptr; + } + jsKeys.AppendElement(jsKey); + } - nativeHandler->SetReturnValuePromise(promise); - callbackResult->AppendNativeHandler(nativeHandler); + nsTArray messages; + SequenceRooter messagesRooter(cx, &messages); + mLocalization->FormatMessagesSync(jsKeys, messages); + nsTArray l10nData; + SequenceRooter l10nDataRooter(cx, &l10nData); + + for (auto& msg : messages) { + JS::Rooted rootedMsg(cx); + rootedMsg.set(msg); + L10nMessage* slotPtr = l10nData.AppendElement(mozilla::fallible); + if (!slotPtr) { + promise->MaybeRejectWithUndefined(); + return MaybeWrapPromise(promise); + } + + if (!slotPtr->Init(cx, rootedMsg)) { + promise->MaybeRejectWithUndefined(); + return MaybeWrapPromise(promise); + } + } + + ApplyTranslations(domElements, l10nData, aRv); + if (NS_WARN_IF(aRv.Failed())) { + promise->MaybeRejectWithUndefined(); + return MaybeWrapPromise(promise); + } + + promise->MaybeResolveWithUndefined(); + } else { + RefPtr callbackResult = FormatMessages(cx, l10nKeys, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + nativeHandler->SetReturnValuePromise(promise); + callbackResult->AppendNativeHandler(nativeHandler); + } return MaybeWrapPromise(promise); } diff --git a/dom/l10n/DocumentL10n.cpp b/dom/l10n/DocumentL10n.cpp index 3bf9e3439927..b78b67a06061 100644 --- a/dom/l10n/DocumentL10n.cpp +++ b/dom/l10n/DocumentL10n.cpp @@ -34,6 +34,11 @@ DocumentL10n::DocumentL10n(Document* aDocument) mDocument(aDocument), mState(DocumentL10nState::Initialized) { mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink()); + + Element* elem = mDocument->GetDocumentElement(); + if (elem) { + mIsSync = elem->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nsync); + } } void DocumentL10n::Init(nsTArray& aResourceIds, ErrorResult& aRv) { @@ -133,6 +138,14 @@ void DocumentL10n::InitialDocumentTranslationCompleted() { if (mContentSink) { mContentSink->InitialDocumentTranslationCompleted(); } + + // If sync was true, we want to change the state of + // mozILocalization to async now. + if (mIsSync) { + mIsSync = false; + + mLocalization->SetIsSync(mIsSync); + } } Promise* DocumentL10n::Ready() { return mReady; } diff --git a/dom/l10n/tests/mochitest/chrome.ini b/dom/l10n/tests/mochitest/chrome.ini index dc72202fe08a..3763cfa71f51 100644 --- a/dom/l10n/tests/mochitest/chrome.ini +++ b/dom/l10n/tests/mochitest/chrome.ini @@ -36,5 +36,6 @@ [document_l10n/test_docl10n_initialize_after_parse.xul] [document_l10n/test_docl10n.xhtml] [document_l10n/test_docl10n.html] +[document_l10n/test_docl10n_sync.html] [document_l10n/test_docl10n_ready_rejected.html] [document_l10n/test_docl10n_removeResourceIds.html] diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html new file mode 100644 index 000000000000..f931ed3351dd --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html @@ -0,0 +1,54 @@ + + + + + Test DocumentL10n in HTML environment + + + + + + +

+ +

+ + diff --git a/intl/l10n/L10nRegistry.jsm b/intl/l10n/L10nRegistry.jsm index e5d02422ce4a..1a60769ed926 100644 --- a/intl/l10n/L10nRegistry.jsm +++ b/intl/l10n/L10nRegistry.jsm @@ -81,7 +81,7 @@ const isParentProcess = appinfo.processType === appinfo.PROCESS_TYPE_DEFAULT; * * Notice: L10nRegistry is primarily an asynchronous API, but * it does provide a synchronous version of it's main method - * for use by the `LocalizationSync` class. + * for use by the `Localization` class when in `sync` state. * This API should be only used in very specialized cases and * the uses should be reviewed by the toolkit owner/peer. */ diff --git a/intl/l10n/Localization.cpp b/intl/l10n/Localization.cpp index 884a52feb948..96a153b53274 100644 --- a/intl/l10n/Localization.cpp +++ b/intl/l10n/Localization.cpp @@ -34,14 +34,16 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Localization) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_END -Localization::Localization(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) {} +Localization::Localization(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal), mIsSync(false) {} void Localization::Init(nsTArray& aResourceIds, ErrorResult& aRv) { nsCOMPtr jsm = do_ImportModule("resource://gre/modules/Localization.jsm"); MOZ_RELEASE_ASSERT(jsm); - Unused << jsm->GetLocalization(aResourceIds, getter_AddRefs(mLocalization)); + Unused << jsm->GetLocalization(aResourceIds, mIsSync, + getter_AddRefs(mLocalization)); MOZ_RELEASE_ASSERT(mLocalization); RegisterObservers(); diff --git a/intl/l10n/Localization.h b/intl/l10n/Localization.h index 734a6c19face..483d5dc6c2f8 100644 --- a/intl/l10n/Localization.h +++ b/intl/l10n/Localization.h @@ -74,6 +74,7 @@ class Localization : public nsIObserver, nsCOMPtr mGlobal; nsCOMPtr mLocalization; + bool mIsSync; }; } // namespace intl diff --git a/intl/l10n/Localization.jsm b/intl/l10n/Localization.jsm index 393bed41a7a4..8780f21774c1 100644 --- a/intl/l10n/Localization.jsm +++ b/intl/l10n/Localization.jsm @@ -211,20 +211,28 @@ function maybeReportErrorToGecko(error) { */ class Localization { /** - * @param {Array} resourceIds - List of resource IDs - * @param {Function} generateBundles - Function that returns a - * generator over FluentBundles + * @param {Array} resourceIds - List of resource IDs + * @param {Function} generateBundles - Function that returns an async + * generator over FluentBundles + * @param {Function} generateBundlesSync - Function that returns a sync + * generator over FluentBundles * * @returns {Localization} */ - constructor(resourceIds = [], generateBundles = defaultGenerateBundles) { + constructor(resourceIds = [], sync = false, generateBundles = defaultGenerateBundles, generateBundlesSync = defaultGenerateBundlesSync) { + this.isSync = sync; this.resourceIds = resourceIds; this.generateBundles = generateBundles; + this.generateBundlesSync = generateBundlesSync; this.onChange(true); } cached(iterable) { - return CachedAsyncIterable.from(iterable); + if (this.isSync) { + return CachedSyncIterable.from(iterable); + } else { + return CachedAsyncIterable.from(iterable); + } } /** @@ -280,6 +288,46 @@ class Localization { return translations; } + /** + * Format translations and handle fallback if needed. + * + * Format translations for `keys` from `FluentBundle` instances on this + * Localization. In case of errors, fetch the next context in the + * fallback chain. + * + * @param {Array} keys - Translation keys to format. + * @param {Function} method - Formatting function. + * @returns {Array} + * @private + */ + formatWithFallbackSync(keys, method) { + if (!this.isSync) { + throw new Error("Can't use sync formatWithFallback when state is async."); + } + const translations = new Array(keys.length); + let hasAtLeastOneBundle = false; + + for (const bundle of this.bundles) { + hasAtLeastOneBundle = true; + const missingIds = keysFromBundle(method, bundle, keys, translations); + + if (missingIds.size === 0) { + break; + } + + const locale = bundle.locales[0]; + const ids = Array.from(missingIds).join(", "); + maybeReportErrorToGecko(`[fluent] Missing translations in ${locale}: ${ids}.`); + } + + if (!hasAtLeastOneBundle) { + maybeReportErrorToGecko(`[fluent] Request for keys failed because no resource bundles got generated.\n keys: ${JSON.stringify(keys)}.\n resourceIds: ${JSON.stringify(this.resourceIds)}.`); + } + + return translations; + } + + /** * Format translations into {value, attributes} objects. * @@ -300,7 +348,7 @@ class Localization { * // } * // ] * - * Returns a Promise resolving to an array of the translation strings. + * Returns a Promise resolving to an array of the translation messages. * * @param {Array} keys * @returns {Promise>} @@ -310,6 +358,19 @@ class Localization { return this.formatWithFallback(keys, messageFromBundle); } + /** + * Sync version of `formatMessages`. + * + * Returns an array of the translation messages. + * + * @param {Array} keys + * @returns {Array<{value: string, attributes: Object}>} + * @private + */ + formatMessagesSync(keys) { + return this.formatWithFallbackSync(keys, messageFromBundle); + } + /** * Retrieve translations corresponding to the passed keys. * @@ -333,6 +394,19 @@ class Localization { return this.formatWithFallback(keys, valueFromBundle); } + /** + * Sync version of `formatValues`. + * + * Returns an array of the translation strings. + * + * @param {Array} keys + * @returns {Array} + * @private + */ + formatValuesSync(keys) { + return this.formatWithFallbackSync(keys, valueFromBundle); + } + /** * Retrieve the translation corresponding to the `id` identifier. * @@ -345,7 +419,7 @@ class Localization { * * // 'Hello, world!' * - * Returns a Promise resolving to the translation string. + * Returns a Promise resolving to a translation string. * * Use this sparingly for one-off messages which don't need to be * retranslated when the user changes their language preferences, e.g. in @@ -360,6 +434,20 @@ class Localization { return val; } + /** + * Sync version of `formatValue`. + * + * Returns a translation string. + * + * @param {Array} keys + * @returns {string>} + * @private + */ + formatValueSync(id, args) { + const [val] = this.formatValuesSync([{id, args}]); + return val; + } + /** * Register weak observers on events that will trigger cache invalidation */ @@ -398,8 +486,8 @@ class Localization { * @param {bool} eager - whether the I/O for new context should begin eagerly */ onChange(eager = false) { - this.bundles = this.cached( - this.generateBundles(this.resourceIds)); + let generateMessages = this.isSync ? this.generateBundlesSync : this.generateBundles; + this.bundles = this.cached(generateMessages(this.resourceIds)); if (eager) { // If the first app locale is the same as last fallback // it means that we have all resources in this locale, and @@ -412,51 +500,17 @@ class Localization { this.bundles.touchNext(prefetchCount); } } + + setIsSync(isSync) { + this.isSync = isSync; + this.onChange(); + } } Localization.prototype.QueryInterface = ChromeUtils.generateQI([ Ci.nsISupportsWeakReference, ]); -class LocalizationSync extends Localization { - constructor(resourceIds = [], generateBundles = defaultGenerateBundlesSync) { - super(resourceIds, generateBundles); - } - - cached(iterable) { - return CachedSyncIterable.from(iterable); - } - - formatWithFallback(keys, method) { - const translations = new Array(keys.length); - let hasAtLeastOneBundle = false; - - for (const bundle of this.bundles) { - hasAtLeastOneBundle = true; - const missingIds = keysFromBundle(method, bundle, keys, translations); - - if (missingIds.size === 0) { - break; - } - - const locale = bundle.locales[0]; - const ids = Array.from(missingIds).join(", "); - maybeReportErrorToGecko(`[fluent] Missing translations in ${locale}: ${ids}.`); - } - - if (!hasAtLeastOneBundle) { - maybeReportErrorToGecko(`[fluent] Request for keys failed because no resource bundles got generated.\n keys: ${JSON.stringify(keys)}.\n resourceIds: ${JSON.stringify(this.resourceIds)}.`); - } - - return translations; - } - - formatValue(id, args) { - const [val] = this.formatValues([{id, args}]); - return val; - } -} - /** * Format the value of a message into a string. * @@ -577,7 +631,6 @@ function keysFromBundle(method, bundle, keys, translations) { } }); - return missingIds; } @@ -585,14 +638,13 @@ function keysFromBundle(method, bundle, keys, translations) { * Helper function which allows us to construct a new * Localization from Localization. */ -var getLocalization = (resourceIds) => { - return new Localization(resourceIds); +var getLocalization = (resourceIds, sync = false) => { + return new Localization(resourceIds, sync); }; var getLocalizationWithCustomGenerateMessages = (resourceIds, generateMessages) => { - return new Localization(resourceIds, generateMessages); + return new Localization(resourceIds, false, generateMessages); }; this.Localization = Localization; -this.LocalizationSync = LocalizationSync; -var EXPORTED_SYMBOLS = ["Localization", "LocalizationSync", "getLocalization", "getLocalizationWithCustomGenerateMessages"]; +var EXPORTED_SYMBOLS = ["Localization", "getLocalization", "getLocalizationWithCustomGenerateMessages"]; diff --git a/intl/l10n/docs/fluent_tutorial.rst b/intl/l10n/docs/fluent_tutorial.rst index ed42c00dbe0c..82c1defc678d 100644 --- a/intl/l10n/docs/fluent_tutorial.rst +++ b/intl/l10n/docs/fluent_tutorial.rst @@ -540,16 +540,16 @@ contexts manually using the `Localization` class: const { Localization } = ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); - - + + const myL10n = new Localization([ "branding/brand.ftl", "browser/preferences/preferences.ftl" ]); - - + + let [isDefaultMsg, isNotDefaultMsg] = - myL10n.formatValues({id: "is-default"}, {id: "is-not-default"}); + await myL10n.formatValues({id: "is-default"}, {id: "is-not-default"}); .. admonition:: Example @@ -563,6 +563,32 @@ contexts manually using the `Localization` class: A developer may create manually a new context with the same resources as the main one, but hardcode it to `en-US` and then build the search index using both contexts. + +By default, all `Localization` contexts are asynchronous. It is possible to create a synchronous +one by passing an `sync = false` argument to the constructor, or calling the `SetIsSync(bool)` method +on the class. + + +.. code-block:: javascript + + const { Localization } = + ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); + + + const myL10n = new Localization([ + "branding/brand.ftl", + "browser/preferences/preferences.ftl" + ], false); + + + let [isDefaultMsg, isNotDefaultMsg] = + myL10n.formatValuesSync({id: "is-default"}, {id: "is-not-default"}); + + +Synchronous contexts should be always avoided as they require synchronous I/O. If you think your use case +requires a synchronous localization context, please consult Gecko, Performance and L10n Drivers teams. + + Designing Localizable APIs ========================== diff --git a/intl/l10n/mozILocalization.idl b/intl/l10n/mozILocalization.idl index e7ad72845162..bf820915de4b 100644 --- a/intl/l10n/mozILocalization.idl +++ b/intl/l10n/mozILocalization.idl @@ -3,6 +3,12 @@ * 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/. */ +/** + * This is an internal XPIDL used to expose a JS based Localization class to be used + * by its C++ wrapper. + * + * Consumers should use the WebIDL Localization API instead of this one. + */ #include "nsISupports.idl" [scriptable, uuid(7d468600-551f-4fe0-98c9-92a53b63ec8d)] @@ -15,11 +21,14 @@ interface mozILocalization : nsISupports Promise formatMessages(in Array aKeys); Promise formatValues(in Array aKeys); Promise formatValue(in AString aId, [optional] in jsval aArgs); + + Array formatMessagesSync(in Array aKeys); + void setIsSync(in boolean isSync); }; [scriptable, uuid(96632d26-1422-12e9-b1ce-9bb586acd241)] interface mozILocalizationJSM : nsISupports { - mozILocalization getLocalization(in Array resourceIds); + mozILocalization getLocalization(in Array resourceIds, in bool sync); mozILocalization getLocalizationWithCustomGenerateMessages(in Array resourceIds, in jsval generateMessages); }; diff --git a/intl/l10n/test/test_localization.js b/intl/l10n/test/test_localization.js index ff01d94fd120..cbec36480f7e 100644 --- a/intl/l10n/test/test_localization.js +++ b/intl/l10n/test/test_localization.js @@ -35,7 +35,7 @@ add_task(async function test_methods_calling() { const l10n = new Localization([ "/browser/menu.ftl", - ], generateMessages); + ], false, generateMessages); let values = await l10n.formatValues([{id: "key"}, {id: "key2"}]); @@ -81,7 +81,7 @@ key = { PLATFORM() -> const l10n = new Localization([ "/test.ftl", - ], generateMessages); + ], false, generateMessages); let values = await l10n.formatValues([{id: "key"}]); @@ -114,7 +114,7 @@ add_task(async function test_add_remove_resourceIds() { yield * await L10nRegistry.generateBundles(["en-US"], resIds); } - const l10n = new Localization(["/browser/menu.ftl"], generateMessages); + const l10n = new Localization(["/browser/menu.ftl"], false, generateMessages); let values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]); @@ -139,3 +139,81 @@ add_task(async function test_add_remove_resourceIds() { L10nRegistry.load = originalLoad; Services.locale.requestedLocales = originalRequested; }); + +add_task(async function test_switch_to_async() { + const { L10nRegistry, FileSource } = + ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm"); + + const fs = { + "/localization/en-US/browser/menu.ftl": "key1 = Value1", + "/localization/en-US/toolkit/menu.ftl": "key2 = Value2", + }; + const originalLoad = L10nRegistry.load; + const originalLoadSync = L10nRegistry.loadSync; + const originalRequested = Services.locale.requestedLocales; + + let syncLoads = 0; + let asyncLoads = 0; + + L10nRegistry.load = async function(url) { + asyncLoads += 1; + return fs[url]; + }; + + L10nRegistry.loadSync = function(url) { + syncLoads += 1; + return fs[url]; + }; + + const source = new FileSource("test", ["en-US"], "/localization/{locale}"); + L10nRegistry.registerSource(source); + + async function* generateMessages(resIds) { + yield * await L10nRegistry.generateBundles(["en-US"], resIds); + } + + function* generateMessagesSync(resIds) { + yield * L10nRegistry.generateBundlesSync(["en-US"], resIds); + } + + const l10n = new Localization(["/browser/menu.ftl"], false, generateMessages, generateMessagesSync); + + let values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]); + + equal(values[0], "Value1"); + equal(values[1], undefined); + equal(syncLoads, 0); + equal(asyncLoads, 1); + + l10n.setIsSync(true); + + l10n.addResourceIds(["/toolkit/menu.ftl"]); + + // Nothing happens when we switch, because + // the next load is lazy. + equal(syncLoads, 0); + equal(asyncLoads, 1); + + values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]); + let values2 = await l10n.formatValues([{id: "key1"}, {id: "key2"}]); + + deepEqual(values, values2); + equal(values[0], "Value1"); + equal(values[1], "Value2"); + equal(syncLoads, 1); + equal(asyncLoads, 1); + + l10n.removeResourceIds(["/browser/menu.ftl"]); + + values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]); + + equal(values[0], undefined); + equal(values[1], "Value2"); + equal(syncLoads, 1); + equal(asyncLoads, 1); + + L10nRegistry.sources.clear(); + L10nRegistry.load = originalLoad; + L10nRegistry.loadSync = originalLoadSync; + Services.locale.requestedLocales = originalRequested; +}); diff --git a/intl/l10n/test/test_localization_sync.js b/intl/l10n/test/test_localization_sync.js new file mode 100644 index 000000000000..560b89735c81 --- /dev/null +++ b/intl/l10n/test/test_localization_sync.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +const { Localization } = ChromeUtils.import("resource://gre/modules/Localization.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +add_task(function test_methods_calling() { + const { L10nRegistry, FileSource } = + ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm"); + + const fs = { + "/localization/de/browser/menu.ftl": "key = [de] Value2", + "/localization/en-US/browser/menu.ftl": "key = [en] Value2\nkey2 = [en] Value3", + }; + const originalLoadSync = L10nRegistry.loadSync; + const originalRequested = Services.locale.requestedLocales; + + L10nRegistry.loadSync = function(url) { + return fs[url]; + }; + + const source = new FileSource("test", ["de", "en-US"], "/localization/{locale}"); + L10nRegistry.registerSource(source); + + function* generateMessagesSync(resIds) { + yield * L10nRegistry.generateBundlesSync(["de", "en-US"], resIds); + } + + const l10n = new Localization([ + "/browser/menu.ftl", + ], true, null, generateMessagesSync); + + let values = l10n.formatValuesSync([{id: "key"}, {id: "key2"}]); + + equal(values[0], "[de] Value2"); + equal(values[1], "[en] Value3"); + + L10nRegistry.sources.clear(); + L10nRegistry.loadSync = originalLoadSync; + Services.locale.requestedLocales = originalRequested; +}); + +add_task(function test_builtins() { + const { L10nRegistry, FileSource } = + ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm"); + + const known_platforms = { + "linux": "linux", + "win": "windows", + "macosx": "macos", + "android": "android", + }; + + const fs = { + "/localization/en-US/test.ftl": ` +key = { PLATFORM() -> + ${ Object.values(known_platforms).map( + name => ` [${ name }] ${ name.toUpperCase() } Value\n`).join("") } + *[other] OTHER Value + }`, + }; + const originalLoadSync = L10nRegistry.loadSync; + + L10nRegistry.loadSync = function(url) { + return fs[url]; + }; + + const source = new FileSource("test", ["en-US"], "/localization/{locale}"); + L10nRegistry.registerSource(source); + + function* generateMessagesSync(resIds) { + yield * L10nRegistry.generateBundlesSync(["en-US"], resIds); + } + + const l10n = new Localization([ + "/test.ftl", + ], true, null, generateMessagesSync); + + let values = l10n.formatValuesSync([{id: "key"}]); + + ok(values[0].includes( + `${ known_platforms[AppConstants.platform].toUpperCase() } Value`)); + + L10nRegistry.sources.clear(); + L10nRegistry.loadSync = originalLoadSync; +}); + +add_task(function test_add_remove_resourceIds() { + const { L10nRegistry, FileSource } = + ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm"); + + const fs = { + "/localization/en-US/browser/menu.ftl": "key1 = Value1", + "/localization/en-US/toolkit/menu.ftl": "key2 = Value2", + }; + const originalLoadSync = L10nRegistry.loadSYnc; + const originalRequested = Services.locale.requestedLocales; + + L10nRegistry.loadSync = function(url) { + return fs[url]; + }; + + const source = new FileSource("test", ["en-US"], "/localization/{locale}"); + L10nRegistry.registerSource(source); + + function* generateMessagesSync(resIds) { + yield * L10nRegistry.generateBundlesSync(["en-US"], resIds); + } + + const l10n = new Localization(["/browser/menu.ftl"], true, null, generateMessagesSync); + + let values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]); + + equal(values[0], "Value1"); + equal(values[1], undefined); + + l10n.addResourceIds(["/toolkit/menu.ftl"]); + + values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]); + + equal(values[0], "Value1"); + equal(values[1], "Value2"); + + l10n.removeResourceIds(["/browser/menu.ftl"]); + + values = l10n.formatValuesSync([{id: "key1"}, {id: "key2"}]); + + equal(values[0], undefined); + equal(values[1], "Value2"); + + L10nRegistry.sources.clear(); + L10nRegistry.loadSync = originalLoadSync; + Services.locale.requestedLocales = originalRequested; +}); + +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 sync formatWithFallback when state is async./); + + Assert.throws(() => { + l10n.formatValueSync("key1"); + }, /Can't use sync formatWithFallback when state is async./); + + Assert.throws(() => { + l10n.formatMessagesSync([{ id: "key1"}]); + }, /Can't use sync formatWithFallback when state is async./); +}); \ No newline at end of file diff --git a/intl/l10n/test/test_pseudo.js b/intl/l10n/test/test_pseudo.js index faab77bd8e45..61c7498e3276 100644 --- a/intl/l10n/test/test_pseudo.js +++ b/intl/l10n/test/test_pseudo.js @@ -46,7 +46,7 @@ add_task(async function test_accented_works() { const l10n = new Localization([ "/browser/menu.ftl", - ], generateMessages); + ], false, generateMessages); l10n.registerObservers(); { @@ -108,7 +108,7 @@ add_task(async function test_unavailable_strategy_works() { const l10n = new Localization([ "/browser/menu.ftl", - ], generateMessages); + ], false, generateMessages); l10n.registerObservers(); { diff --git a/intl/l10n/test/xpcshell.ini b/intl/l10n/test/xpcshell.ini index fae662a465f5..17e1d6f88f7b 100644 --- a/intl/l10n/test/xpcshell.ini +++ b/intl/l10n/test/xpcshell.ini @@ -4,5 +4,6 @@ head = [test_l10nregistry.js] [test_l10nregistry_sync.js] [test_localization.js] +[test_localization_sync.js] [test_messagecontext.js] [test_pseudo.js] diff --git a/toolkit/components/mozintl/mozIntl.jsm b/toolkit/components/mozintl/mozIntl.jsm index 867cee691bc0..11e09670a054 100644 --- a/toolkit/components/mozintl/mozIntl.jsm +++ b/toolkit/components/mozintl/mozIntl.jsm @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { LocalizationSync } = ChromeUtils.import("resource://gre/modules/Localization.jsm", null); +const { Localization } = ChromeUtils.import("resource://gre/modules/Localization.jsm", null); const mozIntlHelper = Cc["@mozilla.org/mozintlhelper;1"].getService(Ci.mozIMozIntlHelper); @@ -269,7 +269,7 @@ class MozIntl { } if (!this._cache.hasOwnProperty("languageLocalization")) { - const loc = new LocalizationSync(["toolkit/intl/languageNames.ftl"]); + const loc = new Localization(["toolkit/intl/languageNames.ftl"], true); this._cache.languageLocalization = loc; } @@ -281,7 +281,7 @@ class MozIntl { } let lcLangCode = langCode.toLowerCase(); if (availableLocaleDisplayNames.language.has(lcLangCode)) { - const value = loc.formatValue(`language-name-${lcLangCode}`); + const value = loc.formatValueSync(`language-name-${lcLangCode}`); if (value !== undefined) { return value; } @@ -296,7 +296,7 @@ class MozIntl { } if (!this._cache.hasOwnProperty("regionLocalization")) { - const loc = new LocalizationSync(["toolkit/intl/regionNames.ftl"]); + const loc = new Localization(["toolkit/intl/regionNames.ftl"], true); this._cache.regionLocalization = loc; } @@ -308,7 +308,7 @@ class MozIntl { } let lcRegionCode = regionCode.toLowerCase(); if (availableLocaleDisplayNames.region.has(lcRegionCode)) { - const value = loc.formatValue(`region-name-${lcRegionCode}`); + const value = loc.formatValueSync(`region-name-${lcRegionCode}`); if (value !== undefined) { return value; } diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js index 67d1a6f4f2f1..3fbf5b273543 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js @@ -312,6 +312,7 @@ module.exports = { "KeyEvent": false, "KeyboardEvent": false, "KeyframeEffect": false, + "Localization": false, "Location": false, "MIDIAccess": false, "MIDIConnectionEvent": false, diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py index 61bcac63b571..f7662a9933fd 100644 --- a/xpcom/ds/StaticAtoms.py +++ b/xpcom/ds/StaticAtoms.py @@ -289,6 +289,7 @@ STATIC_ATOMS = [ Atom("datal10nargs", "data-l10n-args"), Atom("datal10nattrs", "data-l10n-attrs"), Atom("datal10nname", "data-l10n-name"), + Atom("datal10nsync", "data-l10n-sync"), Atom("dataType", "data-type"), Atom("dateTime", "date-time"), Atom("date", "date"),