--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -11,35 +11,38 @@
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/* fluent@0.6.3 */
+/* fluent-dom@0.2.0 */
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
// Match the opening angle bracket (<) in HTML tags, and HTML entities like
// &, &, &.
const reOverlay = /<|&#?\w+;/;
/**
- * The list of elements that are allowed to be inserted into a localization.
+ * Elements allowed in translations even if they are not present in the source
+ * HTML. They are text-level elements as defined by the HTML5 spec:
+ * https://www.w3.org/TR/html5/text-level-semantics.html with the exception of:
*
- * Source: https://www.w3.org/TR/html5/text-level-semantics.html
+ * - a - because we don't allow href on it anyways,
+ * - ruby, rt, rp - because we don't allow nested elements to be inserted.
*/
-const LOCALIZABLE_ELEMENTS = {
+const TEXT_LEVEL_ELEMENTS = {
"http://www.w3.org/1999/xhtml": [
- "a", "em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data",
+ "em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data",
"time", "code", "var", "samp", "kbd", "sub", "sup", "i", "b", "u",
- "mark", "ruby", "rt", "rp", "bdi", "bdo", "span", "br", "wbr"
+ "mark", "bdi", "bdo", "span", "br", "wbr"
],
};
const LOCALIZABLE_ATTRIBUTES = {
"http://www.w3.org/1999/xhtml": {
global: ["title", "aria-label", "aria-valuetext", "aria-moz-hint"],
a: ["download"],
area: ["download", "alt"],
@@ -61,177 +64,196 @@ const LOCALIZABLE_ATTRIBUTES = {
key: ["key", "keycode"],
textbox: ["placeholder"],
toolbarbutton: ["tooltiptext"],
}
};
/**
- * Overlay translation onto a DOM element.
+ * Translate an element.
*
- * @param {Element} targetElement
- * @param {string|Object} translation
+ * Translate the element's text content and attributes. Some HTML markup is
+ * allowed in the translation. The element's children with the data-l10n-name
+ * attribute will be treated as arguments to the translation. If the
+ * translation defines the same children, their attributes and text contents
+ * will be used for translating the matching source child.
+ *
+ * @param {Element} element
+ * @param {Object} translation
* @private
*/
-function overlayElement(targetElement, translation) {
+function translateElement(element, translation) {
const value = translation.value;
if (typeof value === "string") {
if (!reOverlay.test(value)) {
// If the translation doesn't contain any markup skip the overlay logic.
- targetElement.textContent = value;
+ element.textContent = value;
} else {
// Else parse the translation's HTML using an inert template element,
- // sanitize it and replace the targetElement's content.
- const templateElement = targetElement.ownerDocument.createElementNS(
- "http://www.w3.org/1999/xhtml", "template");
+ // sanitize it and replace the element's content.
+ const templateElement = element.ownerDocument.createElementNS(
+ "http://www.w3.org/1999/xhtml", "template"
+ );
// eslint-disable-next-line no-unsanitized/property
templateElement.innerHTML = value;
- targetElement.appendChild(
- // The targetElement will be cleared at the end of sanitization.
- sanitizeUsing(templateElement.content, targetElement)
- );
+ overlayChildNodes(templateElement.content, element);
}
}
- const explicitlyAllowed = targetElement.hasAttribute("data-l10n-attrs")
- ? targetElement.getAttribute("data-l10n-attrs")
+ // Even if the translation doesn't define any localizable attributes, run
+ // overlayAttributes to remove any localizable attributes set by previous
+ // translations.
+ overlayAttributes(translation, element);
+}
+
+/**
+ * Replace child nodes of an element with child nodes of another element.
+ *
+ * The contents of the target element will be cleared and fully replaced with
+ * sanitized contents of the source element.
+ *
+ * @param {DocumentFragment} fromElement - The source of children to overlay.
+ * @param {Element} toElement - The target of the overlay.
+ * @private
+ */
+function overlayChildNodes(fromElement, toElement) {
+ const content = toElement.ownerDocument.createDocumentFragment();
+
+ for (const childNode of fromElement.childNodes) {
+ content.appendChild(sanitizeUsing(toElement, childNode));
+ }
+
+ toElement.textContent = "";
+ toElement.appendChild(content);
+}
+
+/**
+ * Transplant localizable attributes of an element to another element.
+ *
+ * Any localizable attributes already set on the target element will be
+ * cleared.
+ *
+ * @param {Element|Object} fromElement - The source of child nodes to overlay.
+ * @param {Element} toElement - The target of the overlay.
+ * @private
+ */
+function overlayAttributes(fromElement, toElement) {
+ const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs")
+ ? toElement.getAttribute("data-l10n-attrs")
.split(",").map(i => i.trim())
: null;
- // Remove localizable attributes which may have been set by a previous
- // translation.
- for (const attr of Array.from(targetElement.attributes)) {
- if (isAttrNameLocalizable(attr.name, targetElement, explicitlyAllowed)) {
- targetElement.removeAttribute(attr.name);
+ // Remove existing localizable attributes.
+ for (const attr of Array.from(toElement.attributes)) {
+ if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed)) {
+ toElement.removeAttribute(attr.name);
}
}
- if (translation.attrs) {
- for (const {name, value} of translation.attrs) {
- if (isAttrNameLocalizable(name, targetElement, explicitlyAllowed)) {
- targetElement.setAttribute(name, value);
- }
+ // fromElement might be a {value, attributes} object as returned by
+ // Localization.messageFromContext. In which case attributes may be null to
+ // save GC cycles.
+ if (!fromElement.attributes) {
+ return;
+ }
+
+ // Set localizable attributes.
+ for (const attr of Array.from(fromElement.attributes)) {
+ if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed)) {
+ toElement.setAttribute(attr.name, attr.value);
}
}
}
/**
- * Sanitize `translationFragment` using `sourceElement` to add functional
- * HTML attributes to children. `sourceElement` will have all its child nodes
- * removed.
+ * Sanitize a child node created by the translation.
*
- * The sanitization is conducted according to the following rules:
- *
- * - Allow text nodes.
- * - Replace forbidden children with their textContent.
- * - Remove forbidden attributes from allowed children.
+ * If childNode has the data-l10n-name attribute, try to find a corresponding
+ * child in sourceElement and use it as the base for the sanitization. This
+ * will preserve functional attribtues defined on the child element in the
+ * source HTML.
*
- * Additionally when a child of the same type is present in `sourceElement` its
- * attributes will be merged into the translated child. Whitelisted attributes
- * of the translated child will then overwrite the ones present in the source.
- *
- * The overlay logic is subject to the following limitations:
+ * This function must return new nodes or clones in all code paths. The
+ * returned nodes are immediately appended to the intermediate DocumentFragment
+ * which also _removes_ them from the constructed <template> containing the
+ * translation, which in turn breaks the for…of iteration over its child nodes.
*
- * - Children are always cloned. Event handlers attached to them are lost.
- * - Nested HTML in source and in translations is not supported.
- * - Multiple children of the same type will be matched in order.
- *
- * @param {DocumentFragment} translationFragment
- * @param {Element} sourceElement
- * @returns {DocumentFragment}
+ * @param {Element} sourceElement - The source for data-l10n-name lookups.
+ * @param {Element} childNode - The child node to be sanitized.
+ * @returns {Element}
* @private
*/
-function sanitizeUsing(translationFragment, sourceElement) {
- const ownerDocument = translationFragment.ownerDocument;
- // Take one node from translationFragment at a time and check it against
- // the allowed list or try to match it with a corresponding element
- // in the source.
- for (const childNode of translationFragment.childNodes) {
-
- if (childNode.nodeType === childNode.TEXT_NODE) {
- continue;
- }
-
- // If the child is forbidden just take its textContent.
- if (!isElementLocalizable(childNode)) {
- const text = ownerDocument.createTextNode(childNode.textContent);
- translationFragment.replaceChild(text, childNode);
- continue;
- }
-
- // Start the sanitization with an empty element.
- const mergedChild = ownerDocument.createElement(childNode.localName);
-
- // Explicitly discard nested HTML by serializing childNode to a TextNode.
- mergedChild.textContent = childNode.textContent;
-
- // If a child of the same type exists in sourceElement, take its functional
- // (i.e. non-localizable) attributes. This also removes the child from
- // sourceElement.
- const sourceChild = shiftNamedElement(sourceElement, childNode.localName);
-
- // Find the union of all safe attributes: localizable attributes from
- // childNode and functional attributes from sourceChild.
- const safeAttributes = sanitizeAttrsUsing(childNode, sourceChild);
-
- for (const attr of safeAttributes) {
- mergedChild.setAttribute(attr.name, attr.value);
- }
-
- translationFragment.replaceChild(mergedChild, childNode);
+function sanitizeUsing(sourceElement, childNode) {
+ if (childNode.nodeType === childNode.TEXT_NODE) {
+ return childNode.cloneNode(false);
}
- // SourceElement might have been already modified by shiftNamedElement.
- // Let's clear it to make sure other code doesn't rely on random leftovers.
- sourceElement.textContent = "";
-
- return translationFragment;
-}
+ if (childNode.hasAttribute("data-l10n-name")) {
+ const childName = childNode.getAttribute("data-l10n-name");
+ const sourceChild = sourceElement.querySelector(
+ `[data-l10n-name="${childName}"]`
+ );
-/**
- * Sanitize and merge attributes.
- *
- * Only localizable attributes from the translated child element and only
- * functional attributes from the source child element are considered safe.
- *
- * @param {Element} translatedElement
- * @param {Element} sourceElement
- * @returns {Array<Attr>}
- * @private
- */
-function sanitizeAttrsUsing(translatedElement, sourceElement) {
- const localizedAttrs = Array.from(translatedElement.attributes).filter(
- attr => isAttrNameLocalizable(attr.name, translatedElement)
+ if (!sourceChild) {
+ console.warn(
+ `An element named "${childName}" wasn't found in the source.`
+ );
+ } else if (sourceChild.localName !== childNode.localName) {
+ console.warn(
+ `An element named "${childName}" was found in the translation ` +
+ `but its type ${childNode.localName} didn't match the element ` +
+ `found in the source (${sourceChild.localName}).`
+ );
+ } else {
+ // Remove it from sourceElement so that the translation cannot use
+ // the same reference name again.
+ sourceElement.removeChild(sourceChild);
+ // We can't currently guarantee that a translation won't remove
+ // sourceChild from the element completely, which could break the app if
+ // it relies on an event handler attached to the sourceChild. Let's make
+ // this limitation explicit for now by breaking the identitiy of the
+ // sourceChild by cloning it. This will destroy all event handlers
+ // attached to sourceChild via addEventListener and via on<name>
+ // properties.
+ const clone = sourceChild.cloneNode(false);
+ return shallowPopulateUsing(childNode, clone);
+ }
+ }
+
+ if (isElementAllowed(childNode)) {
+ // Start with an empty element of the same type to remove nested children
+ // and non-localizable attributes defined by the translation.
+ const clone = childNode.ownerDocument.createElement(childNode.localName);
+ return shallowPopulateUsing(childNode, clone);
+ }
+
+ console.warn(
+ `An element of forbidden type "${childNode.localName}" was found in ` +
+ "the translation. Only elements with data-l10n-name can be overlaid " +
+ "onto source elements of the same data-l10n-name."
);
- if (!sourceElement) {
- return localizedAttrs;
- }
-
- const functionalAttrs = Array.from(sourceElement.attributes).filter(
- attr => !isAttrNameLocalizable(attr.name, sourceElement)
- );
-
- return localizedAttrs.concat(functionalAttrs);
+ // If all else fails, convert the element to its text content.
+ return childNode.ownerDocument.createTextNode(childNode.textContent);
}
/**
* Check if element is allowed in the translation.
*
* This method is used by the sanitizer when the translation markup contains
* an element which is not present in the source code.
*
* @param {Element} element
* @returns {boolean}
* @private
*/
-function isElementLocalizable(element) {
- const allowed = LOCALIZABLE_ELEMENTS[element.namespaceURI];
+function isElementAllowed(element) {
+ const allowed = TEXT_LEVEL_ELEMENTS[element.namespaceURI];
return allowed && allowed.includes(element.localName);
}
/**
* Check if attribute is allowed for the given element.
*
* This method is used by the sanitizer when the translation markup contains
* DOM attributes, or when the translation has traits which map to DOM
@@ -282,31 +304,27 @@ function isAttrNameLocalizable(name, ele
return true;
}
}
return false;
}
/**
- * Remove and return the first child of the given type.
+ * Helper to set textContent and localizable attributes on an element.
*
- * @param {DOMFragment} element
- * @param {string} localName
- * @returns {Element | null}
+ * @param {Element} fromElement
+ * @param {Element} toElement
+ * @returns {Element}
* @private
*/
-function shiftNamedElement(element, localName) {
- for (const child of element.children) {
- if (child.localName === localName) {
- element.removeChild(child);
- return child;
- }
- }
- return null;
+function shallowPopulateUsing(fromElement, toElement) {
+ toElement.textContent = fromElement.textContent;
+ overlayAttributes(fromElement, toElement);
+ return toElement;
}
/**
* Sanitizes a translation before passing them to Node.localize API.
*
* It returns `false` if the translation contains DOM Overlays and should
* not go into Node.localize.
*
@@ -327,29 +345,28 @@ function shiftNamedElement(element, loca
* @returns boolean
* @private
*/
function sanitizeTranslationForNodeLocalize(l10nItem, translation) {
if (reOverlay.test(translation.value)) {
return false;
}
- if (translation.attrs) {
+ if (translation.attributes) {
const explicitlyAllowed = l10nItem.l10nAttrs === null ? null :
l10nItem.l10nAttrs.split(",").map(i => i.trim());
- for (const [j, {name}] of translation.attrs.entries()) {
+ for (const [j, {name}] of translation.attributes.entries()) {
if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) {
- translation.attrs.splice(j, 1);
+ translation.attributes.splice(j, 1);
}
}
}
return true;
}
-
const L10NID_ATTR_NAME = "data-l10n-id";
const L10NARGS_ATTR_NAME = "data-l10n-args";
const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`;
/**
* The `DOMLocalization` class is responsible for fetching resources and
* formatting translations.
@@ -538,17 +555,19 @@ class DOMLocalization extends Localizati
* Translate mutations detected by the `MutationObserver`.
*
* @private
*/
translateMutations(mutations) {
for (const mutation of mutations) {
switch (mutation.type) {
case "attributes":
- this.pendingElements.add(mutation.target);
+ if (mutation.target.hasAttribute("data-l10n-id")) {
+ this.pendingElements.add(mutation.target);
+ }
break;
case "childList":
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
if (addedNode.childElementCount) {
for (const element of this.getTranslatables(addedNode)) {
this.pendingElements.add(element);
}
@@ -629,17 +648,17 @@ class DOMLocalization extends Localizati
return translations;
};
return frag.localize(getTranslationsForItems.bind(this))
.then(untranslatedElements => {
for (let i = 0; i < overlayTranslations.length; i++) {
if (overlayTranslations[i] !== undefined &&
untranslatedElements[i] !== undefined) {
- overlayElement(untranslatedElements[i], overlayTranslations[i]);
+ translateElement(untranslatedElements[i], overlayTranslations[i]);
}
}
this.resumeObserving();
})
.catch(() => this.resumeObserving());
}
return this.translateElements(this.getTranslatables(frag));
}
@@ -674,17 +693,17 @@ class DOMLocalization extends Localizati
* @param {Array<Object>} translations
* @private
*/
applyTranslations(elements, translations) {
this.pauseObserving();
for (let i = 0; i < elements.length; i++) {
if (translations[i] !== undefined) {
- overlayElement(elements[i], translations[i]);
+ translateElement(elements[i], translations[i]);
}
}
this.resumeObserving();
}
/**
* Collects all translatable child elements of the element.
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -11,17 +11,17 @@
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/* fluent@0.6.3 */
+/* fluent-dom@0.2.0 */
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global console */
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
@@ -322,25 +322,25 @@ function valueFromContext(ctx, errors, i
* @returns {Object}
* @private
*/
function messageFromContext(ctx, errors, id, args) {
const msg = ctx.getMessage(id);
const formatted = {
value: ctx.format(msg, args, errors),
- attrs: null,
+ attributes: null,
};
if (msg.attrs) {
- formatted.attrs = [];
- for (const name in msg.attrs) {
- const value = ctx.format(msg.attrs[name], args, errors);
+ formatted.attributes = [];
+ for (const [name, attr] of Object.entries(msg.attrs)) {
+ const value = ctx.format(attr, args, errors);
if (value !== null) {
- formatted.attrs.push({ name, value });
+ formatted.attributes.push({name, value});
}
}
}
return formatted;
}
/**
--- a/intl/l10n/MessageContext.jsm
+++ b/intl/l10n/MessageContext.jsm
@@ -1603,41 +1603,45 @@ function Pattern(env, ptn) {
errors.push(new RangeError("Cyclic reference"));
return new FluentNone();
}
// Tag the pattern as dirty for the purpose of the current resolution.
dirty.add(ptn);
const result = [];
+ // Wrap interpolations with Directional Isolate Formatting characters
+ // only when the pattern has more than one element.
+ const useIsolating = ctx._useIsolating && ptn.length > 1;
+
for (const elem of ptn) {
if (typeof elem === "string") {
result.push(elem);
continue;
}
const part = Type(env, elem).toString(ctx);
- if (ctx._useIsolating) {
+ if (useIsolating) {
result.push(FSI);
}
if (part.length > MAX_PLACEABLE_LENGTH) {
errors.push(
new RangeError(
"Too many characters in placeable " +
`(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
)
);
result.push(part.slice(MAX_PLACEABLE_LENGTH));
} else {
result.push(part);
}
- if (ctx._useIsolating) {
+ if (useIsolating) {
result.push(PDI);
}
}
dirty.delete(ptn);
return result.join("");
}
@@ -1770,18 +1774,26 @@ class MessageContext {
* @returns {Array<Error>}
*/
addMessages(source) {
const [entries, errors] = parse(source);
for (const id in entries) {
if (id.startsWith("-")) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from MessageContext.
+ if (this._terms.has(id)) {
+ errors.push(`Attempt to override an existing term: "${id}"`);
+ continue;
+ }
this._terms.set(id, entries[id]);
} else {
+ if (this._messages.has(id)) {
+ errors.push(`Attempt to override an existing message: "${id}"`);
+ continue;
+ }
this._messages.set(id, entries[id]);
}
}
return errors;
}
/**
--- a/intl/l10n/fluent.js.patch
+++ b/intl/l10n/fluent.js.patch
@@ -1,26 +1,145 @@
diff -uNr ./dist/DOMLocalization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm
---- ./dist/DOMLocalization.jsm 2018-01-30 13:46:58.589811108 -0800
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm 2018-01-30 13:46:13.613146435 -0800
-@@ -18,7 +18,8 @@
+--- ./dist/DOMLocalization.jsm 2018-04-13 08:25:21.143138950 -0700
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm 2018-04-13 08:27:11.658083766 -0700
+@@ -18,10 +18,8 @@
- /* fluent@0.6.0 */
+ /* fluent-dom@0.2.0 */
-import Localization from '../../fluent-dom/src/localization.js';
+-
+-/* eslint no-console: ["error", {allow: ["warn"]}] */
+-/* global console */
+const { Localization } =
-+ Components.utils.import("resource://gre/modules/Localization.jsm", {});
++ ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
// Match the opening angle bracket (<) in HTML tags, and HTML entities like
// &, &, &.
-@@ -623,36 +624,5 @@
+@@ -96,6 +94,7 @@
+ const templateElement = element.ownerDocument.createElementNS(
+ "http://www.w3.org/1999/xhtml", "template"
+ );
++ // eslint-disable-next-line no-unsanitized/property
+ templateElement.innerHTML = value;
+ overlayChildNodes(templateElement.content, element);
+ }
+@@ -323,6 +322,46 @@
+ return toElement;
+ }
+
++/**
++ * Sanitizes a translation before passing them to Node.localize API.
++ *
++ * It returns `false` if the translation contains DOM Overlays and should
++ * not go into Node.localize.
++ *
++ * Note: There's a third item of work that JS DOM Overlays do - removal
++ * of attributes from the previous translation.
++ * This is not trivial to implement for Node.localize scenario, so
++ * at the moment it is not supported.
++ *
++ * @param {{
++ * localName: string,
++ * namespaceURI: string,
++ * type: string || null
++ * l10nId: string,
++ * l10nArgs: Array<Object> || null,
++ * l10nAttrs: string ||null,
++ * }} l10nItems
++ * @param {{value: string, attrs: Object}} translations
++ * @returns boolean
++ * @private
++ */
++function sanitizeTranslationForNodeLocalize(l10nItem, translation) {
++ if (reOverlay.test(translation.value)) {
++ return false;
++ }
++
++ if (translation.attributes) {
++ const explicitlyAllowed = l10nItem.l10nAttrs === null ? null :
++ l10nItem.l10nAttrs.split(",").map(i => i.trim());
++ for (const [j, {name}] of translation.attributes.entries()) {
++ if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) {
++ translation.attributes.splice(j, 1);
++ }
++ }
++ }
++ return true;
++}
++
+ const L10NID_ATTR_NAME = "data-l10n-id";
+ const L10NARGS_ATTR_NAME = "data-l10n-args";
+
+@@ -568,6 +607,59 @@
+ * @returns {Promise}
+ */
+ translateFragment(frag) {
++ if (frag.localize) {
++ // This is a temporary fast-path offered by Gecko to workaround performance
++ // issues coming from Fluent and XBL+Stylo performing unnecesary
++ // operations during startup.
++ // For details see bug 1441037, bug 1442262, and bug 1363862.
++
++ // A sparse array which will store translations separated out from
++ // all translations that is needed for DOM Overlay.
++ const overlayTranslations = [];
++
++ const getTranslationsForItems = async l10nItems => {
++ const keys = l10nItems.map(l10nItem => [l10nItem.l10nId, l10nItem.l10nArgs]);
++ const translations = await this.formatMessages(keys);
++
++ // Here we want to separate out elements that require DOM Overlays.
++ // Those elements will have to be translated using our JS
++ // implementation, while everything else is going to use the fast-path.
++ for (const [i, translation] of translations.entries()) {
++ if (translation === undefined) {
++ continue;
++ }
++
++ const hasOnlyText =
++ sanitizeTranslationForNodeLocalize(l10nItems[i], translation);
++ if (!hasOnlyText) {
++ // Removing from translations to make Node.localize skip it.
++ // We will translate it below using JS DOM Overlays.
++ overlayTranslations[i] = translations[i];
++ translations[i] = undefined;
++ }
++ }
++
++ // We pause translation observing here because Node.localize
++ // will translate the whole DOM next, using the `translations`.
++ //
++ // The observer will be resumed after DOM Overlays are localized
++ // in the next microtask.
++ this.pauseObserving();
++ return translations;
++ };
++
++ return frag.localize(getTranslationsForItems.bind(this))
++ .then(untranslatedElements => {
++ for (let i = 0; i < overlayTranslations.length; i++) {
++ if (overlayTranslations[i] !== undefined &&
++ untranslatedElements[i] !== undefined) {
++ translateElement(untranslatedElements[i], overlayTranslations[i]);
++ }
++ }
++ this.resumeObserving();
++ })
++ .catch(() => this.resumeObserving());
++ }
+ return this.translateElements(this.getTranslatables(frag));
+ }
+
+@@ -647,37 +739,5 @@
}
}
-/* global L10nRegistry, Services */
+-
-/**
- * The default localization strategy for Gecko. It comabines locales
- * available in L10nRegistry, with locales requested by the user to
- * generate the iterator over MessageContexts.
- *
- * In the future, we may want to allow certain modules to override this
- * with a different negotitation strategy to allow for the module to
- * be localized into a different language - for example DevTools.
@@ -42,50 +161,94 @@ diff -uNr ./dist/DOMLocalization.jsm /ho
- resourceIds,
- generateMessages = defaultGenerateMessages
- ) {
- super(windowElement, resourceIds, generateMessages);
- }
-}
-
-this.DOMLocalization = GeckoDOMLocalization;
+-this.EXPORTED_SYMBOLS = ["DOMLocalization"];
+this.DOMLocalization = DOMLocalization;
- this.EXPORTED_SYMBOLS = ['DOMLocalization'];
++var EXPORTED_SYMBOLS = ["DOMLocalization"];
diff -uNr ./dist/l10n.js /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js
---- ./dist/l10n.js 2018-01-30 13:46:58.749811101 -0800
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js 2018-01-26 20:52:09.106650798 -0800
-@@ -1,7 +1,6 @@
+--- ./dist/l10n.js 2018-04-13 08:25:21.307139138 -0700
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js 2018-04-13 08:27:25.230296529 -0700
+@@ -1,20 +1,26 @@
-/* global Components, document, window */
{
const { DOMLocalization } =
-- Components.utils.import('resource://gre/modules/DOMLocalization.jsm');
-+ Components.utils.import("resource://gre/modules/DOMLocalization.jsm");
+- Components.utils.import("resource://gre/modules/DOMLocalization.jsm");
++ ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
/**
* Polyfill for document.ready polyfill.
+ * See: https://github.com/whatwg/html/issues/127 for details.
+ *
++ * XXX: The callback is a temporary workaround for bug 1193394. Once Promises in Gecko
++ * start beeing a microtask and stop pushing translation post-layout, we can
++ * remove it and start using the returned Promise again.
++ *
++ * @param {Function} callback - function to be called when the document is ready.
+ * @returns {Promise}
+ */
+- function documentReady() {
++ function documentReady(callback) {
+ if (document.contentType === "application/vnd.mozilla.xul+xml") {
+ // XUL
+ return new Promise(
+ resolve => document.addEventListener(
+- "MozBeforeInitialXULLayout", resolve, { once: true }
++ "MozBeforeInitialXULLayout", () => {
++ resolve(callback());
++ }, { once: true }
+ )
+ );
+ }
+@@ -22,11 +28,13 @@
+ // HTML
+ const rs = document.readyState;
+ if (rs === "interactive" || rs === "completed") {
+- return Promise.resolve();
++ return Promise.resolve(callback);
+ }
+ return new Promise(
+ resolve => document.addEventListener(
+- "readystatechange", resolve, { once: true }
++ "readystatechange", () => {
++ resolve(callback());
++ }, { once: true }
+ )
+ );
+ }
+@@ -50,11 +58,8 @@
+ // trigger first context to be fetched eagerly
+ document.l10n.ctxs.touchNext();
+
+- document.l10n.ready = documentReady().then(() => {
++ document.l10n.ready = documentReady(() => {
+ document.l10n.registerObservers();
+- window.addEventListener("unload", () => {
+- document.l10n.unregisterObservers();
+- });
+ document.l10n.connectRoot(document.documentElement);
+ return document.l10n.translateRoots();
+ });
diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm
---- ./dist/Localization.jsm 2018-01-30 13:46:58.393144450 -0800
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm 2018-01-30 13:46:04.593146834 -0800
-@@ -18,92 +18,16 @@
+--- ./dist/Localization.jsm 2018-04-13 08:25:20.946138732 -0700
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm 2018-04-13 08:27:16.396155987 -0700
+@@ -18,70 +18,13 @@
- /* fluent@0.6.0 */
+ /* fluent-dom@0.2.0 */
-/* eslint no-magic-numbers: [0] */
-
-/* global Intl */
-
-/**
-- * The `FluentType` class is the base of Fluent's type system.
-- *
-- * Fluent types wrap JavaScript values and store additional configuration for
-- * them, which can then be used in the `toString` method together with a proper
-- * `Intl` formatter.
-- */
--
--/**
- * @overview
- *
- * The FTL resolver ships with a number of functions built-in.
- *
- * Each function take two arguments:
- * - args - an array of positional args
- * - opts - an object of key-value args
- *
@@ -137,40 +300,24 @@ diff -uNr ./dist/Localization.jsm /home/
- * * {Object} args
- * list of developer provided arguments that can be used
- * * {Array} errors
- * list of errors collected while resolving
- * * {WeakSet} dirty
- * Set of patterns already encountered during this resolution.
- * This is used to prevent cyclic resolutions.
- */
-+const Cu = Components.utils;
-+const Cc = Components.classes;
-+const Ci = Components.interfaces;
-
--/**
-- * Message contexts are single-language stores of translations. They are
-- * responsible for parsing translation resources in the Fluent syntax and can
-- * format translation units (entities) to strings.
-- *
-- * Always use `MessageContext.format` to retrieve translation units from
-- * a context. Translations can contain references to other entities or
-- * external arguments, conditional logic in form of select expressions, traits
-- * which describe their grammatical features, and can use Fluent builtins which
-- * make use of the `Intl` formatters to format numbers, dates, lists and more
-- * into the context's language. See the documentation of the Fluent syntax for
-- * more information.
-- */
-+const { L10nRegistry } = Cu.import("resource://gre/modules/L10nRegistry.jsm", {});
-+const LocaleService = Cc["@mozilla.org/intl/localeservice;1"].getService(Ci.mozILocaleService);
-+const ObserverService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
++const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
++const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
++const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
++const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
/*
* CachedIterable caches the elements yielded by an iterable.
-@@ -170,87 +94,6 @@
+@@ -148,58 +91,19 @@
}
}
-/*
- * @overview
- *
- * Functions for managing ordered sequences of MessageContexts.
- *
@@ -207,171 +354,133 @@ diff -uNr ./dist/Localization.jsm /home/
- * }
- *
- * const contexts = new CachedIterable(generateMessages());
- * const ctx = mapContextSync(contexts, id);
- *
- */
-
-/*
-- * Synchronously map an identifier or an array of identifiers to the best
-- * `MessageContext` instance(s).
-- *
-- * @param {Iterable} iterable
-- * @param {string|Array<string>} ids
-- * @returns {MessageContext|Array<MessageContext>}
-- */
--
--
--/*
-- * Asynchronously map an identifier or an array of identifiers to the best
-- * `MessageContext` instance(s).
-- *
-- * @param {AsyncIterable} iterable
-- * @param {string|Array<string>} ids
-- * @returns {Promise<MessageContext|Array<MessageContext>>}
-- */
--
--/**
-- * Template literal tag for dedenting FTL code.
-- *
-- * Strip the common indent of non-blank lines. Remove blank lines.
-- *
-- * @param {Array<string>} strings
-- */
--
--/*
- * @module fluent
- * @overview
- *
- * `fluent` is a JavaScript implementation of Project Fluent, a localization
- * framework designed to unleash the expressive power of the natural language.
-- *
-- */
--
--/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
--/* global console */
--
- /**
- * Specialized version of an Error used to indicate errors that are result
- * of a problem during the localization process.
-@@ -269,6 +112,26 @@
- }
- }
-
-+ /**
++/**
+ * The default localization strategy for Gecko. It comabines locales
+ * available in L10nRegistry, with locales requested by the user to
+ * generate the iterator over MessageContexts.
-+ *
+ *
+ * In the future, we may want to allow certain modules to override this
+ * with a different negotitation strategy to allow for the module to
+ * be localized into a different language - for example DevTools.
-+ */
+ */
+-
+-/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
+function defaultGenerateMessages(resourceIds) {
-+ const availableLocales = L10nRegistry.getAvailableLocales();
-+
-+ const requestedLocales = LocaleService.getRequestedLocales();
-+ const defaultLocale = LocaleService.defaultLocale;
-+ const locales = LocaleService.negotiateLanguages(
-+ requestedLocales, availableLocales, defaultLocale,
-+ );
-+ return L10nRegistry.generateContexts(locales, resourceIds);
++ const appLocales = Services.locale.getAppLocalesAsLangTags();
++ return L10nRegistry.generateContexts(appLocales, resourceIds);
+}
-+
+
/**
* The `Localization` class is a central high-level API for vanilla
- * JavaScript use of Fluent.
-@@ -283,7 +146,7 @@
+@@ -215,7 +119,7 @@
*
* @returns {Localization}
*/
- constructor(resourceIds, generateMessages) {
+ constructor(resourceIds, generateMessages = defaultGenerateMessages) {
this.resourceIds = resourceIds;
this.generateMessages = generateMessages;
this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds));
-@@ -303,7 +166,7 @@
- */
+@@ -236,7 +140,7 @@
async formatWithFallback(keys, method) {
const translations = [];
+
- for (let ctx of this.ctxs) {
+ for await (let ctx of this.ctxs) {
// This can operate on synchronous and asynchronous
// contexts coming from the iterator.
- if (typeof ctx.then === 'function') {
-@@ -394,8 +257,38 @@
+ if (typeof ctx.then === "function") {
+@@ -248,7 +152,7 @@
+ break;
+ }
+
+- if (typeof console !== "undefined") {
++ if (AppConstants.NIGHTLY_BUILD) {
+ const locale = ctx.locales[0];
+ const ids = Array.from(missingIds).join(", ");
+ console.warn(`Missing translations in ${locale}: ${ids}`);
+@@ -335,8 +239,28 @@
return val;
}
- handleEvent() {
- this.onLanguageChange();
+ /**
-+ * Register observers on events that will trigger cache invalidation
++ * Register weak observers on events that will trigger cache invalidation
+ */
+ registerObservers() {
-+ ObserverService.addObserver(this, 'l10n:available-locales-changed', false);
-+ ObserverService.addObserver(this, 'intl:requested-locales-changed', false);
-+ }
-+
-+ /**
-+ * Unregister observers on events that will trigger cache invalidation
-+ */
-+ unregisterObservers() {
-+ ObserverService.removeObserver(this, 'l10n:available-locales-changed');
-+ ObserverService.removeObserver(this, 'intl:requested-locales-changed');
++ Services.obs.addObserver(this, "intl:app-locales-changed", true);
+ }
+
+ /**
+ * Default observer handler method.
+ *
+ * @param {String} subject
+ * @param {String} topic
+ * @param {Object} data
+ */
+ observe(subject, topic, data) {
+ switch (topic) {
-+ case 'l10n:available-locales-changed':
-+ case 'intl:requested-locales-changed':
++ case "intl:app-locales-changed":
+ this.onLanguageChange();
+ break;
+ default:
+ break;
+ }
}
/**
-@@ -538,7 +431,8 @@
- hasErrors = true;
- }
+@@ -348,6 +272,10 @@
+ }
+ }
-- if (messageErrors.length && typeof console !== 'undefined') {
-+ if (messageErrors.length) {
-+ const { console } = Cu.import("resource://gre/modules/Console.jsm", {});
- messageErrors.forEach(error => console.warn(error));
- }
- });
-@@ -546,45 +440,5 @@
- return hasErrors;
++Localization.prototype.QueryInterface = XPCOMUtils.generateQI([
++ Ci.nsISupportsWeakReference
++]);
++
+ /**
+ * Format the value of a message into a string.
+ *
+@@ -368,6 +296,7 @@
+ */
+ function valueFromContext(ctx, errors, id, args) {
+ const msg = ctx.getMessage(id);
++
+ return ctx.format(msg, args, errors);
+ }
+
+@@ -467,44 +396,5 @@
+ return missingIds;
}
-/* global Components */
-/* eslint no-unused-vars: 0 */
-
-const Cu = Components.utils;
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
-const { L10nRegistry } =
-- Cu.import('resource://gre/modules/L10nRegistry.jsm', {});
+- Cu.import("resource://gre/modules/L10nRegistry.jsm", {});
-const ObserverService =
-- Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
+- Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
-const { Services } =
-- Cu.import('resource://gre/modules/Services.jsm', {});
--
+- Cu.import("resource://gre/modules/Services.jsm", {});
-
-/**
- * The default localization strategy for Gecko. It comabines locales
- * available in L10nRegistry, with locales requested by the user to
- * generate the iterator over MessageContexts.
- *
- * In the future, we may want to allow certain modules to override this
- * with a different negotitation strategy to allow for the module to
@@ -389,22 +498,32 @@ diff -uNr ./dist/Localization.jsm /home/
-
-class GeckoLocalization extends Localization {
- constructor(resourceIds, generateMessages = defaultGenerateMessages) {
- super(resourceIds, generateMessages);
- }
-}
-
-this.Localization = GeckoLocalization;
+-this.EXPORTED_SYMBOLS = ["Localization"];
+this.Localization = Localization;
- this.EXPORTED_SYMBOLS = ['Localization'];
++var EXPORTED_SYMBOLS = ["Localization"];
diff -uNr ./dist/MessageContext.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm
---- ./dist/MessageContext.jsm 2018-01-30 13:46:58.119811129 -0800
-+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm 2018-01-30 13:53:23.036460739 -0800
-@@ -1838,90 +1838,5 @@
+--- ./dist/MessageContext.jsm 2018-04-13 08:25:20.698138486 -0700
++++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm 2018-04-13 08:27:20.944227388 -0700
+@@ -16,7 +16,7 @@
+ */
+
+
+-/* fluent-dom@0.2.0 */
++/* fluent@0.6.3 */
+
+ /* eslint no-magic-numbers: [0] */
+
+@@ -1858,63 +1858,5 @@
}
}
-/*
- * CachedIterable caches the elements yielded by an iterable.
- *
- * It can be used to iterate over an iterable many times without depleting the
- * iterable.
@@ -448,45 +567,19 @@ diff -uNr ./dist/MessageContext.jsm /hom
- * }
- *
- * const contexts = new CachedIterable(generateMessages());
- * const ctx = mapContextSync(contexts, id);
- *
- */
-
-/*
-- * Synchronously map an identifier or an array of identifiers to the best
-- * `MessageContext` instance(s).
-- *
-- * @param {Iterable} iterable
-- * @param {string|Array<string>} ids
-- * @returns {MessageContext|Array<MessageContext>}
-- */
--
--
--/*
-- * Asynchronously map an identifier or an array of identifiers to the best
-- * `MessageContext` instance(s).
-- *
-- * @param {AsyncIterable} iterable
-- * @param {string|Array<string>} ids
-- * @returns {Promise<MessageContext|Array<MessageContext>>}
-- */
--
--/**
-- * Template literal tag for dedenting FTL code.
-- *
-- * Strip the common indent of non-blank lines. Remove blank lines.
-- *
-- * @param {Array<string>} strings
-- */
--
--/*
- * @module fluent
- * @overview
- *
- * `fluent` is a JavaScript implementation of Project Fluent, a localization
- * framework designed to unleash the expressive power of the natural language.
- *
- */
-
this.MessageContext = MessageContext;
- this.EXPORTED_SYMBOLS = ['MessageContext'];
+-this.EXPORTED_SYMBOLS = ["MessageContext"];
++var EXPORTED_SYMBOLS = ["MessageContext"];
--- a/intl/l10n/test/dom/test_domloc_overlay.html
+++ b/intl/l10n/test/dom/test_domloc_overlay.html
@@ -10,17 +10,17 @@
const { DOMLocalization } =
ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
async function* mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages("title = <strong>Hello</strong> World");
- mc.addMessages("title2 = This is <a>a link</a>!");
+ mc.addMessages(`title2 = This is <a data-l10n-name="link">a link</a>!`);
yield mc;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
@@ -45,12 +45,12 @@
a.click();
};
</script>
</head>
<body>
<p data-l10n-id="title" />
<p data-l10n-id="title2">
- <a href="http://www.mozilla.org"></a>
+ <a href="http://www.mozilla.org" data-l10n-name="link"></a>
</p>
</body>
</html>
--- a/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
@@ -9,17 +9,17 @@
"use strict";
const { DOMLocalization } =
ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
async function* mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
- mc.addMessages("title = Visit <a>Mozilla</a> or <a>Firefox</a> website!");
+ mc.addMessages(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`);
yield mc;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
@@ -41,14 +41,14 @@
is(linkList.length, 2, "There should be exactly two links in the result.");
SimpleTest.finish();
};
</script>
</head>
<body>
<p data-l10n-id="title">
- <a href="http://www.mozilla.org"></a>
- <a href="http://www.firefox.com"></a>
- <a href="http://www.w3.org"></a>
+ <a href="http://www.mozilla.org" data-l10n-name="mozilla-link"></a>
+ <a href="http://www.firefox.com" data-l10n-name="firefox-link"></a>
+ <a href="http://www.w3.org" data-l10n-name="w3-link"></a>
</p>
</body>
</html>
--- a/intl/l10n/test/dom/test_domloc_overlay_repeated.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_repeated.html
@@ -9,17 +9,17 @@
"use strict";
const { DOMLocalization } =
ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
async function* mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
- mc.addMessages("title = Visit <a>Mozilla</a> or <a>Firefox</a> website!");
+ mc.addMessages(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`);
yield mc;
}
window.onload = async function() {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
@@ -39,13 +39,13 @@
is(linkList[1].textContent, "Firefox");
SimpleTest.finish();
};
</script>
</head>
<body>
<p data-l10n-id="title">
- <a href="http://www.mozilla.org"></a>
- <a href="http://www.firefox.com"></a>
+ <a href="http://www.mozilla.org" data-l10n-name="mozilla-link"></a>
+ <a href="http://www.firefox.com" data-l10n-name="firefox-link"></a>
</p>
</body>
</html>
--- a/intl/l10n/test/dom/test_domloc_repeated_l10nid.html
+++ b/intl/l10n/test/dom/test_domloc_repeated_l10nid.html
@@ -12,17 +12,17 @@
const { MessageContext } =
ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
async function* mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages(`
key1 = Translation For Key 1
-key2 = Visit <a>this link<a/>.
+key2 = Visit <a data-l10n-name="link">this link<a/>.
`);
yield mc;
}
SimpleTest.waitForExplicitFinish();
addLoadEvent(async () => {
const domLoc = new DOMLocalization(
window,
@@ -48,16 +48,16 @@ key2 = Visit <a>this link<a/>.
});
</script>
</head>
<body>
<h1 id="elem1" data-l10n-id="key1"></h1>
<h2 id="elem2" data-l10n-id="key1"></h2>
<p id="elem3" data-l10n-id="key2">
- <a href="http://www.mozilla.org"></a>
+ <a href="http://www.mozilla.org" data-l10n-name="link"></a>
</p>
<p id="elem4" data-l10n-id="key2">
- <a href="http://www.firefox.com"></a>
+ <a href="http://www.firefox.com" data-l10n-name="link"></a>
</p>
</body>
</html>