Bug 1453480 - Update fluent to 0.6.4 and fluent-dom to 0.2.0. r=stas

MozReview-Commit-ID: La8uSw0sq4p

--HG--
extra : rebase_source : 95181fb06ec75c36ebc2c234fdac68956b9e049e
This commit is contained in:
Zibi Braniecki 2018-04-11 13:06:35 -07:00
parent 781fa96acd
commit a8f62de0ae
8 changed files with 444 additions and 320 deletions

View File

@ -16,7 +16,7 @@
*/
/* fluent@0.6.3 */
/* fluent-dom@0.2.0 */
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
@ -26,15 +26,18 @@ const { Localization } =
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"
],
};
@ -66,153 +69,172 @@ const LOCALIZABLE_ATTRIBUTES = {
/**
* 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:
* 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.
*
* - Allow text nodes.
* - Replace forbidden children with their textContent.
* - Remove forbidden attributes from allowed children.
* 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 forof iteration over its child nodes.
*
* 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:
*
* - 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 = "";
if (childNode.hasAttribute("data-l10n-name")) {
const childName = childNode.getAttribute("data-l10n-name");
const sourceChild = sourceElement.querySelector(
`[data-l10n-name="${childName}"]`
);
return translationFragment;
}
/**
* 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 (!sourceElement) {
return localizedAttrs;
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);
}
}
const functionalAttrs = Array.from(sourceElement.attributes).filter(
attr => !isAttrNameLocalizable(attr.name, sourceElement)
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."
);
return localizedAttrs.concat(functionalAttrs);
// If all else fails, convert the element to its text content.
return childNode.ownerDocument.createTextNode(childNode.textContent);
}
/**
@ -225,8 +247,8 @@ function sanitizeAttrsUsing(translatedElement, sourceElement) {
* @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);
}
@ -287,21 +309,17 @@ function isAttrNameLocalizable(name, element, explicitlyAllowed = null) {
}
/**
* 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;
}
/**
@ -332,19 +350,18 @@ function sanitizeTranslationForNodeLocalize(l10nItem, translation) {
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";
@ -543,7 +560,9 @@ class DOMLocalization extends Localization {
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) {
@ -634,7 +653,7 @@ class DOMLocalization extends Localization {
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();
@ -679,7 +698,7 @@ class DOMLocalization extends Localization {
for (let i = 0; i < elements.length; i++) {
if (translations[i] !== undefined) {
overlayElement(elements[i], translations[i]);
translateElement(elements[i], translations[i]);
}
}

View File

@ -16,7 +16,7 @@
*/
/* fluent@0.6.3 */
/* fluent-dom@0.2.0 */
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global console */
@ -327,15 +327,15 @@ function messageFromContext(ctx, errors, id, args) {
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});
}
}
}

View File

@ -1608,6 +1608,10 @@ function Pattern(env, ptn) {
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);
@ -1616,7 +1620,7 @@ function Pattern(env, ptn) {
const part = Type(env, elem).toString(ctx);
if (ctx._useIsolating) {
if (useIsolating) {
result.push(FSI);
}
@ -1632,7 +1636,7 @@ function Pattern(env, ptn) {
result.push(part);
}
if (ctx._useIsolating) {
if (useIsolating) {
result.push(PDI);
}
}
@ -1775,8 +1779,16 @@ class MessageContext {
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]);
}
}

View File

@ -1,21 +1,140 @@
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
// &amp;, &#0038;, &#x0026;.
@@ -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
@ -47,40 +166,84 @@ diff -uNr ./dist/DOMLocalization.jsm /home/zbraniecki/projects/mozilla-unified/i
-}
-
-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.
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 @@
* 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();
/* fluent@0.6.0 */
- 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-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-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.
@ -142,30 +305,14 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
- * 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 @@
}
}
@ -212,75 +359,30 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
- */
-
-/*
- * 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}
*/
@ -289,35 +391,35 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
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);
+ }
+
+ /**
@ -329,8 +431,7 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
+ */
+ 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:
@ -339,18 +440,27 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
}
/**
@@ -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 */
@ -361,12 +471,11 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
-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
@ -394,12 +503,22 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl
-}
-
-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 @@
}
}
@ -453,33 +572,6 @@ diff -uNr ./dist/MessageContext.jsm /home/zbraniecki/projects/mozilla-unified/in
- */
-
-/*
- * 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
- *
@ -489,4 +581,5 @@ diff -uNr ./dist/MessageContext.jsm /home/zbraniecki/projects/mozilla-unified/in
- */
-
this.MessageContext = MessageContext;
this.EXPORTED_SYMBOLS = ['MessageContext'];
-this.EXPORTED_SYMBOLS = ["MessageContext"];
+var EXPORTED_SYMBOLS = ["MessageContext"];

View File

@ -15,7 +15,7 @@
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;
}
@ -50,7 +50,7 @@
<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>

View File

@ -14,7 +14,7 @@
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;
}
@ -46,9 +46,9 @@
</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>

View File

@ -14,7 +14,7 @@
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;
}
@ -44,8 +44,8 @@
</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>

View File

@ -17,7 +17,7 @@
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;
}
@ -53,11 +53,11 @@ key2 = Visit <a>this link<a/>.
<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>