Bug 1426054 - Update Fluent in Gecko to 0.6. r=Pike

This upstream uses fluent.js revision ae1b55a.

MozReview-Commit-ID: 1IynCPWWN14

--HG--
extra : rebase_source : d76bd81668e447e7fe8b62cf0834f040c32f6982
This commit is contained in:
Zibi Braniecki 2018-01-26 14:01:34 -08:00
parent e483ecaab0
commit 64594123bb
6 changed files with 891 additions and 350 deletions

View File

@ -16,7 +16,7 @@
*/
/* fluent@0.4.1 */
/* fluent@0.6.0 */
const { Localization } =
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
@ -30,7 +30,7 @@ const reOverlay = /<|&#?\w+;/;
*
* Source: https://www.w3.org/TR/html5/text-level-semantics.html
*/
const ALLOWED_ELEMENTS = {
const LOCALIZABLE_ELEMENTS = {
'http://www.w3.org/1999/xhtml': [
'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data',
'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u',
@ -38,12 +38,12 @@ const ALLOWED_ELEMENTS = {
],
};
const ALLOWED_ATTRIBUTES = {
const LOCALIZABLE_ATTRIBUTES = {
'http://www.w3.org/1999/xhtml': {
global: ['title', 'aria-label', 'aria-valuetext', 'aria-moz-hint'],
a: ['download'],
area: ['download', 'alt'],
// value is special-cased in isAttrNameAllowed
// value is special-cased in isAttrNameLocalizable
input: ['alt', 'placeholder'],
menuitem: ['label'],
menu: ['label'],
@ -92,18 +92,24 @@ function overlayElement(targetElement, translation) {
}
}
if (translation.attrs === null) {
return;
}
const explicitlyAllowed = targetElement.hasAttribute('data-l10n-attrs')
? targetElement.getAttribute('data-l10n-attrs')
.split(',').map(i => i.trim())
: null;
for (const [name, val] of translation.attrs) {
if (isAttrNameAllowed(name, targetElement, explicitlyAllowed)) {
targetElement.setAttribute(name, val);
// 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);
}
}
if (translation.attrs) {
for (const [name, val] of translation.attrs) {
if (isAttrNameLocalizable(name, targetElement, explicitlyAllowed)) {
targetElement.setAttribute(name, val);
}
}
}
}
@ -131,9 +137,11 @@ function overlayElement(targetElement, translation) {
*
* @param {DocumentFragment} translationFragment
* @param {Element} sourceElement
* @returns {DocumentFragment}
* @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.
@ -144,32 +152,29 @@ function sanitizeUsing(translationFragment, sourceElement) {
}
// If the child is forbidden just take its textContent.
if (!isElementAllowed(childNode)) {
const text = translationFragment.ownerDocument.createTextNode(
childNode.textContent
);
if (!isElementLocalizable(childNode)) {
const text = ownerDocument.createTextNode(childNode.textContent);
translationFragment.replaceChild(text, childNode);
continue;
}
// If a child of the same type exists in sourceElement, use it as the base
// for the resultChild. This also removes the child from sourceElement.
const sourceChild = shiftNamedElement(sourceElement, childNode.localName);
const mergedChild = sourceChild
// Shallow-clone the sourceChild to remove all childNodes.
? sourceChild.cloneNode(false)
// Create a fresh element as a way to remove all forbidden attributes.
: childNode.ownerDocument.createElement(childNode.localName);
// 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;
for (const attr of Array.from(childNode.attributes)) {
if (isAttrNameAllowed(attr.name, childNode)) {
mergedChild.setAttribute(attr.name, attr.value);
}
// 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);
@ -182,6 +187,33 @@ function sanitizeUsing(translationFragment, sourceElement) {
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;
}
const functionalAttrs = Array.from(sourceElement.attributes).filter(
attr => !isAttrNameLocalizable(attr.name, sourceElement)
);
return localizedAttrs.concat(functionalAttrs);
}
/**
* Check if element is allowed in the translation.
*
@ -192,8 +224,8 @@ function sanitizeUsing(translationFragment, sourceElement) {
* @returns {boolean}
* @private
*/
function isElementAllowed(element) {
const allowed = ALLOWED_ELEMENTS[element.namespaceURI];
function isElementLocalizable(element) {
const allowed = LOCALIZABLE_ELEMENTS[element.namespaceURI];
return allowed && allowed.includes(element.localName);
}
@ -213,12 +245,12 @@ function isElementAllowed(element) {
* @returns {boolean}
* @private
*/
function isAttrNameAllowed(name, element, explicitlyAllowed = null) {
function isAttrNameLocalizable(name, element, explicitlyAllowed = null) {
if (explicitlyAllowed && explicitlyAllowed.includes(name)) {
return true;
}
const allowed = ALLOWED_ATTRIBUTES[element.namespaceURI];
const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI];
if (!allowed) {
return false;
}
@ -475,7 +507,7 @@ class DOMLocalization extends Localization {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
if (addedNode.childElementCount) {
for (let element of this.getTranslatables(addedNode)) {
for (const element of this.getTranslatables(addedNode)) {
this.pendingElements.add(element);
}
} else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) {
@ -487,8 +519,8 @@ class DOMLocalization extends Localization {
}
}
// This fragment allows us to coalesce all pending translations into a single
// requestAnimationFrame.
// This fragment allows us to coalesce all pending translations
// into a single requestAnimationFrame.
if (this.pendingElements.size > 0) {
if (this.pendingrAF === null) {
this.pendingrAF = this.windowElement.requestAnimationFrame(() => {
@ -500,6 +532,7 @@ class DOMLocalization extends Localization {
}
}
/**
* Translate a DOM element or fragment asynchronously using this
* `DOMLocalization` object.

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
/* fluent@0.4.1 */
/* fluent@0.6.0 */
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global console */
@ -28,22 +29,45 @@ const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry
const LocaleService = Cc["@mozilla.org/intl/localeservice;1"].getService(Ci.mozILocaleService);
const ObserverService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
/**
/*
* CachedIterable caches the elements yielded by an iterable.
*
* It can be used to iterate over an iterable many times without depleting the
* iterable.
*/
class CachedIterable {
/**
* Create an `CachedIterable` instance.
*
* @param {Iterable} iterable
* @returns {CachedIterable}
*/
constructor(iterable) {
if (!(Symbol.asyncIterator in Object(iterable))) {
throw new TypeError('Argument must implement the async iteration protocol.');
if (Symbol.asyncIterator in Object(iterable)) {
this.iterator = iterable[Symbol.asyncIterator]();
} else if (Symbol.iterator in Object(iterable)) {
this.iterator = iterable[Symbol.iterator]();
} else {
throw new TypeError('Argument must implement the iteration protocol.');
}
this.iterator = iterable[Symbol.asyncIterator]();
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;
@ -88,7 +112,7 @@ class L10nError extends Error {
}
}
/**
/**
* The default localization strategy for Gecko. It comabines locales
* available in L10nRegistry, with locales requested by the user to
* generate the iterator over MessageContexts.
@ -117,7 +141,7 @@ function defaultGenerateMessages(resourceIds) {
class Localization {
/**
* @param {Array<String>} resourceIds - List of resource IDs
* @param {Function} generateMessages - Function that returns the
* @param {Function} generateMessages - Function that returns a
* generator over MessageContexts
*
* @returns {Localization}
@ -186,6 +210,9 @@ class Localization {
/**
* Retrieve translations corresponding to the passed keys.
*
* A generalized version of `DOMLocalization.formatValue`. Keys can
* either be simple string identifiers or `[id, args]` arrays.
*
* docL10n.formatValues([
* ['hello', { who: 'Mary' }],
* ['hello', { who: 'John' }],

View File

@ -16,13 +16,15 @@
*/
/* fluent@0.4.1 */
/* fluent@0.6.0 */
/* eslint no-magic-numbers: [0] */
const MAX_PLACEABLES = 100;
const identifierRe = new RegExp('[a-zA-Z_][a-zA-Z0-9_-]*', 'y');
const entryIdentifierRe = /-?[a-zA-Z][a-zA-Z0-9_-]*/y;
const identifierRe = /[a-zA-Z][a-zA-Z0-9_-]*/y;
const functionIdentifierRe = /^[A-Z][A-Z_?-]*$/;
/**
* The `Parser` class is responsible for parsing FTL resources.
@ -92,7 +94,8 @@ class RuntimeParser {
const ch = this._source[this._index];
// We don't care about comments or sections at runtime
if (ch === '/') {
if (ch === '/' ||
(ch === '#' && [' ', '#'].includes(this._source[this._index + 1]))) {
this.skipComment();
return;
}
@ -119,7 +122,7 @@ class RuntimeParser {
this._index += 1;
this.skipInlineWS();
this.getSymbol();
this.getVariantName();
this.skipInlineWS();
if (this._source[this._index] !== ']' ||
@ -137,61 +140,49 @@ class RuntimeParser {
* @private
*/
getMessage() {
const id = this.getIdentifier();
let attrs = null;
let tags = null;
const id = this.getEntryIdentifier();
this.skipInlineWS();
let ch = this._source[this._index];
let val;
if (ch === '=') {
if (this._source[this._index] === '=') {
this._index++;
}
this.skipInlineWS();
const val = this.getPattern();
if (id.startsWith('-') && val === null) {
throw this.error('Expected term to have a value');
}
let attrs = null;
if (this._source[this._index] === ' ') {
const lineStart = this._index;
this.skipInlineWS();
val = this.getPattern();
} else {
this.skipWS();
}
ch = this._source[this._index];
if (ch === '\n') {
this._index++;
this.skipInlineWS();
ch = this._source[this._index];
}
if (ch === '.') {
attrs = this.getAttributes();
}
if (ch === '#') {
if (attrs !== null) {
throw this.error('Tags cannot be added to a message with attributes.');
if (this._source[this._index] === '.') {
this._index = lineStart;
attrs = this.getAttributes();
}
tags = this.getTags();
}
if (tags === null && attrs === null && typeof val === 'string') {
if (attrs === null && typeof val === 'string') {
this.entries[id] = val;
} else {
if (val === undefined) {
if (tags === null && attrs === null) {
throw this.error(`Expected a value (like: " = value") or
an attribute (like: ".key = value")`);
}
if (val === null && attrs === null) {
throw this.error('Expected message to have a value or attributes');
}
this.entries[id] = { val };
if (attrs) {
this.entries[id].attrs = attrs;
this.entries[id] = {};
if (val !== null) {
this.entries[id].val = val;
}
if (tags) {
this.entries[id].tags = tags;
if (attrs !== null) {
this.entries[id].attrs = attrs;
}
}
}
@ -221,49 +212,81 @@ class RuntimeParser {
}
/**
* Get Message identifier.
* Skip blank lines.
*
* @private
*/
skipBlankLines() {
while (true) {
const ptr = this._index;
this.skipInlineWS();
if (this._source[this._index] === '\n') {
this._index += 1;
} else {
this._index = ptr;
break;
}
}
}
/**
* Get identifier using the provided regex.
*
* By default this will get identifiers of public messages, attributes and
* external arguments (without the $).
*
* @returns {String}
* @private
*/
getIdentifier() {
identifierRe.lastIndex = this._index;
const result = identifierRe.exec(this._source);
getIdentifier(re = identifierRe) {
re.lastIndex = this._index;
const result = re.exec(this._source);
if (result === null) {
this._index += 1;
throw this.error('Expected an identifier (starting with [a-zA-Z_])');
throw this.error(`Expected an identifier [${re.toString()}]`);
}
this._index = identifierRe.lastIndex;
this._index = re.lastIndex;
return result[0];
}
/**
* Get Symbol.
* Get identifier of a Message or a Term (staring with a dash).
*
* @returns {String}
* @private
*/
getEntryIdentifier() {
return this.getIdentifier(entryIdentifierRe);
}
/**
* Get Variant name.
*
* @returns {Object}
* @private
*/
getSymbol() {
getVariantName() {
let name = '';
const start = this._index;
let cc = this._source.charCodeAt(this._index);
if ((cc >= 97 && cc <= 122) || // a-z
(cc >= 65 && cc <= 90) || // A-Z
cc === 95 || cc === 32) { // _ <space>
(cc >= 65 && cc <= 90) || // A-Z
cc === 95 || cc === 32) { // _ <space>
cc = this._source.charCodeAt(++this._index);
} else {
throw this.error('Expected a keyword (starting with [a-zA-Z_])');
}
while ((cc >= 97 && cc <= 122) || // a-z
(cc >= 65 && cc <= 90) || // A-Z
(cc >= 48 && cc <= 57) || // 0-9
cc === 95 || cc === 45 || cc === 32) { // _- <space>
(cc >= 65 && cc <= 90) || // A-Z
(cc >= 48 && cc <= 57) || // 0-9
cc === 95 || cc === 45 || cc === 32) { // _- <space>
cc = this._source.charCodeAt(++this._index);
}
@ -277,7 +300,7 @@ class RuntimeParser {
name += this._source.slice(start, this._index);
return { type: 'sym', name };
return { type: 'varname', name };
}
/**
@ -297,7 +320,7 @@ class RuntimeParser {
}
if (ch === '\n') {
break;
throw this.error('Unterminated string expression');
}
}
@ -325,21 +348,40 @@ class RuntimeParser {
eol = this._length;
}
const line = start !== eol ?
this._source.slice(start, eol) : undefined;
const firstLineContent = start !== eol ?
this._source.slice(start, eol) : null;
if (line !== undefined && line.includes('{')) {
if (firstLineContent && firstLineContent.includes('{')) {
return this.getComplexPattern();
}
this._index = eol + 1;
if (this._source[this._index] === ' ') {
this._index = start;
return this.getComplexPattern();
this.skipBlankLines();
if (this._source[this._index] !== ' ') {
// No indentation means we're done with this message.
return firstLineContent;
}
return line;
const lineStart = this._index;
this.skipInlineWS();
if (this._source[this._index] === '.') {
// The pattern is followed by an attribute. Rewind _index to the first
// column of the current line as expected by getAttributes.
this._index = lineStart;
return firstLineContent;
}
if (firstLineContent) {
// It's a multiline pattern which started on the same line as the
// identifier. Reparse the whole pattern to make sure we get all of it.
this._index = start;
}
return this.getComplexPattern();
}
/**
@ -361,9 +403,19 @@ class RuntimeParser {
while (this._index < this._length) {
// This block handles multi-line strings combining strings separated
// by new line and `|` character at the beginning of the next one.
// by new line.
if (ch === '\n') {
this._index++;
// We want to capture the start and end pointers
// around blank lines and add them to the buffer
// but only if the blank lines are in the middle
// of the string.
const blankLinesStart = this._index;
this.skipBlankLines();
const blankLinesEnd = this._index;
if (this._source[this._index] !== ' ') {
break;
}
@ -372,11 +424,13 @@ class RuntimeParser {
if (this._source[this._index] === '}' ||
this._source[this._index] === '[' ||
this._source[this._index] === '*' ||
this._source[this._index] === '#' ||
this._source[this._index] === '.') {
this._index = blankLinesEnd;
break;
}
buffer += this._source.substring(blankLinesStart, blankLinesEnd);
if (buffer.length || content.length) {
buffer += '\n';
}
@ -415,7 +469,7 @@ class RuntimeParser {
}
if (content.length === 0) {
return buffer.length ? buffer : undefined;
return buffer.length ? buffer : null;
}
if (buffer.length) {
@ -462,6 +516,12 @@ class RuntimeParser {
const ch = this._source[this._index];
if (ch === '}') {
if (selector.type === 'attr' && selector.id.name.startsWith('-')) {
throw this.error(
'Attributes of private messages cannot be interpolated.'
);
}
return selector;
}
@ -469,6 +529,21 @@ class RuntimeParser {
throw this.error('Expected "}" or "->"');
}
if (selector.type === 'ref') {
throw this.error('Message references cannot be used as selectors.');
}
if (selector.type === 'var') {
throw this.error('Variants cannot be used as selectors.');
}
if (selector.type === 'attr' && !selector.id.name.startsWith('-')) {
throw this.error(
'Attributes of public messages cannot be used as selectors.'
);
}
this._index += 2; // ->
this.skipInlineWS();
@ -534,6 +609,10 @@ class RuntimeParser {
this._index++;
const args = this.getCallArgs();
if (!functionIdentifierRe.test(literal.name)) {
throw this.error('Function names must be all upper-case');
}
this._index++;
literal.type = 'fun';
@ -557,19 +636,18 @@ class RuntimeParser {
getCallArgs() {
const args = [];
if (this._source[this._index] === ')') {
return args;
}
while (this._index < this._length) {
this.skipInlineWS();
if (this._source[this._index] === ')') {
return args;
}
const exp = this.getSelectorExpression();
// MessageReference in this place may be an entity reference, like:
// `call(foo)`, or, if it's followed by `:` it will be a key-value pair.
if (exp.type !== 'ref' ||
exp.namespace !== undefined) {
if (exp.type !== 'ref') {
args.push(exp);
} else {
this.skipInlineWS();
@ -678,9 +756,12 @@ class RuntimeParser {
const attrs = {};
while (this._index < this._length) {
const ch = this._source[this._index];
if (this._source[this._index] !== ' ') {
break;
}
this.skipInlineWS();
if (ch !== '.') {
if (this._source[this._index] !== '.') {
break;
}
this._index++;
@ -689,6 +770,9 @@ class RuntimeParser {
this.skipInlineWS();
if (this._source[this._index] !== '=') {
throw this.error('Expected "="');
}
this._index++;
this.skipInlineWS();
@ -703,39 +787,12 @@ class RuntimeParser {
};
}
this.skipWS();
this.skipBlankLines();
}
return attrs;
}
/**
* Parses a list of Message tags.
*
* @returns {Array}
* @private
*/
getTags() {
const tags = [];
while (this._index < this._length) {
const ch = this._source[this._index];
if (ch !== '#') {
break;
}
this._index++;
const symbol = this.getSymbol();
tags.push(symbol.name);
this.skipWS();
}
return tags;
}
/**
* Parses a list of Selector variants.
*
@ -796,7 +853,7 @@ class RuntimeParser {
if ((cc >= 48 && cc <= 57) || cc === 45) {
literal = this.getNumber();
} else {
literal = this.getSymbol();
literal = this.getVariantName();
}
if (this._source[this._index] !== ']') {
@ -814,12 +871,9 @@ class RuntimeParser {
* @private
*/
getLiteral() {
const cc = this._source.charCodeAt(this._index);
if ((cc >= 48 && cc <= 57) || cc === 45) {
return this.getNumber();
} else if (cc === 34) { // "
return this.getString();
} else if (cc === 36) { // $
const cc0 = this._source.charCodeAt(this._index);
if (cc0 === 36) { // $
this._index++;
return {
type: 'ext',
@ -827,10 +881,29 @@ class RuntimeParser {
};
}
return {
type: 'ref',
name: this.getIdentifier()
};
const cc1 = cc0 === 45 // -
// Peek at the next character after the dash.
? this._source.charCodeAt(this._index + 1)
// Or keep using the character at the current index.
: cc0;
if ((cc1 >= 97 && cc1 <= 122) || // a-z
(cc1 >= 65 && cc1 <= 90)) { // A-Z
return {
type: 'ref',
name: this.getEntryIdentifier()
};
}
if ((cc1 >= 48 && cc1 <= 57)) { // 0-9
return this.getNumber();
}
if (cc0 === 34) { // "
return this.getString();
}
throw this.error('Expected literal');
}
/**
@ -844,7 +917,9 @@ class RuntimeParser {
let eol = this._source.indexOf('\n', this._index);
while (eol !== -1 &&
this._source[eol + 1] === '/' && this._source[eol + 2] === '/') {
((this._source[eol + 1] === '/' && this._source[eol + 2] === '/') ||
(this._source[eol + 1] === '#' &&
[' ', '#'].includes(this._source[eol + 2])))) {
this._index = eol + 3;
eol = this._source.indexOf('\n', this._index);
@ -887,8 +962,8 @@ class RuntimeParser {
const cc = this._source.charCodeAt(start);
if ((cc >= 97 && cc <= 122) || // a-z
(cc >= 65 && cc <= 90) || // A-Z
cc === 95 || cc === 47 || cc === 91) { // _/[
(cc >= 65 && cc <= 90) || // A-Z
cc === 47 || cc === 91) { // /[
this._index = start;
return;
}
@ -923,7 +998,7 @@ function parse(string) {
* 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 `valueOf` method together with a proper
* them, which can then be used in the `toString` method together with a proper
* `Intl` formatter.
*/
class FluentType {
@ -941,28 +1016,31 @@ class FluentType {
}
/**
* Unwrap the instance of `FluentType`.
* Unwrap the raw value stored by this `FluentType`.
*
* Unwrapped values are suitable for use outside of the `MessageContext`.
* @returns {Any}
*/
valueOf() {
return this.value;
}
/**
* Format this instance of `FluentType` to a string.
*
* Formatted values are suitable for use outside of the `MessageContext`.
* This method can use `Intl` formatters memoized by the `MessageContext`
* instance passed as an argument.
*
* In most cases, valueOf returns a string, but it can be overriden
* and there are use cases, where the return type is not a string.
*
* An example is fluent-react which implements a custom `FluentType`
* to represent React elements passed as arguments to format().
*
* @param {MessageContext} [ctx]
* @returns {string}
*/
valueOf() {
throw new Error('Subclasses of FluentType must implement valueOf.');
toString() {
throw new Error('Subclasses of FluentType must implement toString.');
}
}
class FluentNone extends FluentType {
valueOf() {
toString() {
return this.value || '???';
}
}
@ -972,11 +1050,16 @@ class FluentNumber extends FluentType {
super(parseFloat(value), opts);
}
valueOf(ctx) {
const nf = ctx._memoizeIntlObject(
Intl.NumberFormat, this.opts
);
return nf.format(this.value);
toString(ctx) {
try {
const nf = ctx._memoizeIntlObject(
Intl.NumberFormat, this.opts
);
return nf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
}
}
/**
@ -999,16 +1082,21 @@ class FluentDateTime extends FluentType {
super(new Date(value), opts);
}
valueOf(ctx) {
const dtf = ctx._memoizeIntlObject(
Intl.DateTimeFormat, this.opts
);
return dtf.format(this.value);
toString(ctx) {
try {
const dtf = ctx._memoizeIntlObject(
Intl.DateTimeFormat, this.opts
);
return dtf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
}
}
}
class FluentSymbol extends FluentType {
valueOf() {
toString() {
return this.value;
}
@ -1029,9 +1117,6 @@ class FluentSymbol extends FluentType {
Intl.PluralRules, other.opts
);
return this.value === pr.select(other.value);
} else if (Array.isArray(other)) {
const values = other.map(symbol => symbol.value);
return values.includes(this.value);
}
return false;
}
@ -1052,9 +1137,9 @@ class FluentSymbol extends FluentType {
const builtins = {
'NUMBER': ([arg], opts) =>
new FluentNumber(arg.value, merge(arg.opts, opts)),
new FluentNumber(arg.valueOf(), merge(arg.opts, opts)),
'DATETIME': ([arg], opts) =>
new FluentDateTime(arg.value, merge(arg.opts, opts)),
new FluentDateTime(arg.valueOf(), merge(arg.opts, opts)),
};
function merge(argopts, opts) {
@ -1063,8 +1148,8 @@ function merge(argopts, opts) {
function values(opts) {
const unwrapped = {};
for (const name of Object.keys(opts)) {
unwrapped[name] = opts[name].value;
for (const [name, opt] of Object.entries(opts)) {
unwrapped[name] = opt.valueOf();
}
return unwrapped;
}
@ -1100,7 +1185,7 @@ function values(opts) {
*
* All other expressions (except for `FunctionReference` which is only used in
* `CallExpression`) resolve to an instance of `FluentType`. The caller should
* use the `valueOf` method to convert the instance to a native value.
* use the `toString` method to convert the instance to a native value.
*
*
* All functions in this file pass around a special object called `env`.
@ -1126,27 +1211,6 @@ const FSI = '\u2068';
const PDI = '\u2069';
/**
* Helper for computing the total character length of a placeable.
*
* Used in Pattern.
*
* @param {Object} env
* Resolver environment object.
* @param {Array} parts
* List of parts of a placeable.
* @returns {Number}
* @private
*/
function PlaceableLength(env, parts) {
const { ctx } = env;
return parts.reduce(
(sum, part) => sum + part.valueOf(ctx).length,
0
);
}
/**
* Helper for choosing the default value from a set of members.
*
@ -1186,48 +1250,21 @@ function DefaultMember(env, members, def) {
*/
function MessageReference(env, {name}) {
const { ctx, errors } = env;
const message = ctx.getMessage(name);
const message = name.startsWith('-')
? ctx._terms.get(name)
: ctx._messages.get(name);
if (!message) {
errors.push(new ReferenceError(`Unknown message: ${name}`));
const err = name.startsWith('-')
? new ReferenceError(`Unknown term: ${name}`)
: new ReferenceError(`Unknown message: ${name}`);
errors.push(err);
return new FluentNone(name);
}
return message;
}
/**
* Resolve an array of tags.
*
* @param {Object} env
* Resolver environment object.
* @param {Object} id
* The identifier of the message with tags.
* @param {String} id.name
* The name of the identifier.
* @returns {Array}
* @private
*/
function Tags(env, {name}) {
const { ctx, errors } = env;
const message = ctx.getMessage(name);
if (!message) {
errors.push(new ReferenceError(`Unknown message: ${name}`));
return new FluentNone(name);
}
if (!message.tags) {
errors.push(new RangeError(`No tags in message "${name}"`));
return new FluentNone(name);
}
return message.tags.map(
tag => new FluentSymbol(tag)
);
}
/**
* Resolve a variant expression to the variant object.
*
@ -1269,7 +1306,7 @@ function VariantExpression(env, {id, key}) {
}
}
errors.push(new ReferenceError(`Unknown variant: ${keyword.valueOf(ctx)}`));
errors.push(new ReferenceError(`Unknown variant: ${keyword.toString(ctx)}`));
return Type(env, message);
}
@ -1329,9 +1366,7 @@ function SelectExpression(env, {exp, vars, def}) {
return DefaultMember(env, vars, def);
}
const selector = exp.type === 'ref'
? Tags(env, exp)
: Type(env, exp);
const selector = Type(env, exp);
if (selector instanceof FluentNone) {
return DefaultMember(env, vars, def);
}
@ -1361,7 +1396,7 @@ function SelectExpression(env, {exp, vars, def}) {
* Resolve expression to a Fluent type.
*
* JavaScript strings are a special case. Since they natively have the
* `valueOf` method they can be used as if they were a Fluent type without
* `toString` method they can be used as if they were a Fluent type without
* paying the cost of creating a instance of one.
*
* @param {Object} env
@ -1386,7 +1421,7 @@ function Type(env, expr) {
switch (expr.type) {
case 'sym':
case 'varname':
return new FluentSymbol(expr.name);
case 'num':
return new FluentNumber(expr.val);
@ -1414,7 +1449,7 @@ function Type(env, expr) {
}
case undefined: {
// If it's a node with a value, resolve the value.
if (expr.val !== undefined) {
if (expr.val !== null && expr.val !== undefined) {
return Type(env, expr.val);
}
@ -1449,6 +1484,7 @@ function ExternalArgument(env, {name}) {
const arg = args[name];
// Return early if the argument already is an instance of FluentType.
if (arg instanceof FluentType) {
return arg;
}
@ -1524,7 +1560,7 @@ function CallExpression(env, {fun, args}) {
}
const posargs = [];
const keyargs = [];
const keyargs = {};
for (const arg of args) {
if (arg.type === 'narg') {
@ -1534,8 +1570,12 @@ function CallExpression(env, {fun, args}) {
}
}
// XXX functions should also report errors
return callee(posargs, keyargs);
try {
return callee(posargs, keyargs);
} catch (e) {
// XXX Report errors.
return new FluentNone();
}
}
/**
@ -1566,26 +1606,20 @@ function Pattern(env, ptn) {
continue;
}
const part = Type(env, elem);
const part = Type(env, elem).toString(ctx);
if (ctx._useIsolating) {
result.push(FSI);
}
if (Array.isArray(part)) {
const len = PlaceableLength(env, part);
if (len > MAX_PLACEABLE_LENGTH) {
errors.push(
new RangeError(
'Too many characters in placeable ' +
`(${len}, max allowed is ${MAX_PLACEABLE_LENGTH})`
)
);
result.push(new FluentNone());
} else {
result.push(...part);
}
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);
}
@ -1596,13 +1630,11 @@ function Pattern(env, ptn) {
}
dirty.delete(ptn);
return result;
return result.join('');
}
/**
* Format a translation into an `FluentType`.
*
* The return value must be unwrapped via `valueOf` by the caller.
* Format a translation into a string.
*
* @param {MessageContext} ctx
* A MessageContext instance which will be used to resolve the
@ -1620,7 +1652,7 @@ function resolve(ctx, args, message, errors = []) {
const env = {
ctx, args, errors, dirty: new WeakSet()
};
return Type(env, message);
return Type(env, message).toString(ctx);
}
/**
@ -1672,6 +1704,7 @@ class MessageContext {
constructor(locales, { functions = {}, useIsolating = true } = {}) {
this.locales = Array.isArray(locales) ? locales : [locales];
this._terms = new Map();
this._messages = new Map();
this._functions = functions;
this._useIsolating = useIsolating;
@ -1679,7 +1712,7 @@ class MessageContext {
}
/*
* Return an iterator over `[id, message]` pairs.
* Return an iterator over public `[id, message]` pairs.
*
* @returns {Iterator}
*/
@ -1701,7 +1734,7 @@ class MessageContext {
* Return the internal representation of a message.
*
* The internal representation should only be used as an argument to
* `MessageContext.format` and `MessageContext.formatToParts`.
* `MessageContext.format`.
*
* @param {string} id - The identifier of the message to check.
* @returns {Any}
@ -1731,67 +1764,18 @@ class MessageContext {
addMessages(source) {
const [entries, errors] = parse(source);
for (const id in entries) {
this._messages.set(id, entries[id]);
if (id.startsWith('-')) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from MessageContext.
this._terms.set(id, entries[id]);
} else {
this._messages.set(id, entries[id]);
}
}
return errors;
}
/**
* Format a message to an array of `FluentTypes` or null.
*
* Format a raw `message` from the context into an array of `FluentType`
* instances which may be used to build the final result. It may also return
* `null` if it has a null value. `args` will be used to resolve references
* to external arguments inside of the translation.
*
* See the documentation of {@link MessageContext#format} for more
* information about error handling.
*
* In case of errors `format` will try to salvage as much of the translation
* as possible and will still return a string. For performance reasons, the
* encountered errors are not returned but instead are appended to the
* `errors` array passed as the third argument.
*
* ctx.addMessages('hello = Hello, { $name }!');
* const hello = ctx.getMessage('hello');
* ctx.formatToParts(hello, { name: 'Jane' }, []);
* // → ['Hello, ', '\u2068', 'Jane', '\u2069']
*
* The returned parts need to be formatted via `valueOf` before they can be
* used further. This will ensure all values are correctly formatted
* according to the `MessageContext`'s locale.
*
* const parts = ctx.formatToParts(hello, { name: 'Jane' }, []);
* const str = parts.map(part => part.valueOf(ctx)).join('');
*
* @see MessageContext#format
* @param {Object | string} message
* @param {Object | undefined} args
* @param {Array} errors
* @returns {?Array<FluentType>}
*/
formatToParts(message, args, errors) {
// optimize entities which are simple strings with no attributes
if (typeof message === 'string') {
return [message];
}
// optimize simple-string entities with attributes
if (typeof message.val === 'string') {
return [message.val];
}
// optimize entities with null values
if (message.val === undefined) {
return null;
}
const result = resolve(this, args, message, errors);
return result instanceof FluentNone ? null : result;
}
/**
* Format a message to a string or null.
*
@ -1838,13 +1822,7 @@ class MessageContext {
return null;
}
const result = resolve(this, args, message, errors);
if (result instanceof FluentNone) {
return null;
}
return result.map(part => part.valueOf(this)).join('');
return resolve(this, args, message, errors);
}
_memoizeIntlObject(ctor, opts) {

17
intl/l10n/README Normal file
View File

@ -0,0 +1,17 @@
The content of this directory is partially sourced from the fluent.js project.
The following files are affected:
- MessageContext.jsm
- Localization.jsm
- DOMLocalization.jsm
- l10n.js
At the moment, the tool used to produce those files in fluent.js repository, doesn't
fully align with how the code is structured here, so we perform a manual adjustments
mostly around header and footer.
The result difference is stored in `./fluent.js.patch` file which can be used to
approximate the changes needed to be applied on the output of the
fluent.js/fluent-gecko's make.
In b.m.o. bug 1434434 we will try to reduce this difference to zero.

492
intl/l10n/fluent.js.patch Normal file
View File

@ -0,0 +1,492 @@
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 @@
/* fluent@0.6.0 */
-import Localization from '../../fluent-dom/src/localization.js';
+const { Localization } =
+ Components.utils.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 @@
}
}
-/* 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.
- */
-function defaultGenerateMessages(resourceIds) {
- const requestedLocales = Services.locale.getRequestedLocales();
- const availableLocales = L10nRegistry.getAvailableLocales();
- const defaultLocale = Services.locale.defaultLocale;
- const locales = Services.locale.negotiateLanguages(
- requestedLocales, availableLocales, defaultLocale,
- );
- return L10nRegistry.generateContexts(locales, resourceIds);
-}
-
-
-class GeckoDOMLocalization extends DOMLocalization {
- constructor(
- windowElement,
- resourceIds,
- generateMessages = defaultGenerateMessages
- ) {
- super(windowElement, resourceIds, generateMessages);
- }
-}
-
-this.DOMLocalization = GeckoDOMLocalization;
+this.DOMLocalization = DOMLocalization;
this.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 @@
-/* global Components, document, window */
{
const { DOMLocalization } =
- Components.utils.import('resource://gre/modules/DOMLocalization.jsm');
+ Components.utils.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 @@
/* fluent@0.6.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
- *
- * Arguments to functions are guaranteed to already be instances of
- * `FluentType`. Functions must return `FluentType` objects as well.
- */
+/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
+/* global console */
-/**
- * @overview
- *
- * The role of the Fluent resolver is to format a translation object to an
- * instance of `FluentType` or an array of instances.
- *
- * Translations can contain references to other messages 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.
- *
- * In case of errors the resolver will try to salvage as much of the
- * translation as possible. In rare situations where the resolver didn't know
- * how to recover from an error it will return an instance of `FluentNone`.
- *
- * `MessageReference`, `VariantExpression`, `AttributeExpression` and
- * `SelectExpression` resolve to raw Runtime Entries objects and the result of
- * the resolution needs to be passed into `Type` to get their real value.
- * This is useful for composing expressions. Consider:
- *
- * brand-name[nominative]
- *
- * which is a `VariantExpression` with properties `id: MessageReference` and
- * `key: Keyword`. If `MessageReference` was resolved eagerly, it would
- * instantly resolve to the value of the `brand-name` message. Instead, we
- * want to get the message object and look for its `nominative` variant.
- *
- * All other expressions (except for `FunctionReference` which is only used in
- * `CallExpression`) resolve to an instance of `FluentType`. The caller should
- * use the `toString` method to convert the instance to a native value.
- *
- *
- * All functions in this file pass around a special object called `env`.
- * This object stores a set of elements used by all resolve functions:
- *
- * * {MessageContext} ctx
- * context for which the given resolution is happening
- * * {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);
/*
* CachedIterable caches the elements yielded by an iterable.
@@ -170,87 +94,6 @@
}
}
-/*
- * @overview
- *
- * Functions for managing ordered sequences of MessageContexts.
- *
- * An ordered iterable of MessageContext instances can represent the current
- * negotiated fallback chain of languages. This iterable can be used to find
- * the best existing translation for a given identifier.
- *
- * The mapContext* methods can be used to find the first MessageContext in the
- * given iterable which contains the translation with the given identifier. If
- * the iterable is ordered according to the result of a language negotiation
- * the returned MessageContext contains the best available translation.
- *
- * A simple function which formats translations based on the identifier might
- * be implemented as follows:
- *
- * formatString(id, args) {
- * const ctx = mapContextSync(contexts, id);
- *
- * if (ctx === null) {
- * return id;
- * }
- *
- * const msg = ctx.getMessage(id);
- * return ctx.format(msg, args);
- * }
- *
- * In order to pass an iterator to mapContext*, wrap it in CachedIterable.
- * This allows multiple calls to mapContext* without advancing and eventually
- * depleting the iterator.
- *
- * function *generateMessages() {
- * // Some lazy logic for yielding MessageContexts.
- * yield *[ctx1, ctx2];
- * }
- *
- * 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.
+ */
+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);
+}
+
/**
* The `Localization` class is a central high-level API for vanilla
* JavaScript use of Fluent.
@@ -283,7 +146,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 @@
*/
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 @@
return val;
}
- handleEvent() {
- this.onLanguageChange();
+ /**
+ * Register 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');
+ }
+
+ /**
+ * 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':
+ this.onLanguageChange();
+ break;
+ default:
+ break;
+ }
}
/**
@@ -538,7 +431,8 @@
hasErrors = true;
}
- 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;
}
-/* 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', {});
-const ObserverService =
- Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
-const { Services } =
- 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
- * be localized into a different language - for example DevTools.
- */
-function defaultGenerateMessages(resourceIds) {
- const requestedLocales = Services.locale.getRequestedLocales();
- const availableLocales = L10nRegistry.getAvailableLocales();
- const defaultLocale = Services.locale.defaultLocale;
- const locales = Services.locale.negotiateLanguages(
- requestedLocales, availableLocales, defaultLocale,
- );
- return L10nRegistry.generateContexts(locales, resourceIds);
-}
-
-class GeckoLocalization extends Localization {
- constructor(resourceIds, generateMessages = defaultGenerateMessages) {
- super(resourceIds, generateMessages);
- }
-}
-
-this.Localization = GeckoLocalization;
+this.Localization = Localization;
this.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 @@
}
}
-/*
- * CachedIterable caches the elements yielded by an iterable.
- *
- * It can be used to iterate over an iterable many times without depleting the
- * iterable.
- */
-
-/*
- * @overview
- *
- * Functions for managing ordered sequences of MessageContexts.
- *
- * An ordered iterable of MessageContext instances can represent the current
- * negotiated fallback chain of languages. This iterable can be used to find
- * the best existing translation for a given identifier.
- *
- * The mapContext* methods can be used to find the first MessageContext in the
- * given iterable which contains the translation with the given identifier. If
- * the iterable is ordered according to the result of a language negotiation
- * the returned MessageContext contains the best available translation.
- *
- * A simple function which formats translations based on the identifier might
- * be implemented as follows:
- *
- * formatString(id, args) {
- * const ctx = mapContextSync(contexts, id);
- *
- * if (ctx === null) {
- * return id;
- * }
- *
- * const msg = ctx.getMessage(id);
- * return ctx.format(msg, args);
- * }
- *
- * In order to pass an iterator to mapContext*, wrap it in CachedIterable.
- * This allows multiple calls to mapContext* without advancing and eventually
- * depleting the iterator.
- *
- * function *generateMessages() {
- * // Some lazy logic for yielding MessageContexts.
- * yield *[ctx1, ctx2];
- * }
- *
- * 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'];

View File

@ -14,7 +14,6 @@ function test_methods_presence(MessageContext) {
const ctx = new MessageContext(["en-US", "pl"]);
equal(typeof ctx.addMessages, "function");
equal(typeof ctx.format, "function");
equal(typeof ctx.formatToParts, "function");
}
function test_methods_calling(MessageContext) {
@ -25,15 +24,10 @@ function test_methods_calling(MessageContext) {
const msg = ctx.getMessage("key");
equal(ctx.format(msg), "Value");
deepEqual(ctx.formatToParts(msg), ["Value"]);
ctx.addMessages("key2 = Hello { $name }");
const msg2 = ctx.getMessage("key2");
equal(ctx.format(msg2, { name: "Amy" }), "Hello Amy");
deepEqual(ctx.formatToParts(msg2), ["Hello ", {
value: "name",
opts: undefined
}]);
ok(true);
}