зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1461048 - Update FluentDOM to 0.3.0. r=mossop
MozReview-Commit-ID: FKHICfiqXVr --HG-- extra : rebase_source : 93258d2425509bbdc2a4a9e77b98ed87da526af0
This commit is contained in:
Родитель
4fecaa7a1a
Коммит
1fc028c7d8
|
@ -82,7 +82,7 @@ const LOCALIZABLE_ATTRIBUTES = {
|
|||
* @private
|
||||
*/
|
||||
function translateElement(element, translation) {
|
||||
const value = translation.value;
|
||||
const {value} = translation;
|
||||
|
||||
if (typeof value === "string") {
|
||||
if (!reOverlay.test(value)) {
|
||||
|
@ -112,19 +112,41 @@ function translateElement(element, translation) {
|
|||
* 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 {DocumentFragment} fromFragment - The source of children to overlay.
|
||||
* @param {Element} toElement - The target of the overlay.
|
||||
* @private
|
||||
*/
|
||||
function overlayChildNodes(fromElement, toElement) {
|
||||
const content = toElement.ownerDocument.createDocumentFragment();
|
||||
function overlayChildNodes(fromFragment, toElement) {
|
||||
for (const childNode of fromFragment.childNodes) {
|
||||
if (childNode.nodeType === childNode.TEXT_NODE) {
|
||||
// Keep the translated text node.
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const childNode of fromElement.childNodes) {
|
||||
content.appendChild(sanitizeUsing(toElement, childNode));
|
||||
if (childNode.hasAttribute("data-l10n-name")) {
|
||||
const sanitized = namedChildFrom(toElement, childNode);
|
||||
fromFragment.replaceChild(sanitized, childNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isElementAllowed(childNode)) {
|
||||
const sanitized = allowedChild(childNode);
|
||||
fromFragment.replaceChild(sanitized, childNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`An element of forbidden type "${childNode.localName}" was found in ` +
|
||||
"the translation. Only safe text-level elements and elements with " +
|
||||
"data-l10n-name are allowed."
|
||||
);
|
||||
|
||||
// If all else fails, replace the element with its text content.
|
||||
fromFragment.replaceChild(textNode(childNode), childNode);
|
||||
}
|
||||
|
||||
toElement.textContent = "";
|
||||
toElement.appendChild(content);
|
||||
toElement.appendChild(fromFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,75 +188,79 @@ function overlayAttributes(fromElement, toElement) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitize a child node created by the translation.
|
||||
* Sanitize a child element created by the translation.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @param {Element} sourceElement - The source for data-l10n-name lookups.
|
||||
* @param {Element} childNode - The child node to be sanitized.
|
||||
* @param {Element} translatedChild - The translated child to be sanitized.
|
||||
* @returns {Element}
|
||||
* @private
|
||||
*/
|
||||
function sanitizeUsing(sourceElement, childNode) {
|
||||
if (childNode.nodeType === childNode.TEXT_NODE) {
|
||||
return childNode.cloneNode(false);
|
||||
}
|
||||
|
||||
if (childNode.hasAttribute("data-l10n-name")) {
|
||||
const childName = childNode.getAttribute("data-l10n-name");
|
||||
const sourceChild = sourceElement.querySelector(
|
||||
`[data-l10n-name="${childName}"]`
|
||||
);
|
||||
|
||||
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."
|
||||
function namedChildFrom(sourceElement, translatedChild) {
|
||||
const childName = translatedChild.getAttribute("data-l10n-name");
|
||||
const sourceChild = sourceElement.querySelector(
|
||||
`[data-l10n-name="${childName}"]`
|
||||
);
|
||||
|
||||
// If all else fails, convert the element to its text content.
|
||||
return childNode.ownerDocument.createTextNode(childNode.textContent);
|
||||
if (!sourceChild) {
|
||||
console.warn(
|
||||
`An element named "${childName}" wasn't found in the source.`
|
||||
);
|
||||
return textNode(translatedChild);
|
||||
}
|
||||
|
||||
if (sourceChild.localName !== translatedChild.localName) {
|
||||
console.warn(
|
||||
`An element named "${childName}" was found in the translation ` +
|
||||
`but its type ${translatedChild.localName} didn't match the ` +
|
||||
`element found in the source (${sourceChild.localName}).`
|
||||
);
|
||||
return textNode(translatedChild);
|
||||
}
|
||||
|
||||
// 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(translatedChild, clone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize an allowed element.
|
||||
*
|
||||
* Text-level elements allowed in translations may only use safe attributes
|
||||
* and will have any nested markup stripped to text content.
|
||||
*
|
||||
* @param {Element} element - The element to be sanitized.
|
||||
* @returns {Element}
|
||||
* @private
|
||||
*/
|
||||
function allowedChild(element) {
|
||||
// Start with an empty element of the same type to remove nested children
|
||||
// and non-localizable attributes defined by the translation.
|
||||
const clone = element.ownerDocument.createElement(element.localName);
|
||||
return shallowPopulateUsing(element, clone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an element to a text node.
|
||||
*
|
||||
* @param {Element} element - The element to be sanitized.
|
||||
* @returns {Node}
|
||||
* @private
|
||||
*/
|
||||
function textNode(element) {
|
||||
return element.ownerDocument.createTextNode(element.textContent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -406,8 +432,8 @@ class DOMLocalization extends Localization {
|
|||
};
|
||||
}
|
||||
|
||||
onLanguageChange() {
|
||||
super.onLanguageChange();
|
||||
onChange() {
|
||||
super.onChange();
|
||||
this.translateRoots();
|
||||
}
|
||||
|
||||
|
@ -728,14 +754,14 @@ class DOMLocalization extends Localization {
|
|||
* array.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @returns {Array<string, Object>}
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
getKeysForElement(element) {
|
||||
return [
|
||||
element.getAttribute(L10NID_ATTR_NAME),
|
||||
JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null)
|
||||
];
|
||||
return {
|
||||
id: element.getAttribute(L10NID_ATTR_NAME),
|
||||
args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,20 +50,6 @@ class CachedAsyncIterable {
|
|||
this.seen = [];
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
const { seen, iterator } = this;
|
||||
let cur = 0;
|
||||
|
||||
return {
|
||||
next() {
|
||||
if (seen.length <= cur) {
|
||||
seen.push(iterator.next());
|
||||
}
|
||||
return seen[cur++];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
const { seen, iterator } = this;
|
||||
let cur = 0;
|
||||
|
@ -126,7 +112,18 @@ class Localization {
|
|||
constructor(resourceIds, generateMessages = defaultGenerateMessages) {
|
||||
this.resourceIds = resourceIds;
|
||||
this.generateMessages = generateMessages;
|
||||
this.ctxs = new CachedAsyncIterable(this.generateMessages(this.resourceIds));
|
||||
this.ctxs =
|
||||
new CachedAsyncIterable(this.generateMessages(this.resourceIds));
|
||||
}
|
||||
|
||||
addResourceIds(resourceIds) {
|
||||
this.resourceIds.push(...resourceIds);
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
removeResourceIds(resourceIds) {
|
||||
this.resourceIds = this.resourceIds.filter(r => !resourceIds.includes(r));
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,7 +133,7 @@ class Localization {
|
|||
* DOMLocalization. In case of errors, fetch the next context in the
|
||||
* fallback chain.
|
||||
*
|
||||
* @param {Array<Array>} keys - Translation keys to format.
|
||||
* @param {Array<Object>} keys - Translation keys to format.
|
||||
* @param {Function} method - Formatting function.
|
||||
* @returns {Promise<Array<string|Object>>}
|
||||
* @private
|
||||
|
@ -144,12 +141,7 @@ class Localization {
|
|||
async formatWithFallback(keys, method) {
|
||||
const translations = [];
|
||||
|
||||
for await (let ctx of this.ctxs) {
|
||||
// This can operate on synchronous and asynchronous
|
||||
// contexts coming from the iterator.
|
||||
if (typeof ctx.then === "function") {
|
||||
ctx = await ctx;
|
||||
}
|
||||
for await (const ctx of this.ctxs) {
|
||||
const missingIds = keysFromContext(method, ctx, keys, translations);
|
||||
|
||||
if (missingIds.size === 0) {
|
||||
|
@ -167,26 +159,26 @@ class Localization {
|
|||
}
|
||||
|
||||
/**
|
||||
* Format translations into {value, attrs} objects.
|
||||
* 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, attrs} objects
|
||||
* which are suitable for the translation of DOM elements.
|
||||
* is stricter (an array of arrays) and it returns {value, attributes}
|
||||
* objects which are suitable for the translation of DOM elements.
|
||||
*
|
||||
* docL10n.formatMessages([
|
||||
* ['hello', { who: 'Mary' }],
|
||||
* ['welcome', undefined]
|
||||
* {id: 'hello', args: { who: 'Mary' }},
|
||||
* {id: 'welcome'}
|
||||
* ]).then(console.log);
|
||||
*
|
||||
* // [
|
||||
* // { value: 'Hello, Mary!', attrs: null },
|
||||
* // { value: 'Welcome!', attrs: { title: 'Hello' } }
|
||||
* // { value: 'Hello, Mary!', attributes: null },
|
||||
* // { value: 'Welcome!', attributes: { title: 'Hello' } }
|
||||
* // ]
|
||||
*
|
||||
* Returns a Promise resolving to an array of the translation strings.
|
||||
*
|
||||
* @param {Array<Array>} keys
|
||||
* @returns {Promise<Array<{value: string, attrs: Object}>>}
|
||||
* @param {Array<Object>} keys
|
||||
* @returns {Promise<Array<{value: string, attributes: Object}>>}
|
||||
* @private
|
||||
*/
|
||||
formatMessages(keys) {
|
||||
|
@ -200,16 +192,16 @@ class Localization {
|
|||
* either be simple string identifiers or `[id, args]` arrays.
|
||||
*
|
||||
* docL10n.formatValues([
|
||||
* ['hello', { who: 'Mary' }],
|
||||
* ['hello', { who: 'John' }],
|
||||
* ['welcome']
|
||||
* {id: 'hello', args: { who: 'Mary' }},
|
||||
* {id: 'hello', args: { who: 'John' }},
|
||||
* {id: 'welcome'}
|
||||
* ]).then(console.log);
|
||||
*
|
||||
* // ['Hello, Mary!', 'Hello, John!', 'Welcome!']
|
||||
*
|
||||
* Returns a Promise resolving to an array of the translation strings.
|
||||
*
|
||||
* @param {Array<Array>} keys
|
||||
* @param {Array<Object>} keys
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
formatValues(keys) {
|
||||
|
@ -239,7 +231,7 @@ class Localization {
|
|||
* @returns {Promise<string>}
|
||||
*/
|
||||
async formatValue(id, args) {
|
||||
const [val] = await this.formatValues([[id, args]]);
|
||||
const [val] = await this.formatValues([{id, args}]);
|
||||
return val;
|
||||
}
|
||||
|
||||
|
@ -260,7 +252,7 @@ class Localization {
|
|||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "intl:app-locales-changed":
|
||||
this.onLanguageChange();
|
||||
this.onChange();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -271,8 +263,9 @@ class Localization {
|
|||
* This method should be called when there's a reason to believe
|
||||
* that language negotiation or available resources changed.
|
||||
*/
|
||||
onLanguageChange() {
|
||||
this.ctxs = new CachedAsyncIterable(this.generateMessages(this.resourceIds));
|
||||
onChange() {
|
||||
this.ctxs =
|
||||
new CachedAsyncIterable(this.generateMessages(this.resourceIds));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,12 +293,11 @@ Localization.prototype.QueryInterface = ChromeUtils.generateQI([
|
|||
*/
|
||||
function valueFromContext(ctx, errors, id, args) {
|
||||
const msg = ctx.getMessage(id);
|
||||
|
||||
return ctx.format(msg, args, errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all public values of a message into a { value, attrs } object.
|
||||
* Format all public values of a message into a {value, attributes} object.
|
||||
*
|
||||
* This function is passed as a method to `keysFromContext` and resolve
|
||||
* a single L10n Entity using provided `MessageContext`.
|
||||
|
@ -314,7 +306,7 @@ function valueFromContext(ctx, errors, id, args) {
|
|||
* entity.
|
||||
*
|
||||
* If the function fails to retrieve the entity, the value is set to the ID of
|
||||
* an entity, and attrs to `null`. If formatting fails, it will return
|
||||
* an entity, and attributes to `null`. If formatting fails, it will return
|
||||
* a partially resolved value and attributes.
|
||||
*
|
||||
* In both cases, an error is being added to the errors array.
|
||||
|
@ -374,7 +366,7 @@ function messageFromContext(ctx, errors, id, args) {
|
|||
* @param {Function} method
|
||||
* @param {MessageContext} ctx
|
||||
* @param {Array<string>} keys
|
||||
* @param {{Array<{value: string, attrs: Object}>}} translations
|
||||
* @param {{Array<{value: string, attributes: Object}>}} translations
|
||||
*
|
||||
* @returns {Set<string>}
|
||||
* @private
|
||||
|
@ -383,17 +375,17 @@ function keysFromContext(method, ctx, keys, translations) {
|
|||
const messageErrors = [];
|
||||
const missingIds = new Set();
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
keys.forEach(({id, args}, i) => {
|
||||
if (translations[i] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.hasMessage(key[0])) {
|
||||
if (ctx.hasMessage(id)) {
|
||||
messageErrors.length = 0;
|
||||
translations[i] = method(ctx, messageErrors, key[0], key[1]);
|
||||
translations[i] = method(ctx, messageErrors, id, args);
|
||||
// XXX: Report resolver errors
|
||||
} else {
|
||||
missingIds.add(key[0]);
|
||||
missingIds.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ add_task(async function test_methods_calling() {
|
|||
equal(values[1], "[en] Value3");
|
||||
|
||||
L10nRegistry.sources.clear();
|
||||
L10nRegistry.ctxCache.clear();
|
||||
L10nRegistry.load = originalLoad;
|
||||
Services.locale.setRequestedLocales(originalRequested);
|
||||
});
|
||||
|
@ -89,5 +90,55 @@ key = { PLATFORM() ->
|
|||
`${ known_platforms[AppConstants.platform].toUpperCase() } Value`));
|
||||
|
||||
L10nRegistry.sources.clear();
|
||||
L10nRegistry.ctxCache.clear();
|
||||
L10nRegistry.load = originalLoad;
|
||||
});
|
||||
|
||||
add_task(async 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 originalLoad = L10nRegistry.load;
|
||||
const originalRequested = Services.locale.getRequestedLocales();
|
||||
|
||||
L10nRegistry.load = async function(url) {
|
||||
return fs[url];
|
||||
};
|
||||
|
||||
const source = new FileSource("test", ["en-US"], "/localization/{locale}");
|
||||
L10nRegistry.registerSource(source);
|
||||
|
||||
async function* generateMessages(resIds) {
|
||||
yield * await L10nRegistry.generateContexts(["en-US"], resIds);
|
||||
}
|
||||
|
||||
const l10n = new Localization(["/browser/menu.ftl"], generateMessages);
|
||||
|
||||
let values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
|
||||
|
||||
equal(values[0], "Value1");
|
||||
equal(values[1], undefined);
|
||||
|
||||
l10n.addResourceIds(["/toolkit/menu.ftl"]);
|
||||
|
||||
values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
|
||||
|
||||
equal(values[0], "Value1");
|
||||
equal(values[1], "Value2");
|
||||
|
||||
l10n.removeResourceIds(["/browser/menu.ftl"]);
|
||||
|
||||
values = await l10n.formatValues([{id: "key1"}, {id: "key2"}]);
|
||||
|
||||
equal(values[0], undefined);
|
||||
equal(values[1], "Value2");
|
||||
|
||||
L10nRegistry.sources.clear();
|
||||
L10nRegistry.ctxCache.clear();
|
||||
L10nRegistry.load = originalLoad;
|
||||
Services.locale.setRequestedLocales(originalRequested);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче