Bug 1558602 - Allow DocumentL10n to use LocalizationSync. r=smaug,Pike
authorZibi Braniecki <zbraniecki@mozilla.com>
Mon, 01 Jul 2019 17:56:57 +0000
changeset 540474 24e366d72108f284984da25a27be95738fa3f3a5
parent 540473 1c18f35d0163f1ac2b63ecc5c4bd4d7a63dedb02
child 540475 832c35afa350fdad83140fa71d46de9609991e79
push id11529
push userarchaeopteryx@coole-files.de
push dateThu, 04 Jul 2019 15:22:33 +0000
treeherdermozilla-beta@ebb510a784b8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, Pike
bugs1558602
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1558602 - Allow DocumentL10n to use LocalizationSync. r=smaug,Pike Differential Revision: https://phabricator.services.mozilla.com/D34584
browser/components/aboutlogins/AboutLoginsParent.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/preferences/in-content/main.js
browser/components/touchbar/MacTouchBar.js
dom/l10n/DOMLocalization.cpp
dom/l10n/DocumentL10n.cpp
dom/l10n/tests/mochitest/chrome.ini
dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html
intl/l10n/L10nRegistry.jsm
intl/l10n/Localization.cpp
intl/l10n/Localization.h
intl/l10n/Localization.jsm
intl/l10n/docs/fluent_tutorial.rst
intl/l10n/mozILocalization.idl
intl/l10n/test/test_localization.js
intl/l10n/test/test_localization_sync.js
intl/l10n/test/test_pseudo.js
intl/l10n/test/xpcshell.ini
toolkit/components/mozintl/mozIntl.jsm
tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js
xpcom/ds/StaticAtoms.py
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -4,18 +4,16 @@
 
 "use strict";
 
 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",
                                "resource:///modules/MigrationUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -1,13 +1,12 @@
 /* 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/. */
 "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");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 async function getAddonInfo() {
   try {
     let {content, source} = await AttributionCode.getAttrDataAsync();
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -8,17 +8,16 @@
 /* import-globals-from ../../../base/content/aboutDialog-appUpdater.js */
 
 var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var {Downloads} = ChromeUtils.import("resource://gre/modules/Downloads.jsm");
 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");
 ChromeUtils.defineModuleGetter(this, "SelectionChangedMenulist",
                                "resource:///modules/SelectionChangedMenulist.jsm");
 ChromeUtils.defineModuleGetter(this, "UpdateUtils",
   "resource://gre/modules/UpdateUtils.jsm");
 
--- a/browser/components/touchbar/MacTouchBar.js
+++ b/browser/components/touchbar/MacTouchBar.js
@@ -4,17 +4,16 @@
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
-  Localization: "resource://gre/modules/Localization.jsm",
 });
 
 /**
  * Executes a XUL command on the top window. Called by the callbacks in each
  * TouchBarInput.
  * @param {string} commandName
  *        A XUL command.
  * @param {string} telemetryKey
--- a/dom/l10n/DOMLocalization.cpp
+++ b/dom/l10n/DOMLocalization.cpp
@@ -304,24 +304,65 @@ already_AddRefed<Promise> DOMLocalizatio
     }
   }
 
   RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  RefPtr<Promise> callbackResult = FormatMessages(cx, l10nKeys, aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    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);
   }
 
-  nativeHandler->SetReturnValuePromise(promise);
-  callbackResult->AppendNativeHandler(nativeHandler);
-
   return MaybeWrapPromise(promise);
 }
 
 /**
  * Promise handler used to set localization data on
  * roots of elements that got successfully translated.
  */
 class L10nRootTranslationHandler final : public PromiseNativeHandler {
--- a/dom/l10n/DocumentL10n.cpp
+++ b/dom/l10n/DocumentL10n.cpp
@@ -29,16 +29,21 @@ NS_IMPL_RELEASE_INHERITED(DocumentL10n, 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n)
 NS_INTERFACE_MAP_END_INHERITING(DOMLocalization)
 
 DocumentL10n::DocumentL10n(Document* aDocument)
     : DOMLocalization(aDocument->GetScopeObject()),
       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) {
   DOMLocalization::Init(aResourceIds, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
   mReady = Promise::Create(mGlobal, aRv);
@@ -128,13 +133,21 @@ void DocumentL10n::InitialDocumentTransl
   mState = DocumentL10nState::InitialTranslationCompleted;
 
   mDocument->InitialDocumentTranslationCompleted();
 
   // In XUL scenario contentSink is nullptr.
   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; }
 
 void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); }
\ No newline at end of file
--- a/dom/l10n/tests/mochitest/chrome.ini
+++ b/dom/l10n/tests/mochitest/chrome.ini
@@ -31,10 +31,11 @@
 [dom_localization/test_domloc.xul]
 
 
 [document_l10n/test_docl10n.xul]
 [document_l10n/test_docl10n_initialize_after_parse.xhtml]
 [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]
new file mode 100644
--- /dev/null
+++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.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>
--- a/intl/l10n/L10nRegistry.jsm
+++ b/intl/l10n/L10nRegistry.jsm
@@ -76,17 +76,17 @@ const isParentProcess = appinfo.processT
  * on the next in case of a missing string or error.
  *
  * If during the life-cycle of the app a new source is added, the generator can be called again
  * and will produce a new set of permutations placing the language pack provided resources
  * at the top.
  *
  * 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.
  */
 class L10nRegistryService {
   constructor() {
     this.sources = new Map();
 
     if (isParentProcess) {
--- a/intl/l10n/Localization.cpp
+++ b/intl/l10n/Localization.cpp
@@ -29,24 +29,26 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(Localiz
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Localization)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
   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();
 }
 
 void Localization::Init(nsTArray<nsString>& aResourceIds,
                         JS::Handle<JS::Value> aGenerateMessages,
                         ErrorResult& aRv) {
--- a/intl/l10n/Localization.h
+++ b/intl/l10n/Localization.h
@@ -69,14 +69,15 @@ class Localization : public nsIObserver,
   void OnChange();
   already_AddRefed<Promise> MaybeWrapPromise(Promise* aInnerPromise);
   void ConvertL10nArgsToJSValue(JSContext* aCx, const L10nArgs& aArgs,
                                 JS::MutableHandle<JS::Value> aRetVal,
                                 ErrorResult& aRv);
 
   nsCOMPtr<nsIGlobalObject> mGlobal;
   nsCOMPtr<mozILocalization> mLocalization;
+  bool mIsSync;
 };
 
 }  // namespace intl
 }  // namespace mozilla
 
 #endif
\ No newline at end of file
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -206,30 +206,38 @@ function maybeReportErrorToGecko(error) 
 /**
  * The `Localization` class is a central high-level API for vanilla
  * JavaScript use of Fluent.
  * It combines language negotiation, FluentBundle and I/O to
  * provide a scriptable API to format translations.
  */
 class Localization {
   /**
-   * @param {Array<String>} resourceIds     - List of resource IDs
-   * @param {Function}      generateBundles - Function that returns a
-   *                                          generator over FluentBundles
+   * @param {Array<String>} 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);
+    }
   }
 
   /**
    * @param {Array<String>} resourceIds - List of resource IDs
    * @param {bool}                eager - whether the I/O for new context should
    *                                      begin eagerly
    */
   addResourceIds(resourceIds, eager = false) {
@@ -276,16 +284,56 @@ class Localization {
     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 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.
    *
    * The fallback logic is the same as in `formatValues` but the argument type
    * is stricter (an array of arrays) and it returns {value, attributes}
    * objects which are suitable for the translation of DOM elements.
    *
    *     docL10n.formatMessages([
    *       {id: 'hello', args: { who: 'Mary' }},
@@ -295,27 +343,40 @@ class Localization {
    *     // [
    *     //   { value: 'Hello, Mary!', attributes: null },
    *     //   {
    *     //     value: 'Welcome!',
    *     //     attributes: [ { name: "title", value: 'Hello' } ]
    *     //   }
    *     // ]
    *
-   * 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}>>}
    * @private
    */
   formatMessages(keys) {
     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.
    *
    * A generalized version of `Localization.formatValue`. Keys can
    * either be simple string identifiers or `[id, args]` arrays.
    *
    *     docL10n.formatValues([
    *       {id: 'hello', args: { who: 'Mary' }},
    *       {id: 'hello', args: { who: 'John' }},
@@ -329,43 +390,70 @@ class Localization {
    * @param   {Array<Object>} keys
    * @returns {Promise<Array<string>>}
    */
   formatValues(keys) {
     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.
    *
    * If passed, `args` is a simple hash object with a list of variables that
    * will be interpolated in the value of the translation.
    *
    *     docL10n.formatValue(
    *       'hello', { who: 'world' }
    *     ).then(console.log);
    *
    *     // '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
    * notifications.
    *
    * @param   {string}  id     - Identifier of the translation to format
    * @param   {Object}  [args] - Optional external arguments
    * @returns {Promise<string>}
    */
   async formatValue(id, args) {
     const [val] = await this.formatValues([{id, args}]);
     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
    */
   registerObservers() {
     Services.obs.addObserver(this, "intl:app-locales-changed", true);
     Services.prefs.addObserver("intl.l10n.pseudo", this, true);
   }
 
   /**
@@ -393,75 +481,41 @@ class Localization {
 
   /**
    * This method should be called when there's a reason to believe
    * that language negotiation or available resources changed.
    *
    * @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
       // we want to eagerly fetch just that one.
       // Otherwise, we're in a scenario where the first locale may
       // be partial and we want to eagerly fetch a fallback as well.
       const appLocale = Services.locale.appLocaleAsBCP47;
       const lastFallback = Services.locale.lastFallbackLocale;
       const prefetchCount = appLocale === lastFallback ? 1 : 2;
       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.
  *
  * This function is passed as a method to `keysFromBundle` and resolve
  * a value of a single L10n Entity using provided `FluentBundle`.
  *
  * If the function fails to retrieve the entity, it will return an ID of it.
  * If formatting fails, it will return a partially resolved entity.
@@ -572,27 +626,25 @@ function keysFromBundle(method, bundle, 
         const errors = messageErrors.join(", ");
         maybeReportErrorToGecko(`[fluent][resolver] errors in ${locale}/${id}: ${errors}.`);
       }
     } else {
       missingIds.add(id);
     }
   });
 
-
   return missingIds;
 }
 
 /**
  * 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"];
--- a/intl/l10n/docs/fluent_tutorial.rst
+++ b/intl/l10n/docs/fluent_tutorial.rst
@@ -535,39 +535,65 @@ In almost all scenarios that's sufficien
 In rare edge cases where the developer needs to fetch additional resources, or
 the same resources in another language, it is possible to create additional
 contexts manually using the `Localization` class:
 
 .. code-block:: javascript
 
   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
 
   An example of a use case is the Preferences UI in Firefox, which uses the
   main context to localize the UI but also to build a search index.
 
   It is common to build such search index both in a current language and additionally
   in English, since a lot of documentation and online help exist only in English.
 
   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
 ==========================
 
 When designing localizable APIs, the most important rule is to resolve localization as
 late as possible. That means that instead of resolving strings somewhere deep in the
 codebase and then passing them on, or even caching, it is highly recommended to pass
 around :code:`l10n-id` or :code:`[l10n-id, l10n-args]` pairs until the top-most code
 resolves them or applies them onto the DOM element.
--- a/intl/l10n/mozILocalization.idl
+++ b/intl/l10n/mozILocalization.idl
@@ -1,25 +1,34 @@
 /* -*- Mode: IDL; 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/. */
 
+/**
+ * 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)]
 interface mozILocalization : nsISupports
 {
   unsigned long addResourceIds(in Array<AString> resourceIds, in bool aEager);
   unsigned long removeResourceIds(in Array<AString> resourceIds);
   void onChange();
 
   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);
 };
--- a/intl/l10n/test/test_localization.js
+++ b/intl/l10n/test/test_localization.js
@@ -30,17 +30,17 @@ add_task(async function test_methods_cal
   L10nRegistry.registerSource(source);
 
   async function* generateMessages(resIds) {
     yield * await L10nRegistry.generateBundles(["de", "en-US"], resIds);
   }
 
   const l10n = new Localization([
     "/browser/menu.ftl",
-  ], generateMessages);
+  ], false, generateMessages);
 
   let values = await l10n.formatValues([{id: "key"}, {id: "key2"}]);
 
   equal(values[0], "[de] Value2");
   equal(values[1], "[en] Value3");
 
   L10nRegistry.sources.clear();
   L10nRegistry.load = originalLoad;
@@ -76,17 +76,17 @@ key = { PLATFORM() ->
   L10nRegistry.registerSource(source);
 
   async function* generateMessages(resIds) {
     yield * await L10nRegistry.generateBundles(["en-US"], resIds);
   }
 
   const l10n = new Localization([
     "/test.ftl",
-  ], generateMessages);
+  ], false, generateMessages);
 
   let values = await l10n.formatValues([{id: "key"}]);
 
   ok(values[0].includes(
     `${ known_platforms[AppConstants.platform].toUpperCase() } Value`));
 
   L10nRegistry.sources.clear();
   L10nRegistry.load = originalLoad;
@@ -109,17 +109,17 @@ add_task(async function test_add_remove_
 
   const source = new FileSource("test", ["en-US"], "/localization/{locale}");
   L10nRegistry.registerSource(source);
 
   async function* generateMessages(resIds) {
     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"}]);
 
   equal(values[0], "Value1");
   equal(values[1], undefined);
 
   l10n.addResourceIds(["/toolkit/menu.ftl"]);
 
@@ -134,8 +134,86 @@ add_task(async function test_add_remove_
 
   equal(values[0], undefined);
   equal(values[1], "Value2");
 
   L10nRegistry.sources.clear();
   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;
+});
new file mode 100644
--- /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
--- a/intl/l10n/test/test_pseudo.js
+++ b/intl/l10n/test/test_pseudo.js
@@ -41,17 +41,17 @@ key = This is a single message
  */
 add_task(async function test_accented_works() {
   Services.prefs.setStringPref("intl.l10n.pseudo", "");
 
   let generateMessages = addMockFileSource();
 
   const l10n = new Localization([
     "/browser/menu.ftl",
-  ], generateMessages);
+  ], false, generateMessages);
   l10n.registerObservers();
 
   {
     // 1. Start with no pseudo
 
     let message = (await l10n.formatMessages([{id: "key"}]))[0];
 
     ok(message.value.includes("This is a single message"));
@@ -103,17 +103,17 @@ add_task(async function test_accented_wo
  */
 add_task(async function test_unavailable_strategy_works() {
   Services.prefs.setStringPref("intl.l10n.pseudo", "");
 
   let generateMessages = addMockFileSource();
 
   const l10n = new Localization([
     "/browser/menu.ftl",
-  ], generateMessages);
+  ], false, generateMessages);
   l10n.registerObservers();
 
   {
     // 1. Set unavailable pseudo strategy
     Services.prefs.setStringPref("intl.l10n.pseudo", "unknown-strategy");
 
     let message = (await l10n.formatMessages([{id: "key"}]))[0];
 
--- a/intl/l10n/test/xpcshell.ini
+++ b/intl/l10n/test/xpcshell.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 head =
 
 [test_l10nregistry.js]
 [test_l10nregistry_sync.js]
 [test_localization.js]
+[test_localization_sync.js]
 [test_messagecontext.js]
 [test_pseudo.js]
--- a/toolkit/components/mozintl/mozIntl.jsm
+++ b/toolkit/components/mozintl/mozIntl.jsm
@@ -1,14 +1,14 @@
 /* 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/. */
 
 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);
 const osPrefs =
   Cc["@mozilla.org/intl/ospreferences;1"].getService(Ci.mozIOSPreferences);
 
 /**
  * RegExp used to parse a BCP47 language tag (ex: en-US, sr-Cyrl-RU etc.)
@@ -264,56 +264,56 @@ class MozIntl {
   }
 
   getLanguageDisplayNames(locales, langCodes) {
     if (locales !== undefined) {
       throw new Error("First argument support not implemented yet");
     }
 
     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;
     }
 
     const loc = this._cache.languageLocalization;
 
     return langCodes.map(langCode => {
       if (typeof langCode !== "string") {
         throw new TypeError("All language codes must be strings.");
       }
       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;
         }
       }
       return lcLangCode;
     });
   }
 
   getRegionDisplayNames(locales, regionCodes) {
     if (locales !== undefined) {
       throw new Error("First argument support not implemented yet");
     }
 
     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;
     }
 
     const loc = this._cache.regionLocalization;
 
     return regionCodes.map(regionCode => {
       if (typeof regionCode !== "string") {
         throw new TypeError("All region codes must be strings.");
       }
       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;
         }
       }
       return regionCode.toUpperCase();
     });
   }
 
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js
@@ -307,16 +307,17 @@ module.exports = {
     "InstallTriggerImpl": false,
     "IntersectionObserver": false,
     "IntersectionObserverEntry": false,
     "JSWindowActorChild": false,
     "JSWindowActorParent": false,
     "KeyEvent": false,
     "KeyboardEvent": false,
     "KeyframeEffect": false,
+    "Localization": false,
     "Location": false,
     "MIDIAccess": false,
     "MIDIConnectionEvent": false,
     "MIDIInput": false,
     "MIDIInputMap": false,
     "MIDIMessageEvent": false,
     "MIDIOutput": false,
     "MIDIOutputMap": false,
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -284,16 +284,17 @@ STATIC_ATOMS = [
     Atom("cycler", "cycler"),
     Atom("dashed", "dashed"),
     Atom("data", "data"),
     Atom("datalist", "datalist"),
     Atom("datal10nid", "data-l10n-id"),
     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"),
     Atom("datetime", "datetime"),
     Atom("dd", "dd"),
     Atom("decimal", "decimal"),
     Atom("decimalFormat", "decimal-format"),
     Atom("decimalSeparator", "decimal-separator"),