mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 03:15:11 +00:00
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:
parent
e483ecaab0
commit
64594123bb
@ -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.
|
||||
|
@ -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' }],
|
||||
|
@ -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
17
intl/l10n/README
Normal 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
492
intl/l10n/fluent.js.patch
Normal 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
|
||||
// &, &, &.
|
||||
@@ -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'];
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user