forked from mirrors/gecko-dev
Bug 1558602 - Allow DocumentL10n to use LocalizationSync. r=smaug,Pike
Differential Revision: https://phabricator.services.mozilla.com/D34584 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
3cc10e36db
commit
b0ba25f8b9
21 changed files with 510 additions and 84 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -309,13 +309,54 @@ already_AddRefed<Promise> DOMLocalization::TranslateElements(
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
if (mIsSync) {
|
||||
nsTArray<JS::Value> jsKeys;
|
||||
SequenceRooter<JS::Value> keysRooter(cx, &jsKeys);
|
||||
for (auto& key : l10nKeys) {
|
||||
JS::RootedValue jsKey(cx);
|
||||
if (!ToJSValue(cx, key, &jsKey)) {
|
||||
aRv.NoteJSContextException(cx);
|
||||
return nullptr;
|
||||
}
|
||||
jsKeys.AppendElement(jsKey);
|
||||
}
|
||||
|
||||
nsTArray<JS::Value> messages;
|
||||
SequenceRooter<JS::Value> messagesRooter(cx, &messages);
|
||||
mLocalization->FormatMessagesSync(jsKeys, messages);
|
||||
nsTArray<L10nMessage> l10nData;
|
||||
SequenceRooter<L10nMessage> l10nDataRooter(cx, &l10nData);
|
||||
|
||||
for (auto& msg : messages) {
|
||||
JS::Rooted<JS::Value> 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<Promise> callbackResult = FormatMessages(cx, l10nKeys, aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nativeHandler->SetReturnValuePromise(promise);
|
||||
callbackResult->AppendNativeHandler(nativeHandler);
|
||||
}
|
||||
|
||||
return MaybeWrapPromise(promise);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<nsString>& 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; }
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html data-l10n-sync>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test DocumentL10n in HTML environment</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">
|
||||
<link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
await document.l10n.ready;
|
||||
|
||||
// Test for initial localization applied.
|
||||
let desc = document.getElementById("main-desc");
|
||||
is(desc.textContent.length > 0, true);
|
||||
|
||||
// Test for manual value formatting.
|
||||
let msg = await document.l10n.formatValue("id-heading");
|
||||
is(msg.length > 0, true);
|
||||
|
||||
// Test for mutations applied.
|
||||
let verifyL10n = () => {
|
||||
if (label.textContent.length > 0) {
|
||||
window.removeEventListener("MozAfterPaint", verifyL10n);
|
||||
SimpleTest.finish();
|
||||
}
|
||||
};
|
||||
window.addEventListener("MozAfterPaint", verifyL10n);
|
||||
|
||||
let label = document.getElementById("label1");
|
||||
document.l10n.setAttributes(
|
||||
label,
|
||||
"date-crashed-heading",
|
||||
{
|
||||
name: "John",
|
||||
}
|
||||
);
|
||||
|
||||
// Test for l10n.getAttributes
|
||||
let l10nArgs = document.l10n.getAttributes(label);
|
||||
is(l10nArgs.id, "date-crashed-heading");
|
||||
is(l10nArgs.args.name, "John");
|
||||
}, { once: true});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
|
||||
|
||||
<p id="label1"></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<nsString>& aResourceIds, ErrorResult& aRv) {
|
||||
nsCOMPtr<mozILocalizationJSM> 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();
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class Localization : public nsIObserver,
|
|||
|
||||
nsCOMPtr<nsIGlobalObject> mGlobal;
|
||||
nsCOMPtr<mozILocalization> mLocalization;
|
||||
bool mIsSync;
|
||||
};
|
||||
|
||||
} // namespace intl
|
||||
|
|
|
|||
|
|
@ -212,20 +212,28 @@ function maybeReportErrorToGecko(error) {
|
|||
class Localization {
|
||||
/**
|
||||
* @param {Array<String>} resourceIds - List of resource IDs
|
||||
* @param {Function} generateBundles - Function that returns a
|
||||
* @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) {
|
||||
if (this.isSync) {
|
||||
return CachedSyncIterable.from(iterable);
|
||||
} else {
|
||||
return CachedAsyncIterable.from(iterable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<String>} resourceIds - List of resource IDs
|
||||
|
|
@ -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<Object>} keys - Translation keys to format.
|
||||
* @param {Function} method - Formatting function.
|
||||
* @returns {Array<string|Object>}
|
||||
* @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<Object>} keys
|
||||
* @returns {Promise<Array<{value: string, attributes: Object}>>}
|
||||
|
|
@ -310,6 +358,19 @@ class Localization {
|
|||
return this.formatWithFallback(keys, messageFromBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync version of `formatMessages`.
|
||||
*
|
||||
* Returns an array of the translation messages.
|
||||
*
|
||||
* @param {Array<Object>} 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<Object>} keys
|
||||
* @returns {Array<string>}
|
||||
* @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<Object>} 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"];
|
||||
|
|
|
|||
|
|
@ -549,7 +549,7 @@ contexts manually using the `Localization` class:
|
|||
|
||||
|
||||
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
|
||||
==========================
|
||||
|
||||
|
|
|
|||
|
|
@ -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<jsval> aKeys);
|
||||
Promise formatValues(in Array<jsval> aKeys);
|
||||
Promise formatValue(in AString aId, [optional] in jsval aArgs);
|
||||
|
||||
Array<jsval> formatMessagesSync(in Array<jsval> aKeys);
|
||||
void setIsSync(in boolean isSync);
|
||||
};
|
||||
|
||||
[scriptable, uuid(96632d26-1422-12e9-b1ce-9bb586acd241)]
|
||||
interface mozILocalizationJSM : nsISupports
|
||||
{
|
||||
mozILocalization getLocalization(in Array<AString> resourceIds);
|
||||
mozILocalization getLocalization(in Array<AString> resourceIds, in bool sync);
|
||||
mozILocalization getLocalizationWithCustomGenerateMessages(in Array<AString> resourceIds, in jsval generateMessages);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
151
intl/l10n/test/test_localization_sync.js
Normal file
151
intl/l10n/test/test_localization_sync.js
Normal file
|
|
@ -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./);
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,6 +312,7 @@ module.exports = {
|
|||
"KeyEvent": false,
|
||||
"KeyboardEvent": false,
|
||||
"KeyframeEffect": false,
|
||||
"Localization": false,
|
||||
"Location": false,
|
||||
"MIDIAccess": false,
|
||||
"MIDIConnectionEvent": false,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue