New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@fluent/bundle

Package Overview
Dependencies
Maintainers
3
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluent/bundle - npm Package Compare versions

Comparing version

to
0.14.0

src/scope.js

108

CHANGELOG.md
# Changelog
## @fluent/bundle 0.14.0 (July 30, 2019)
### `FluentBundle` API
- Remove `FluentBundle.addMessages`.
Use `FluentBundle.addResource` instead, combined with a `FluentResource`
instance.
- Remove `FluentBundle.messages`.
The list of messages in the bundle should not be inspected. If you need
to do it, please use the tooling parser from `@fluent/syntax`.
- Change the shape returned by `FluentBundle.getMessage`.
The method now returns the following shape:
{value: Pattern | null, attributes: Record<string, Pattern>}
- The internal representtion of `Pattern` is private.
Raw messages returned from `getMessage` have their values and attributes
stored in a `Pattern` type. The internal representation of this type is
private and implementation-specific. It should not be inspected nor used
for any purposes. The implementation may change without a warning in
future releases. `Patterns` are black boxes and are meant to be used as
arguments to `formatPattern`.
- Rename `FluentBundle.format` to `formatPattern`.
`formatPattern` only accepts valid `Patterns` as the first argument. In
practice, you'll want to first retrieve a raw message from the bundle via
`getMessage`, and then format the value (if present) and the attributes
separately.
```js
let message = bundle.getMessage("hello");
if (message.value) {
bundle.formatPattern(message.value, {userName: "Alex"});
}
```
The list of all attributes defined in the message can be obtained with
`Object.keys(message.attributes)`.
- Throw from `formatPattern` when `errors` are not passed as an argument.
The old `format()` method would silence all errors if the thrid argument,
the `errors` array was not given. `formatPattern` changes this behavior
to throwing on the first encountered error and interrupting the
formatting.
```js
try {
bundle.formatPattern(message.value, args);
} catch (err) {
// Handle the error yourself.
}
```
It's still possible to pass the `errors` array as the third argument to
`formatPattern`. All errors encountered during the formatting will be
then appended to the array. In this scenario, `formatPattern` is
guaranteed to never throw because of errors in the translation.
```js
let errorrs = [];
bundle.formatPattern(message.value, args, errors);
for (let error of errors) {
// Report errors.
}
```
### `FluentResource` API
- Remove the static `FluentResource.fromString` method.
Parse resources by using the constructor: `new FluentResource(text)`.
- Do not extend `Map`.
`FluentResources` are now instances of their own class only.
- Add `FluentResource.body`.
The `body` field is an array storing the resource's parsed messages and
terms.
### `FluentType` API
- `FluentNumber` must be instantiated with a number.
The constructor doesn't call `parseFloat` on the passed value anymore.
- `FluentDateTime` must be instantiated with a number.
The constructor doesn't call `new Date()` on the passed value anymore.
The date is stored as the numerical timestamp, in milliseconds since the
epoch.
### Formatting Changes
- Report errors from instantiating Intl objects used for formatting.
- Report errors from functions, including built-in functions.
- Format numbers and dates to safe defaults in case or errors. (#410)
- When a transform function is given, transform only `TextElements`.
## @fluent/bundle 0.13.0 (July 25, 2019)

@@ -4,0 +112,0 @@

1005

compat.js

@@ -1,2 +0,2 @@

/* @fluent/bundle@0.13.0 */
/* @fluent/bundle@0.14.0 */
(function (global, factory) {

@@ -8,2 +8,149 @@ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :

/* 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.
*/
class FluentType {
/**
* Create a `FluentType` instance.
*
* @param {Any} value - JavaScript value to wrap.
* @returns {FluentType}
*/
constructor(value) {
/** The wrapped native value. */
this.value = value;
}
/**
* Unwrap the raw value stored by this `FluentType`.
*
* @returns {Any}
*/
valueOf() {
return this.value;
}
/**
* Format this instance of `FluentType` to a string.
*
* Formatted values are suitable for use outside of the `FluentBundle`.
* This method can use `Intl` formatters available through the `scope`
* argument.
*
* @abstract
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
// eslint-disable-line no-unused-vars
throw new Error("Subclasses of FluentType must implement toString.");
}
}
/**
* A `FluentType` representing no correct value.
*/
class FluentNone extends FluentType {
/**
* Create an instance of `FluentNone` with an optional fallback value.
* @param {string} value - The fallback value of this `FluentNone`.
* @returns {FluentType}
*/
constructor() {
let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "???";
super(value);
}
/**
* Format this `FluentNone` to the fallback string.
* @returns {string}
*/
toString() {
return "{".concat(this.value, "}");
}
}
/**
* A `FluentType` representing a number.
*/
class FluentNumber extends FluentType {
/**
* Create an instance of `FluentNumber` with options to the
* `Intl.NumberFormat` constructor.
* @param {number} value
* @param {Intl.NumberFormatOptions} opts
* @returns {FluentType}
*/
constructor(value, opts) {
super(value);
/** Options passed to Intl.NumberFormat. */
this.opts = opts;
}
/**
* Format this `FluentNumber` to a string.
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
try {
const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts);
return nf.format(this.value);
} catch (err) {
scope.reportError(err);
return this.value.toString(10);
}
}
}
/**
* A `FluentType` representing a date and time.
*/
class FluentDateTime extends FluentType {
/**
* Create an instance of `FluentDateTime` with options to the
* `Intl.DateTimeFormat` constructor.
* @param {number} value - timestamp in milliseconds
* @param {Intl.DateTimeFormatOptions} opts
* @returns {FluentType}
*/
constructor(value, opts) {
super(value);
/** Options passed to Intl.DateTimeFormat. */
this.opts = opts;
}
/**
* Format this `FluentDateTime` to a string.
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
try {
const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
return dtf.format(this.value);
} catch (err) {
scope.reportError(err);
return new Date(this.value).toISOString();
}
}
}
function _defineProperty(obj, key, value) {

@@ -81,95 +228,2 @@ if (key in obj) {

/* 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.
*/
class FluentType {
/**
* Create an `FluentType` instance.
*
* @param {Any} value - JavaScript value to wrap.
* @param {Object} opts - Configuration.
* @returns {FluentType}
*/
constructor(value, opts) {
this.value = value;
this.opts = opts;
}
/**
* Unwrap the raw value stored by this `FluentType`.
*
* @returns {Any}
*/
valueOf() {
return this.value;
}
/**
* Format this instance of `FluentType` to a string.
*
* Formatted values are suitable for use outside of the `FluentBundle`.
* This method can use `Intl` formatters memoized by the `FluentBundle`
* instance passed as an argument.
*
* @param {FluentBundle} [bundle]
* @returns {string}
*/
toString() {
throw new Error("Subclasses of FluentType must implement toString.");
}
}
class FluentNone extends FluentType {
valueOf() {
return null;
}
toString() {
return "{".concat(this.value || "???", "}");
}
}
class FluentNumber extends FluentType {
constructor(value, opts) {
super(parseFloat(value), opts);
}
toString(bundle) {
try {
const nf = bundle._memoizeIntlObject(Intl.NumberFormat, this.opts);
return nf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
}
}
}
class FluentDateTime extends FluentType {
constructor(value, opts) {
super(new Date(value), opts);
}
toString(bundle) {
try {
const dtf = bundle._memoizeIntlObject(Intl.DateTimeFormat, this.opts);
return dtf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
}
}
}
function merge(argopts, opts) {

@@ -198,10 +252,12 @@ return Object.assign({}, argopts, values(opts));

if (arg instanceof FluentNone) {
return arg;
return new FluentNone("NUMBER(".concat(arg.valueOf(), ")"));
}
if (arg instanceof FluentNumber) {
return new FluentNumber(arg.valueOf(), merge(arg.opts, opts));
let value = Number(arg.valueOf());
if (Number.isNaN(value)) {
throw new TypeError("Invalid argument to NUMBER");
}
return new FluentNone("NUMBER()");
return new FluentNumber(value, merge(arg.opts, opts));
}

@@ -213,10 +269,12 @@ function DATETIME(_ref3, opts) {

if (arg instanceof FluentNone) {
return arg;
return new FluentNone("DATETIME(".concat(arg.valueOf(), ")"));
}
if (arg instanceof FluentDateTime) {
return new FluentDateTime(arg.valueOf(), merge(arg.opts, opts));
let value = Number(arg.valueOf());
if (Number.isNaN(value)) {
throw new TypeError("Invalid argument to DATETIME");
}
return new FluentNone("DATETIME()");
return new FluentDateTime(value, merge(arg.opts, opts));
}

@@ -234,3 +292,3 @@

function match(bundle, selector, key) {
function match(scope, selector, key) {
if (key === selector) {

@@ -247,3 +305,3 @@ // Both are strings.

if (selector instanceof FluentNumber && typeof key === "string") {
let category = bundle._memoizeIntlObject(Intl.PluralRules, selector.opts).select(selector.value);
let category = scope.memoizeIntlObject(Intl.PluralRules, selector.opts).select(selector.value);

@@ -261,6 +319,6 @@ if (key === category) {

if (variants[star]) {
return Type(scope, variants[star]);
return resolvePattern(scope, variants[star].value);
}
scope.errors.push(new RangeError("No default"));
scope.reportError(new RangeError("No default"));
return new FluentNone();

@@ -282,5 +340,5 @@ } // Helper: resolve arguments to a call expression.

if (arg.type === "narg") {
named[arg.name] = Type(scope, arg.value);
named[arg.name] = resolveExpression(scope, arg.value);
} else {
positional.push(Type(scope, arg));
positional.push(resolveExpression(scope, arg));
}

@@ -307,21 +365,3 @@ }

function Type(scope, expr) {
// A fast-path for strings which are the most common case. Since they
// natively have the `toString` method they can be used as if they were
// a FluentType instance without incurring the cost of creating one.
if (typeof expr === "string") {
return scope.bundle._transform(expr);
} // A fast-path for `FluentNone` which doesn't require any additional logic.
if (expr instanceof FluentNone) {
return expr;
} // The Runtime AST (Entries) encodes patterns (complex strings with
// placeables) as Arrays.
if (Array.isArray(expr)) {
return Pattern(scope, expr);
}
function resolveExpression(scope, expr) {
switch (expr.type) {

@@ -351,13 +391,2 @@ case "str":

case undefined:
{
// If it's a node with a value, resolve the value.
if (expr.value !== null && expr.value !== undefined) {
return Type(scope, expr.value);
}
scope.errors.push(new RangeError("No value"));
return new FluentNone();
}
default:

@@ -374,3 +403,3 @@ return new FluentNone();

if (scope.insideTermReference === false) {
scope.errors.push(new ReferenceError("Unknown variable: ".concat(name)));
scope.reportError(new ReferenceError("Unknown variable: $".concat(name)));
}

@@ -397,7 +426,7 @@

if (arg instanceof Date) {
return new FluentDateTime(arg);
return new FluentDateTime(arg.getTime());
}
default:
scope.errors.push(new TypeError("Unsupported variable type: ".concat(name, ", ").concat(typeof arg)));
scope.reportError(new TypeError("Variable type not supported: $".concat(name, ", ").concat(typeof arg)));
return new FluentNone("$".concat(name));

@@ -415,4 +444,3 @@ }

if (!message) {
const err = new ReferenceError("Unknown message: ".concat(name));
scope.errors.push(err);
scope.reportError(new ReferenceError("Unknown message: ".concat(name)));
return new FluentNone(name);

@@ -422,13 +450,18 @@ }

if (attr) {
const attribute = message.attrs && message.attrs[attr];
const attribute = message.attributes[attr];
if (attribute) {
return Type(scope, attribute);
return resolvePattern(scope, attribute);
}
scope.errors.push(new ReferenceError("Unknown attribute: ".concat(attr)));
scope.reportError(new ReferenceError("Unknown attribute: ".concat(attr)));
return new FluentNone("".concat(name, ".").concat(attr));
}
return Type(scope, message);
if (message.value) {
return resolvePattern(scope, message.value);
}
scope.reportError(new ReferenceError("No value: ".concat(name)));
return new FluentNone(name);
} // Resolve a call to a Term with key-value arguments.

@@ -446,6 +479,5 @@

if (!term) {
const err = new ReferenceError("Unknown term: ".concat(id));
scope.errors.push(err);
scope.reportError(new ReferenceError("Unknown term: ".concat(id)));
return new FluentNone(id);
} // Every TermReference has its own args.
} // Every TermReference has its own variables.

@@ -455,21 +487,18 @@

_getArguments2 = _slicedToArray(_getArguments, 2),
keyargs = _getArguments2[1];
params = _getArguments2[1];
const local = _objectSpread({}, scope, {
args: keyargs,
insideTermReference: true
});
const local = scope.cloneForTermReference(params);
if (attr) {
const attribute = term.attrs && term.attrs[attr];
const attribute = term.attributes[attr];
if (attribute) {
return Type(local, attribute);
return resolvePattern(local, attribute);
}
scope.errors.push(new ReferenceError("Unknown attribute: ".concat(attr)));
scope.reportError(new ReferenceError("Unknown attribute: ".concat(attr)));
return new FluentNone("".concat(id, ".").concat(attr));
}
return Type(local, term);
return resolvePattern(local, term.value);
} // Resolve a call to a Function with positional and key-value arguments.

@@ -486,3 +515,3 @@

if (!func) {
scope.errors.push(new ReferenceError("Unknown function: ".concat(name, "()")));
scope.reportError(new ReferenceError("Unknown function: ".concat(name, "()")));
return new FluentNone("".concat(name, "()"));

@@ -492,3 +521,3 @@ }

if (typeof func !== "function") {
scope.errors.push(new TypeError("Function ".concat(name, "() is not callable")));
scope.reportError(new TypeError("Function ".concat(name, "() is not callable")));
return new FluentNone("".concat(name, "()"));

@@ -499,4 +528,4 @@ }

return func(...getArguments(scope, args));
} catch (e) {
// XXX Report errors.
} catch (err) {
scope.reportError(err);
return new FluentNone("".concat(name, "()"));

@@ -511,7 +540,6 @@ }

star = _ref5.star;
let sel = Type(scope, selector);
let sel = resolveExpression(scope, selector);
if (sel instanceof FluentNone) {
const variant = getDefault(scope, variants, star);
return Type(scope, variant);
return getDefault(scope, variants, star);
} // Match the selector against keys of each variant, in order.

@@ -527,6 +555,6 @@

const variant = _step2.value;
const key = Type(scope, variant.key);
const key = resolveExpression(scope, variant.key);
if (match(scope.bundle, sel, key)) {
return Type(scope, variant);
if (match(scope, sel, key)) {
return resolvePattern(scope, variant.value);
}

@@ -549,10 +577,9 @@ }

const variant = getDefault(scope, variants, star);
return Type(scope, variant);
return getDefault(scope, variants, star);
} // Resolve a pattern (a complex string with placeables).
function Pattern(scope, ptn) {
function resolveComplexPattern(scope, ptn) {
if (scope.dirty.has(ptn)) {
scope.errors.push(new RangeError("Cyclic reference"));
scope.reportError(new RangeError("Cyclic reference"));
return new FluentNone();

@@ -580,3 +607,3 @@ } // Tag the pattern as dirty for the purpose of the current resolution.

const part = Type(scope, elem).toString(scope.bundle);
const part = resolveExpression(scope, elem).toString(scope);

@@ -588,8 +615,12 @@ if (useIsolating) {

if (part.length > MAX_PLACEABLE_LENGTH) {
scope.errors.push(new RangeError("Too many characters in placeable " + "(".concat(part.length, ", max allowed is ").concat(MAX_PLACEABLE_LENGTH, ")")));
result.push(part.slice(MAX_PLACEABLE_LENGTH));
} else {
result.push(part);
scope.dirty.delete(ptn); // This is a fatal error which causes the resolver to instantly bail out
// on this pattern. The length check protects against excessive memory
// usage, and throwing protects against eating up the CPU when long
// placeables are deeply nested.
throw new RangeError("Too many characters in placeable " + "(".concat(part.length, ", max allowed is ").concat(MAX_PLACEABLE_LENGTH, ")"));
}
result.push(part);
if (useIsolating) {

@@ -616,31 +647,264 @@ result.push(PDI);

return result.join("");
} // Resolve a simple or a complex Pattern to a FluentString (which is really the
// string primitive).
function resolvePattern(scope, node) {
// Resolve a simple pattern.
if (typeof node === "string") {
return scope.bundle._transform(node);
}
return resolveComplexPattern(scope, node);
}
class Scope {
constructor(bundle, errors, args) {
let insideTermReference = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
let dirty = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : new WeakSet();
/** The bundle for which the given resolution is happening. */
this.bundle = bundle;
/** The list of errors collected while resolving. */
this.errors = errors;
/** A dict of developer-provided variables. */
this.args = args;
/** Term references require different variable lookup logic. */
this.insideTermReference = insideTermReference;
/** The Set of patterns already encountered during this resolution.
* Used to detect and prevent cyclic resolutions. */
this.dirty = dirty;
}
cloneForTermReference(args) {
return new Scope(this.bundle, this.errors, args, true, this.dirty);
}
reportError(error) {
if (!this.errors) {
throw error;
}
this.errors.push(error);
}
memoizeIntlObject(ctor, opts) {
let cache = this.bundle._intls.get(ctor);
if (!cache) {
cache = {};
this.bundle._intls.set(ctor, cache);
}
let id = JSON.stringify(opts);
if (!cache[id]) {
cache[id] = new ctor(this.bundle.locales, opts);
}
return cache[id];
}
}
/**
* Format a translation into a string.
*
* @param {FluentBundle} bundle
* A FluentBundle instance which will be used to resolve the
* contextual information of the message.
* @param {Object} args
* List of arguments provided by the developer which can be accessed
* from the message.
* @param {Object} message
* An object with the Message to be resolved.
* @param {Array} errors
* An error array that any encountered errors will be appended to.
* @returns {FluentType}
* Message bundles are single-language stores of translation resources. They are
* responsible for formatting message values and attributes to strings.
*/
class FluentBundle {
/**
* Create an instance of `FluentBundle`.
*
* The `locales` argument is used to instantiate `Intl` formatters used by
* translations. The `options` object can be used to configure the bundle.
*
* Examples:
*
* let bundle = new FluentBundle(["en-US", "en"]);
*
* let bundle = new FluentBundle(locales, {useIsolating: false});
*
* let bundle = new FluentBundle(locales, {
* useIsolating: true,
* functions: {
* NODE_ENV: () => process.env.NODE_ENV
* }
* });
*
* Available options:
*
* - `functions` - an object of additional functions available to
* translations as builtins.
*
* - `useIsolating` - boolean specifying whether to use Unicode isolation
* marks (FSI, PDI) for bidi interpolations. Default: `true`.
*
* - `transform` - a function used to transform string parts of patterns.
*
* @param {(string|Array.<string>)} locales - The locales of the bundle
* @param {Object} [options]
* @returns {FluentBundle}
*/
constructor(locales) {
let _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$functions = _ref.functions,
functions = _ref$functions === void 0 ? {} : _ref$functions,
_ref$useIsolating = _ref.useIsolating,
useIsolating = _ref$useIsolating === void 0 ? true : _ref$useIsolating,
_ref$transform = _ref.transform,
transform = _ref$transform === void 0 ? v => v : _ref$transform;
function resolve(bundle, args, message) {
let errors = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
const scope = {
bundle,
args,
errors,
dirty: new WeakSet(),
// TermReferences are resolved in a new scope.
insideTermReference: false
};
return Type(scope, message).toString(bundle);
this.locales = Array.isArray(locales) ? locales : [locales];
this._terms = new Map();
this._messages = new Map();
this._functions = functions;
this._useIsolating = useIsolating;
this._transform = transform;
this._intls = new WeakMap();
}
/**
* Check if a message is present in the bundle.
*
* @param {string} id - The identifier of the message to check.
* @returns {bool}
*/
hasMessage(id) {
return this._messages.has(id);
}
/**
* Return a raw unformatted message object from the bundle.
*
* Raw messages are `{value, attributes}` shapes containing translation units
* called `Patterns`. `Patterns` are implementation-specific; they should be
* treated as black boxes and formatted with `FluentBundle.formatPattern`.
*
* interface RawMessage {
* value: Pattern | null;
* attributes: Record<string, Pattern>;
* }
*
* @param {string} id - The identifier of the message to check.
* @returns {{value: ?Pattern, attributes: Object.<string, Pattern>}}
*/
getMessage(id) {
return this._messages.get(id);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must be an instance of `FluentResource`.
*
* let res = new FluentResource("foo = Foo");
* bundle.addResource(res);
* bundle.getMessage("foo");
* // → {value: .., attributes: {..}}
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value. Default: `false`.
*
* @param {FluentResource} res - FluentResource object.
* @param {Object} [options]
* @returns {Array.<FluentError>}
*/
addResource(res) {
let _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref2$allowOverrides = _ref2.allowOverrides,
allowOverrides = _ref2$allowOverrides === void 0 ? false : _ref2$allowOverrides;
const errors = [];
for (let i = 0; i < res.body.length; i++) {
let entry = res.body[i];
if (entry.id.startsWith("-")) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from FluentBundle.
if (allowOverrides === false && this._terms.has(entry.id)) {
errors.push("Attempt to override an existing term: \"".concat(entry.id, "\""));
continue;
}
this._terms.set(entry.id, entry);
} else {
if (allowOverrides === false && this._messages.has(entry.id)) {
errors.push("Attempt to override an existing message: \"".concat(entry.id, "\""));
continue;
}
this._messages.set(entry.id, entry);
}
}
return errors;
}
/**
* Format a `Pattern` to a string.
*
* Format a raw `Pattern` into a string. `args` will be used to resolve
* references to variables passed as arguments to the translation.
*
* In case of errors `formatPattern` 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.
*
* let errors = [];
* bundle.addResource(
* new FluentResource("hello = Hello, {$name}!"));
*
* let hello = bundle.getMessage("hello");
* if (hello.value) {
* bundle.formatPattern(hello.value, {name: "Jane"}, errors);
* // Returns "Hello, Jane!" and `errors` is empty.
*
* bundle.formatPattern(hello.value, undefined, errors);
* // Returns "Hello, {$name}!" and `errors` is now:
* // [<ReferenceError: Unknown variable: name>]
* }
*
* If `errors` is omitted, the first encountered error will be thrown.
*
* @param {Pattern} pattern
* @param {?Object} args
* @param {?Array.<Error>} errors
* @returns {string}
*/
formatPattern(pattern, args, errors) {
// Resolve a simple pattern without creating a scope. No error handling is
// required; by definition simple patterns don't have placeables.
if (typeof pattern === "string") {
return this._transform(pattern);
} // Resolve a complex pattern.
let scope = new Scope(this, errors, args);
try {
let value = resolveComplexPattern(scope, pattern);
return value.toString(scope);
} catch (err) {
if (scope.errors) {
scope.errors.push(err);
return new FluentNone().toString(scope);
}
throw err;
}
}
}

@@ -695,12 +959,13 @@

/**
* Fluent Resource is a structure storing a map of parsed localization entries.
* Fluent Resource is a structure storing parsed localization entries.
*/
class FluentResource extends Map {
/**
* Create a new FluentResource from Fluent code.
*/
static fromString(source) {
class FluentResource {
constructor(source) {
this.body = this._parse(source);
}
_parse(source) {
RE_MESSAGE_START.lastIndex = 0;
let resource = new this();
let resource = [];
let cursor = 0; // Iterate over the beginnings of messages and terms to efficiently skip

@@ -719,3 +984,3 @@ // comments and recover from errors.

try {
resource.set(next[1], parseMessage());
resource.push(parseMessage(next[1]));
} catch (err) {

@@ -732,3 +997,4 @@ if (err instanceof FluentError) {

return resource; // The parser implementation is inlined below for performance reasons.
return resource; // The parser implementation is inlined below for performance reasons,
// as well as for convenience of accessing `source` and `cursor`.
// The parser focuses on minimizing the number of false negatives at the

@@ -799,17 +1065,14 @@ // expense of increasing the risk of false positives. In other words, it

function parseMessage() {
function parseMessage(id) {
let value = parsePattern();
let attrs = parseAttributes();
let attributes = parseAttributes();
if (attrs === null) {
if (value === null) {
throw new FluentError("Expected message value or attributes");
}
return value;
if (value === null && Object.keys(attributes).length === 0) {
throw new FluentError("Expected message value or attributes");
}
return {
id,
value,
attrs
attributes
};

@@ -819,3 +1082,3 @@ }

function parseAttributes() {
let attrs = {};
let attrs = Object.create(null);

@@ -833,3 +1096,3 @@ while (test(RE_ATTRIBUTE_START)) {

return Object.keys(attrs).length > 0 ? attrs : null;
return attrs;
}

@@ -929,5 +1192,2 @@

element = element.value.slice(0, element.value.length - commonIndent);
} else if (element.type === "str") {
// Optimize StringLiterals into their value.
element = element.value;
}

@@ -1122,3 +1382,6 @@

consumeToken(TOKEN_BRACKET_OPEN, FluentError);
let key = test(RE_NUMBER_LITERAL) ? parseNumberLiteral() : match1(RE_IDENTIFIER);
let key = test(RE_NUMBER_LITERAL) ? parseNumberLiteral() : {
type: "str",
value: match1(RE_IDENTIFIER)
};
consumeToken(TOKEN_BRACKET_CLOSE, FluentError);

@@ -1257,292 +1520,2 @@ return key;

/**
* Message bundles 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 `FluentBundle.format` to retrieve translation units from a
* bundle. Translations can contain references to other entities or variables,
* 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
* bundle's language. See the documentation of the Fluent syntax for more
* information.
*/
class FluentBundle {
/**
* Create an instance of `FluentBundle`.
*
* The `locales` argument is used to instantiate `Intl` formatters used by
* translations. The `options` object can be used to configure the bundle.
*
* Examples:
*
* const bundle = new FluentBundle(locales);
*
* const bundle = new FluentBundle(locales, { useIsolating: false });
*
* const bundle = new FluentBundle(locales, {
* useIsolating: true,
* functions: {
* NODE_ENV: () => process.env.NODE_ENV
* }
* });
*
* Available options:
*
* - `functions` - an object of additional functions available to
* translations as builtins.
*
* - `useIsolating` - boolean specifying whether to use Unicode isolation
* marks (FSI, PDI) for bidi interpolations.
* Default: true
*
* - `transform` - a function used to transform string parts of patterns.
*
* @param {string|Array<string>} locales - Locale or locales of the bundle
* @param {Object} [options]
* @returns {FluentBundle}
*/
constructor(locales) {
let _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$functions = _ref.functions,
functions = _ref$functions === void 0 ? {} : _ref$functions,
_ref$useIsolating = _ref.useIsolating,
useIsolating = _ref$useIsolating === void 0 ? true : _ref$useIsolating,
_ref$transform = _ref.transform,
transform = _ref$transform === void 0 ? v => v : _ref$transform;
this.locales = Array.isArray(locales) ? locales : [locales];
this._terms = new Map();
this._messages = new Map();
this._functions = functions;
this._useIsolating = useIsolating;
this._transform = transform;
this._intls = new WeakMap();
}
/*
* Return an iterator over public `[id, message]` pairs.
*
* @returns {Iterator}
*/
get messages() {
return this._messages[Symbol.iterator]();
}
/*
* Check if a message is present in the bundle.
*
* @param {string} id - The identifier of the message to check.
* @returns {bool}
*/
hasMessage(id) {
return this._messages.has(id);
}
/*
* Return the internal representation of a message.
*
* The internal representation should only be used as an argument to
* `FluentBundle.format`.
*
* @param {string} id - The identifier of the message to check.
* @returns {Any}
*/
getMessage(id) {
return this._messages.get(id);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must use the Fluent syntax. It will be parsed by
* the bundle and each translation unit (message) will be available in the
* bundle by its identifier.
*
* bundle.addMessages('foo = Foo');
* bundle.getMessage('foo');
*
* // Returns a raw representation of the 'foo' message.
*
* bundle.addMessages('bar = Bar');
* bundle.addMessages('bar = Newbar', { allowOverrides: true });
* bundle.getMessage('bar');
*
* // Returns a raw representation of the 'bar' message: Newbar.
*
* Parsed entities should be formatted with the `format` method in case they
* contain logic (references, select expressions etc.).
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value.
* Default: false
*
* @param {string} source - Text resource with translations.
* @param {Object} [options]
* @returns {Array<Error>}
*/
addMessages(source, options) {
const res = FluentResource.fromString(source);
return this.addResource(res, options);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must be an instance of FluentResource,
* e.g. parsed by `FluentResource.fromString`.
*
* let res = FluentResource.fromString("foo = Foo");
* bundle.addResource(res);
* bundle.getMessage('foo');
*
* // Returns a raw representation of the 'foo' message.
*
* let res = FluentResource.fromString("bar = Bar");
* bundle.addResource(res);
* res = FluentResource.fromString("bar = Newbar");
* bundle.addResource(res, { allowOverrides: true });
* bundle.getMessage('bar');
*
* // Returns a raw representation of the 'bar' message: Newbar.
*
* Parsed entities should be formatted with the `format` method in case they
* contain logic (references, select expressions etc.).
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value.
* Default: false
*
* @param {FluentResource} res - FluentResource object.
* @param {Object} [options]
* @returns {Array<Error>}
*/
addResource(res) {
let _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref2$allowOverrides = _ref2.allowOverrides,
allowOverrides = _ref2$allowOverrides === void 0 ? false : _ref2$allowOverrides;
const errors = [];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = res[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
const _step$value = _slicedToArray(_step.value, 2),
id = _step$value[0],
value = _step$value[1];
if (id.startsWith("-")) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from FluentBundle.
if (allowOverrides === false && this._terms.has(id)) {
errors.push("Attempt to override an existing term: \"".concat(id, "\""));
continue;
}
this._terms.set(id, value);
} else {
if (allowOverrides === false && this._messages.has(id)) {
errors.push("Attempt to override an existing message: \"".concat(id, "\""));
continue;
}
this._messages.set(id, value);
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
return errors;
}
/**
* Format a message to a string or null.
*
* Format a raw `message` from the bundle into a string (or a null if it has
* a null value). `args` will be used to resolve references to variables
* passed as arguments to the translation.
*
* 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.
*
* const errors = [];
* bundle.addMessages('hello = Hello, { $name }!');
* const hello = bundle.getMessage('hello');
* bundle.format(hello, { name: 'Jane' }, errors);
*
* // Returns 'Hello, Jane!' and `errors` is empty.
*
* bundle.format(hello, undefined, errors);
*
* // Returns 'Hello, name!' and `errors` is now:
*
* [<ReferenceError: Unknown variable: name>]
*
* @param {Object | string} message
* @param {Object | undefined} args
* @param {Array} errors
* @returns {?string}
*/
format(message, args, errors) {
// optimize entities which are simple strings with no attributes
if (typeof message === "string") {
return this._transform(message);
} // optimize entities with null values
if (message === null || message.value === null) {
return null;
} // optimize simple-string entities with attributes
if (typeof message.value === "string") {
return this._transform(message.value);
}
return resolve(this, args, message, errors);
}
_memoizeIntlObject(ctor, opts) {
const cache = this._intls.get(ctor) || {};
const id = JSON.stringify(opts);
if (!cache[id]) {
cache[id] = new ctor(this.locales, opts);
this._intls.set(ctor, cache);
}
return cache[id];
}
}
/*
* @module fluent

@@ -1549,0 +1522,0 @@ * @overview

@@ -1,2 +0,2 @@

/* @fluent/bundle@0.13.0 */
/* @fluent/bundle@0.14.0 */
(function (global, factory) {

@@ -19,11 +19,10 @@ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :

/**
* Create an `FluentType` instance.
* Create a `FluentType` instance.
*
* @param {Any} value - JavaScript value to wrap.
* @param {Object} opts - Configuration.
* @param {Any} value - JavaScript value to wrap.
* @returns {FluentType}
*/
constructor(value, opts) {
constructor(value) {
/** The wrapped native value. */
this.value = value;
this.opts = opts;
}

@@ -44,9 +43,10 @@

* Formatted values are suitable for use outside of the `FluentBundle`.
* This method can use `Intl` formatters memoized by the `FluentBundle`
* instance passed as an argument.
* This method can use `Intl` formatters available through the `scope`
* argument.
*
* @param {FluentBundle} [bundle]
* @abstract
* @param {Scope} scope
* @returns {string}
*/
toString() {
toString(scope) { // eslint-disable-line no-unused-vars
throw new Error("Subclasses of FluentType must implement toString.");

@@ -56,26 +56,53 @@ }

/**
* A `FluentType` representing no correct value.
*/
class FluentNone extends FluentType {
valueOf() {
return null;
/**
* Create an instance of `FluentNone` with an optional fallback value.
* @param {string} value - The fallback value of this `FluentNone`.
* @returns {FluentType}
*/
constructor(value = "???") {
super(value);
}
/**
* Format this `FluentNone` to the fallback string.
* @returns {string}
*/
toString() {
return `{${this.value || "???"}}`;
return `{${this.value}}`;
}
}
/**
* A `FluentType` representing a number.
*/
class FluentNumber extends FluentType {
/**
* Create an instance of `FluentNumber` with options to the
* `Intl.NumberFormat` constructor.
* @param {number} value
* @param {Intl.NumberFormatOptions} opts
* @returns {FluentType}
*/
constructor(value, opts) {
super(parseFloat(value), opts);
super(value);
/** Options passed to Intl.NumberFormat. */
this.opts = opts;
}
toString(bundle) {
/**
* Format this `FluentNumber` to a string.
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
try {
const nf = bundle._memoizeIntlObject(
Intl.NumberFormat, this.opts
);
const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts);
return nf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
} catch (err) {
scope.reportError(err);
return this.value.toString(10);
}

@@ -85,16 +112,31 @@ }

/**
* A `FluentType` representing a date and time.
*/
class FluentDateTime extends FluentType {
/**
* Create an instance of `FluentDateTime` with options to the
* `Intl.DateTimeFormat` constructor.
* @param {number} value - timestamp in milliseconds
* @param {Intl.DateTimeFormatOptions} opts
* @returns {FluentType}
*/
constructor(value, opts) {
super(new Date(value), opts);
super(value);
/** Options passed to Intl.DateTimeFormat. */
this.opts = opts;
}
toString(bundle) {
/**
* Format this `FluentDateTime` to a string.
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
try {
const dtf = bundle._memoizeIntlObject(
Intl.DateTimeFormat, this.opts
);
const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
return dtf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
} catch (err) {
scope.reportError(err);
return (new Date(this.value)).toISOString();
}

@@ -131,8 +173,11 @@ }

if (arg instanceof FluentNone) {
return arg;
return new FluentNone(`NUMBER(${arg.valueOf()})`);
}
if (arg instanceof FluentNumber) {
return new FluentNumber(arg.valueOf(), merge(arg.opts, opts));
let value = Number(arg.valueOf());
if (Number.isNaN(value)) {
throw new TypeError("Invalid argument to NUMBER");
}
return new FluentNone("NUMBER()");
return new FluentNumber(value, merge(arg.opts, opts));
}

@@ -142,8 +187,11 @@

if (arg instanceof FluentNone) {
return arg;
return new FluentNone(`DATETIME(${arg.valueOf()})`);
}
if (arg instanceof FluentDateTime) {
return new FluentDateTime(arg.valueOf(), merge(arg.opts, opts));
let value = Number(arg.valueOf());
if (Number.isNaN(value)) {
throw new TypeError("Invalid argument to DATETIME");
}
return new FluentNone("DATETIME()");
return new FluentDateTime(value, merge(arg.opts, opts));
}

@@ -167,3 +215,3 @@

// Helper: match a variant key to the given selector.
function match(bundle, selector, key) {
function match(scope, selector, key) {
if (key === selector) {

@@ -182,4 +230,4 @@ // Both are strings.

if (selector instanceof FluentNumber && typeof key === "string") {
let category = bundle
._memoizeIntlObject(Intl.PluralRules, selector.opts)
let category = scope
.memoizeIntlObject(Intl.PluralRules, selector.opts)
.select(selector.value);

@@ -197,6 +245,6 @@ if (key === category) {

if (variants[star]) {
return Type(scope, variants[star]);
return resolvePattern(scope, variants[star].value);
}
scope.errors.push(new RangeError("No default"));
scope.reportError(new RangeError("No default"));
return new FluentNone();

@@ -212,5 +260,5 @@ }

if (arg.type === "narg") {
named[arg.name] = Type(scope, arg.value);
named[arg.name] = resolveExpression(scope, arg.value);
} else {
positional.push(Type(scope, arg));
positional.push(resolveExpression(scope, arg));
}

@@ -223,21 +271,3 @@ }

// Resolve an expression to a Fluent type.
function Type(scope, expr) {
// A fast-path for strings which are the most common case. Since they
// natively have the `toString` method they can be used as if they were
// a FluentType instance without incurring the cost of creating one.
if (typeof expr === "string") {
return scope.bundle._transform(expr);
}
// A fast-path for `FluentNone` which doesn't require any additional logic.
if (expr instanceof FluentNone) {
return expr;
}
// The Runtime AST (Entries) encodes patterns (complex strings with
// placeables) as Arrays.
if (Array.isArray(expr)) {
return Pattern(scope, expr);
}
function resolveExpression(scope, expr) {
switch (expr.type) {

@@ -260,11 +290,2 @@ case "str":

return SelectExpression(scope, expr);
case undefined: {
// If it's a node with a value, resolve the value.
if (expr.value !== null && expr.value !== undefined) {
return Type(scope, expr.value);
}
scope.errors.push(new RangeError("No value"));
return new FluentNone();
}
default:

@@ -279,3 +300,3 @@ return new FluentNone();

if (scope.insideTermReference === false) {
scope.errors.push(new ReferenceError(`Unknown variable: ${name}`));
scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
}

@@ -300,7 +321,7 @@ return new FluentNone(`$${name}`);

if (arg instanceof Date) {
return new FluentDateTime(arg);
return new FluentDateTime(arg.getTime());
}
default:
scope.errors.push(
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
scope.reportError(
new TypeError(`Variable type not supported: $${name}, ${typeof arg}`)
);

@@ -315,4 +336,3 @@ return new FluentNone(`$${name}`);

if (!message) {
const err = new ReferenceError(`Unknown message: ${name}`);
scope.errors.push(err);
scope.reportError(new ReferenceError(`Unknown message: ${name}`));
return new FluentNone(name);

@@ -322,11 +342,16 @@ }

if (attr) {
const attribute = message.attrs && message.attrs[attr];
const attribute = message.attributes[attr];
if (attribute) {
return Type(scope, attribute);
return resolvePattern(scope, attribute);
}
scope.errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
return new FluentNone(`${name}.${attr}`);
}
return Type(scope, message);
if (message.value) {
return resolvePattern(scope, message.value);
}
scope.reportError(new ReferenceError(`No value: ${name}`));
return new FluentNone(name);
}

@@ -339,21 +364,20 @@

if (!term) {
const err = new ReferenceError(`Unknown term: ${id}`);
scope.errors.push(err);
scope.reportError(new ReferenceError(`Unknown term: ${id}`));
return new FluentNone(id);
}
// Every TermReference has its own args.
const [, keyargs] = getArguments(scope, args);
const local = {...scope, args: keyargs, insideTermReference: true};
// Every TermReference has its own variables.
const [, params] = getArguments(scope, args);
const local = scope.cloneForTermReference(params);
if (attr) {
const attribute = term.attrs && term.attrs[attr];
const attribute = term.attributes[attr];
if (attribute) {
return Type(local, attribute);
return resolvePattern(local, attribute);
}
scope.errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
return new FluentNone(`${id}.${attr}`);
}
return Type(local, term);
return resolvePattern(local, term.value);
}

@@ -367,3 +391,3 @@

if (!func) {
scope.errors.push(new ReferenceError(`Unknown function: ${name}()`));
scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
return new FluentNone(`${name}()`);

@@ -373,3 +397,3 @@ }

if (typeof func !== "function") {
scope.errors.push(new TypeError(`Function ${name}() is not callable`));
scope.reportError(new TypeError(`Function ${name}() is not callable`));
return new FluentNone(`${name}()`);

@@ -380,4 +404,4 @@ }

return func(...getArguments(scope, args));
} catch (e) {
// XXX Report errors.
} catch (err) {
scope.reportError(err);
return new FluentNone(`${name}()`);

@@ -389,6 +413,5 @@ }

function SelectExpression(scope, {selector, variants, star}) {
let sel = Type(scope, selector);
let sel = resolveExpression(scope, selector);
if (sel instanceof FluentNone) {
const variant = getDefault(scope, variants, star);
return Type(scope, variant);
return getDefault(scope, variants, star);
}

@@ -398,16 +421,15 @@

for (const variant of variants) {
const key = Type(scope, variant.key);
if (match(scope.bundle, sel, key)) {
return Type(scope, variant);
const key = resolveExpression(scope, variant.key);
if (match(scope, sel, key)) {
return resolvePattern(scope, variant.value);
}
}
const variant = getDefault(scope, variants, star);
return Type(scope, variant);
return getDefault(scope, variants, star);
}
// Resolve a pattern (a complex string with placeables).
function Pattern(scope, ptn) {
function resolveComplexPattern(scope, ptn) {
if (scope.dirty.has(ptn)) {
scope.errors.push(new RangeError("Cyclic reference"));
scope.reportError(new RangeError("Cyclic reference"));
return new FluentNone();

@@ -430,3 +452,3 @@ }

const part = Type(scope, elem).toString(scope.bundle);
const part = resolveExpression(scope, elem).toString(scope);

@@ -438,13 +460,15 @@ if (useIsolating) {

if (part.length > MAX_PLACEABLE_LENGTH) {
scope.errors.push(
new RangeError(
"Too many characters in placeable " +
`(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
)
scope.dirty.delete(ptn);
// This is a fatal error which causes the resolver to instantly bail out
// on this pattern. The length check protects against excessive memory
// usage, and throwing protects against eating up the CPU when long
// placeables are deeply nested.
throw 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);
}
result.push(part);
if (useIsolating) {

@@ -459,24 +483,240 @@ result.push(PDI);

// Resolve a simple or a complex Pattern to a FluentString (which is really the
// string primitive).
function resolvePattern(scope, node) {
// Resolve a simple pattern.
if (typeof node === "string") {
return scope.bundle._transform(node);
}
return resolveComplexPattern(scope, node);
}
class Scope {
constructor(
bundle,
errors,
args,
insideTermReference = false,
dirty = new WeakSet()
) {
/** The bundle for which the given resolution is happening. */
this.bundle = bundle;
/** The list of errors collected while resolving. */
this.errors = errors;
/** A dict of developer-provided variables. */
this.args = args;
/** Term references require different variable lookup logic. */
this.insideTermReference = insideTermReference;
/** The Set of patterns already encountered during this resolution.
* Used to detect and prevent cyclic resolutions. */
this.dirty = dirty;
}
cloneForTermReference(args) {
return new Scope(this.bundle, this.errors, args, true, this.dirty);
}
reportError(error) {
if (!this.errors) {
throw error;
}
this.errors.push(error);
}
memoizeIntlObject(ctor, opts) {
let cache = this.bundle._intls.get(ctor);
if (!cache) {
cache = {};
this.bundle._intls.set(ctor, cache);
}
let id = JSON.stringify(opts);
if (!cache[id]) {
cache[id] = new ctor(this.bundle.locales, opts);
}
return cache[id];
}
}
/**
* Format a translation into a string.
*
* @param {FluentBundle} bundle
* A FluentBundle instance which will be used to resolve the
* contextual information of the message.
* @param {Object} args
* List of arguments provided by the developer which can be accessed
* from the message.
* @param {Object} message
* An object with the Message to be resolved.
* @param {Array} errors
* An error array that any encountered errors will be appended to.
* @returns {FluentType}
* Message bundles are single-language stores of translation resources. They are
* responsible for formatting message values and attributes to strings.
*/
function resolve(bundle, args, message, errors = []) {
const scope = {
bundle, args, errors, dirty: new WeakSet(),
// TermReferences are resolved in a new scope.
insideTermReference: false,
};
return Type(scope, message).toString(bundle);
class FluentBundle {
/**
* Create an instance of `FluentBundle`.
*
* The `locales` argument is used to instantiate `Intl` formatters used by
* translations. The `options` object can be used to configure the bundle.
*
* Examples:
*
* let bundle = new FluentBundle(["en-US", "en"]);
*
* let bundle = new FluentBundle(locales, {useIsolating: false});
*
* let bundle = new FluentBundle(locales, {
* useIsolating: true,
* functions: {
* NODE_ENV: () => process.env.NODE_ENV
* }
* });
*
* Available options:
*
* - `functions` - an object of additional functions available to
* translations as builtins.
*
* - `useIsolating` - boolean specifying whether to use Unicode isolation
* marks (FSI, PDI) for bidi interpolations. Default: `true`.
*
* - `transform` - a function used to transform string parts of patterns.
*
* @param {(string|Array.<string>)} locales - The locales of the bundle
* @param {Object} [options]
* @returns {FluentBundle}
*/
constructor(locales, {
functions = {},
useIsolating = true,
transform = v => v,
} = {}) {
this.locales = Array.isArray(locales) ? locales : [locales];
this._terms = new Map();
this._messages = new Map();
this._functions = functions;
this._useIsolating = useIsolating;
this._transform = transform;
this._intls = new WeakMap();
}
/**
* Check if a message is present in the bundle.
*
* @param {string} id - The identifier of the message to check.
* @returns {bool}
*/
hasMessage(id) {
return this._messages.has(id);
}
/**
* Return a raw unformatted message object from the bundle.
*
* Raw messages are `{value, attributes}` shapes containing translation units
* called `Patterns`. `Patterns` are implementation-specific; they should be
* treated as black boxes and formatted with `FluentBundle.formatPattern`.
*
* interface RawMessage {
* value: Pattern | null;
* attributes: Record<string, Pattern>;
* }
*
* @param {string} id - The identifier of the message to check.
* @returns {{value: ?Pattern, attributes: Object.<string, Pattern>}}
*/
getMessage(id) {
return this._messages.get(id);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must be an instance of `FluentResource`.
*
* let res = new FluentResource("foo = Foo");
* bundle.addResource(res);
* bundle.getMessage("foo");
* // → {value: .., attributes: {..}}
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value. Default: `false`.
*
* @param {FluentResource} res - FluentResource object.
* @param {Object} [options]
* @returns {Array.<FluentError>}
*/
addResource(res, {
allowOverrides = false,
} = {}) {
const errors = [];
for (let i = 0; i < res.body.length; i++) {
let entry = res.body[i];
if (entry.id.startsWith("-")) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from FluentBundle.
if (allowOverrides === false && this._terms.has(entry.id)) {
errors.push(`Attempt to override an existing term: "${entry.id}"`);
continue;
}
this._terms.set(entry.id, entry);
} else {
if (allowOverrides === false && this._messages.has(entry.id)) {
errors.push(`Attempt to override an existing message: "${entry.id}"`);
continue;
}
this._messages.set(entry.id, entry);
}
}
return errors;
}
/**
* Format a `Pattern` to a string.
*
* Format a raw `Pattern` into a string. `args` will be used to resolve
* references to variables passed as arguments to the translation.
*
* In case of errors `formatPattern` 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.
*
* let errors = [];
* bundle.addResource(
* new FluentResource("hello = Hello, {$name}!"));
*
* let hello = bundle.getMessage("hello");
* if (hello.value) {
* bundle.formatPattern(hello.value, {name: "Jane"}, errors);
* // Returns "Hello, Jane!" and `errors` is empty.
*
* bundle.formatPattern(hello.value, undefined, errors);
* // Returns "Hello, {$name}!" and `errors` is now:
* // [<ReferenceError: Unknown variable: name>]
* }
*
* If `errors` is omitted, the first encountered error will be thrown.
*
* @param {Pattern} pattern
* @param {?Object} args
* @param {?Array.<Error>} errors
* @returns {string}
*/
formatPattern(pattern, args, errors) {
// Resolve a simple pattern without creating a scope. No error handling is
// required; by definition simple patterns don't have placeables.
if (typeof pattern === "string") {
return this._transform(pattern);
}
// Resolve a complex pattern.
let scope = new Scope(this, errors, args);
try {
let value = resolveComplexPattern(scope, pattern);
return value.toString(scope);
} catch (err) {
if (scope.errors) {
scope.errors.push(err);
return new FluentNone().toString(scope);
}
throw err;
}
}
}

@@ -539,12 +779,13 @@

/**
* Fluent Resource is a structure storing a map of parsed localization entries.
* Fluent Resource is a structure storing parsed localization entries.
*/
class FluentResource extends Map {
/**
* Create a new FluentResource from Fluent code.
*/
static fromString(source) {
class FluentResource {
constructor(source) {
this.body = this._parse(source);
}
_parse(source) {
RE_MESSAGE_START.lastIndex = 0;
let resource = new this();
let resource = [];
let cursor = 0;

@@ -562,3 +803,3 @@

try {
resource.set(next[1], parseMessage());
resource.push(parseMessage(next[1]));
} catch (err) {

@@ -576,3 +817,4 @@ if (err instanceof FluentError) {

// The parser implementation is inlined below for performance reasons.
// The parser implementation is inlined below for performance reasons,
// as well as for convenience of accessing `source` and `cursor`.

@@ -639,18 +881,15 @@ // The parser focuses on minimizing the number of false negatives at the

function parseMessage() {
function parseMessage(id) {
let value = parsePattern();
let attrs = parseAttributes();
let attributes = parseAttributes();
if (attrs === null) {
if (value === null) {
throw new FluentError("Expected message value or attributes");
}
return value;
if (value === null && Object.keys(attributes).length === 0) {
throw new FluentError("Expected message value or attributes");
}
return {value, attrs};
return {id, value, attributes};
}
function parseAttributes() {
let attrs = {};
let attrs = Object.create(null);

@@ -666,3 +905,3 @@ while (test(RE_ATTRIBUTE_START)) {

return Object.keys(attrs).length > 0 ? attrs : null;
return attrs;
}

@@ -749,5 +988,2 @@

element = element.value.slice(0, element.value.length - commonIndent);
} else if (element.type === "str") {
// Optimize StringLiterals into their value.
element = element.value;
}

@@ -882,3 +1118,3 @@ if (element) {

? parseNumberLiteral()
: match1(RE_IDENTIFIER);
: {type: "str", value: match1(RE_IDENTIFIER)};
consumeToken(TOKEN_BRACKET_CLOSE, FluentError);

@@ -996,254 +1232,2 @@ return key;

/**
* Message bundles 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 `FluentBundle.format` to retrieve translation units from a
* bundle. Translations can contain references to other entities or variables,
* 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
* bundle's language. See the documentation of the Fluent syntax for more
* information.
*/
class FluentBundle {
/**
* Create an instance of `FluentBundle`.
*
* The `locales` argument is used to instantiate `Intl` formatters used by
* translations. The `options` object can be used to configure the bundle.
*
* Examples:
*
* const bundle = new FluentBundle(locales);
*
* const bundle = new FluentBundle(locales, { useIsolating: false });
*
* const bundle = new FluentBundle(locales, {
* useIsolating: true,
* functions: {
* NODE_ENV: () => process.env.NODE_ENV
* }
* });
*
* Available options:
*
* - `functions` - an object of additional functions available to
* translations as builtins.
*
* - `useIsolating` - boolean specifying whether to use Unicode isolation
* marks (FSI, PDI) for bidi interpolations.
* Default: true
*
* - `transform` - a function used to transform string parts of patterns.
*
* @param {string|Array<string>} locales - Locale or locales of the bundle
* @param {Object} [options]
* @returns {FluentBundle}
*/
constructor(locales, {
functions = {},
useIsolating = true,
transform = v => v,
} = {}) {
this.locales = Array.isArray(locales) ? locales : [locales];
this._terms = new Map();
this._messages = new Map();
this._functions = functions;
this._useIsolating = useIsolating;
this._transform = transform;
this._intls = new WeakMap();
}
/*
* Return an iterator over public `[id, message]` pairs.
*
* @returns {Iterator}
*/
get messages() {
return this._messages[Symbol.iterator]();
}
/*
* Check if a message is present in the bundle.
*
* @param {string} id - The identifier of the message to check.
* @returns {bool}
*/
hasMessage(id) {
return this._messages.has(id);
}
/*
* Return the internal representation of a message.
*
* The internal representation should only be used as an argument to
* `FluentBundle.format`.
*
* @param {string} id - The identifier of the message to check.
* @returns {Any}
*/
getMessage(id) {
return this._messages.get(id);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must use the Fluent syntax. It will be parsed by
* the bundle and each translation unit (message) will be available in the
* bundle by its identifier.
*
* bundle.addMessages('foo = Foo');
* bundle.getMessage('foo');
*
* // Returns a raw representation of the 'foo' message.
*
* bundle.addMessages('bar = Bar');
* bundle.addMessages('bar = Newbar', { allowOverrides: true });
* bundle.getMessage('bar');
*
* // Returns a raw representation of the 'bar' message: Newbar.
*
* Parsed entities should be formatted with the `format` method in case they
* contain logic (references, select expressions etc.).
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value.
* Default: false
*
* @param {string} source - Text resource with translations.
* @param {Object} [options]
* @returns {Array<Error>}
*/
addMessages(source, options) {
const res = FluentResource.fromString(source);
return this.addResource(res, options);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must be an instance of FluentResource,
* e.g. parsed by `FluentResource.fromString`.
*
* let res = FluentResource.fromString("foo = Foo");
* bundle.addResource(res);
* bundle.getMessage('foo');
*
* // Returns a raw representation of the 'foo' message.
*
* let res = FluentResource.fromString("bar = Bar");
* bundle.addResource(res);
* res = FluentResource.fromString("bar = Newbar");
* bundle.addResource(res, { allowOverrides: true });
* bundle.getMessage('bar');
*
* // Returns a raw representation of the 'bar' message: Newbar.
*
* Parsed entities should be formatted with the `format` method in case they
* contain logic (references, select expressions etc.).
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value.
* Default: false
*
* @param {FluentResource} res - FluentResource object.
* @param {Object} [options]
* @returns {Array<Error>}
*/
addResource(res, {
allowOverrides = false,
} = {}) {
const errors = [];
for (const [id, value] of res) {
if (id.startsWith("-")) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from FluentBundle.
if (allowOverrides === false && this._terms.has(id)) {
errors.push(`Attempt to override an existing term: "${id}"`);
continue;
}
this._terms.set(id, value);
} else {
if (allowOverrides === false && this._messages.has(id)) {
errors.push(`Attempt to override an existing message: "${id}"`);
continue;
}
this._messages.set(id, value);
}
}
return errors;
}
/**
* Format a message to a string or null.
*
* Format a raw `message` from the bundle into a string (or a null if it has
* a null value). `args` will be used to resolve references to variables
* passed as arguments to the translation.
*
* 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.
*
* const errors = [];
* bundle.addMessages('hello = Hello, { $name }!');
* const hello = bundle.getMessage('hello');
* bundle.format(hello, { name: 'Jane' }, errors);
*
* // Returns 'Hello, Jane!' and `errors` is empty.
*
* bundle.format(hello, undefined, errors);
*
* // Returns 'Hello, name!' and `errors` is now:
*
* [<ReferenceError: Unknown variable: name>]
*
* @param {Object | string} message
* @param {Object | undefined} args
* @param {Array} errors
* @returns {?string}
*/
format(message, args, errors) {
// optimize entities which are simple strings with no attributes
if (typeof message === "string") {
return this._transform(message);
}
// optimize entities with null values
if (message === null || message.value === null) {
return null;
}
// optimize simple-string entities with attributes
if (typeof message.value === "string") {
return this._transform(message.value);
}
return resolve(this, args, message, errors);
}
_memoizeIntlObject(ctor, opts) {
const cache = this._intls.get(ctor) || {};
const id = JSON.stringify(opts);
if (!cache[id]) {
cache[id] = new ctor(this.locales, opts);
this._intls.set(ctor, cache);
}
return cache[id];
}
}
/*
* @module fluent

@@ -1250,0 +1234,0 @@ * @overview

{
"name": "@fluent/bundle",
"description": "Localization library for expressive translations.",
"version": "0.13.0",
"version": "0.14.0",
"homepage": "http://projectfluent.org",

@@ -6,0 +6,0 @@ "author": "Mozilla <l10n-drivers@mozilla.org>",

@@ -23,19 +23,20 @@ # @fluent/bundle

```javascript
import { FluentBundle, ftl } from '@fluent/bundle';
import {FluentBundle, FluentResource} from "@fluent/bundle";
const bundle = new FluentBundle('en-US');
const errors = bundle.addMessages(ftl`
-brand-name = Foo 3000
welcome = Welcome, { $name }, to { -brand-name }!
let resource = new FluentResource(`
-brand-name = Foo 3000
welcome = Welcome, {$name}, to {-brand-name}!
`);
let bundle = new FluentBundle("en-US");
let errors = bundle.addResource(resource);
if (errors.length) {
// syntax errors are per-message and don't break the whole resource
// Syntax errors are per-message and don't break the whole resource
}
const welcome = bundle.getMessage('welcome');
bundle.format(welcome, { name: 'Anna' });
// → 'Welcome, Anna, to Foo 3000!'
let welcome = bundle.getMessage("welcome");
if (welcome.value) {
bundle.formatPattern(welcome.value, {name: "Anna"});
// → "Welcome, Anna, to Foo 3000!"
}
```

@@ -59,3 +60,3 @@

import 'intl-pluralrules';
import { FluentBundle } from '@fluent/bundle';
import {FluentBundle} from '@fluent/bundle';
```

@@ -67,3 +68,3 @@

```javascript
import { FluentBundle } from '@fluent/bundle/compat';
import {FluentBundle} from '@fluent/bundle/compat';
```

@@ -70,0 +71,0 @@

@@ -31,8 +31,11 @@ /**

if (arg instanceof FluentNone) {
return arg;
return new FluentNone(`NUMBER(${arg.valueOf()})`);
}
if (arg instanceof FluentNumber) {
return new FluentNumber(arg.valueOf(), merge(arg.opts, opts));
let value = Number(arg.valueOf());
if (Number.isNaN(value)) {
throw new TypeError("Invalid argument to NUMBER");
}
return new FluentNone("NUMBER()");
return new FluentNumber(value, merge(arg.opts, opts));
}

@@ -43,8 +46,11 @@

if (arg instanceof FluentNone) {
return arg;
return new FluentNone(`DATETIME(${arg.valueOf()})`);
}
if (arg instanceof FluentDateTime) {
return new FluentDateTime(arg.valueOf(), merge(arg.opts, opts));
let value = Number(arg.valueOf());
if (Number.isNaN(value)) {
throw new TypeError("Invalid argument to DATETIME");
}
return new FluentNone("DATETIME()");
return new FluentDateTime(value, merge(arg.opts, opts));
}

@@ -1,16 +0,8 @@

import resolve from "./resolver.js";
import FluentResource from "./resource.js";
import {FluentNone} from "./types.js";
import {resolveComplexPattern} from "./resolver.js";
import Scope from "./scope.js";
/**
* Message bundles 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 `FluentBundle.format` to retrieve translation units from a
* bundle. Translations can contain references to other entities or variables,
* 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
* bundle's language. See the documentation of the Fluent syntax for more
* information.
* Message bundles are single-language stores of translation resources. They are
* responsible for formatting message values and attributes to strings.
*/

@@ -22,11 +14,11 @@ export default class FluentBundle {

* The `locales` argument is used to instantiate `Intl` formatters used by
* translations. The `options` object can be used to configure the bundle.
* translations. The `options` object can be used to configure the bundle.
*
* Examples:
*
* const bundle = new FluentBundle(locales);
* let bundle = new FluentBundle(["en-US", "en"]);
*
* const bundle = new FluentBundle(locales, { useIsolating: false });
* let bundle = new FluentBundle(locales, {useIsolating: false});
*
* const bundle = new FluentBundle(locales, {
* let bundle = new FluentBundle(locales, {
* useIsolating: true,

@@ -41,11 +33,10 @@ * functions: {

* - `functions` - an object of additional functions available to
* translations as builtins.
* translations as builtins.
*
* - `useIsolating` - boolean specifying whether to use Unicode isolation
* marks (FSI, PDI) for bidi interpolations.
* Default: true
* marks (FSI, PDI) for bidi interpolations. Default: `true`.
*
* - `transform` - a function used to transform string parts of patterns.
*
* @param {string|Array<string>} locales - Locale or locales of the bundle
* @param {(string|Array.<string>)} locales - The locales of the bundle
* @param {Object} [options]

@@ -69,12 +60,3 @@ * @returns {FluentBundle}

/*
* Return an iterator over public `[id, message]` pairs.
*
* @returns {Iterator}
*/
get messages() {
return this._messages[Symbol.iterator]();
}
/*
/**
* Check if a message is present in the bundle.

@@ -89,10 +71,16 @@ *

/*
* Return the internal representation of a message.
/**
* Return a raw unformatted message object from the bundle.
*
* The internal representation should only be used as an argument to
* `FluentBundle.format`.
* Raw messages are `{value, attributes}` shapes containing translation units
* called `Patterns`. `Patterns` are implementation-specific; they should be
* treated as black boxes and formatted with `FluentBundle.formatPattern`.
*
* interface RawMessage {
* value: Pattern | null;
* attributes: Record<string, Pattern>;
* }
*
* @param {string} id - The identifier of the message to check.
* @returns {Any}
* @returns {{value: ?Pattern, attributes: Object.<string, Pattern>}}
*/

@@ -106,67 +94,17 @@ getMessage(id) {

*
* The translation resource must use the Fluent syntax. It will be parsed by
* the bundle and each translation unit (message) will be available in the
* bundle by its identifier.
* The translation resource must be an instance of `FluentResource`.
*
* bundle.addMessages('foo = Foo');
* bundle.getMessage('foo');
*
* // Returns a raw representation of the 'foo' message.
*
* bundle.addMessages('bar = Bar');
* bundle.addMessages('bar = Newbar', { allowOverrides: true });
* bundle.getMessage('bar');
*
* // Returns a raw representation of the 'bar' message: Newbar.
*
* Parsed entities should be formatted with the `format` method in case they
* contain logic (references, select expressions etc.).
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value.
* Default: false
*
* @param {string} source - Text resource with translations.
* @param {Object} [options]
* @returns {Array<Error>}
*/
addMessages(source, options) {
const res = FluentResource.fromString(source);
return this.addResource(res, options);
}
/**
* Add a translation resource to the bundle.
*
* The translation resource must be an instance of FluentResource,
* e.g. parsed by `FluentResource.fromString`.
*
* let res = FluentResource.fromString("foo = Foo");
* let res = new FluentResource("foo = Foo");
* bundle.addResource(res);
* bundle.getMessage('foo');
* bundle.getMessage("foo");
* // → {value: .., attributes: {..}}
*
* // Returns a raw representation of the 'foo' message.
*
* let res = FluentResource.fromString("bar = Bar");
* bundle.addResource(res);
* res = FluentResource.fromString("bar = Newbar");
* bundle.addResource(res, { allowOverrides: true });
* bundle.getMessage('bar');
*
* // Returns a raw representation of the 'bar' message: Newbar.
*
* Parsed entities should be formatted with the `format` method in case they
* contain logic (references, select expressions etc.).
*
* Available options:
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value.
* Default: false
* an existing message or term with a new value. Default: `false`.
*
* @param {FluentResource} res - FluentResource object.
* @param {Object} [options]
* @returns {Array<Error>}
* @returns {Array.<FluentError>}
*/

@@ -178,17 +116,18 @@ addResource(res, {

for (const [id, value] of res) {
if (id.startsWith("-")) {
for (let i = 0; i < res.body.length; i++) {
let entry = res.body[i];
if (entry.id.startsWith("-")) {
// Identifiers starting with a dash (-) define terms. Terms are private
// and cannot be retrieved from FluentBundle.
if (allowOverrides === false && this._terms.has(id)) {
errors.push(`Attempt to override an existing term: "${id}"`);
if (allowOverrides === false && this._terms.has(entry.id)) {
errors.push(`Attempt to override an existing term: "${entry.id}"`);
continue;
}
this._terms.set(id, value);
this._terms.set(entry.id, entry);
} else {
if (allowOverrides === false && this._messages.has(id)) {
errors.push(`Attempt to override an existing message: "${id}"`);
if (allowOverrides === false && this._messages.has(entry.id)) {
errors.push(`Attempt to override an existing message: "${entry.id}"`);
continue;
}
this._messages.set(id, value);
this._messages.set(entry.id, entry);
}

@@ -201,61 +140,53 @@ }

/**
* Format a message to a string or null.
* Format a `Pattern` to a string.
*
* Format a raw `message` from the bundle into a string (or a null if it has
* a null value). `args` will be used to resolve references to variables
* passed as arguments to the translation.
* Format a raw `Pattern` into a string. `args` will be used to resolve
* references to variables passed as arguments to the translation.
*
* 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.
* In case of errors `formatPattern` 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.
*
* const errors = [];
* bundle.addMessages('hello = Hello, { $name }!');
* const hello = bundle.getMessage('hello');
* bundle.format(hello, { name: 'Jane' }, errors);
* let errors = [];
* bundle.addResource(
* new FluentResource("hello = Hello, {$name}!"));
*
* // Returns 'Hello, Jane!' and `errors` is empty.
* let hello = bundle.getMessage("hello");
* if (hello.value) {
* bundle.formatPattern(hello.value, {name: "Jane"}, errors);
* // Returns "Hello, Jane!" and `errors` is empty.
*
* bundle.format(hello, undefined, errors);
* bundle.formatPattern(hello.value, undefined, errors);
* // Returns "Hello, {$name}!" and `errors` is now:
* // [<ReferenceError: Unknown variable: name>]
* }
*
* // Returns 'Hello, name!' and `errors` is now:
* If `errors` is omitted, the first encountered error will be thrown.
*
* [<ReferenceError: Unknown variable: name>]
*
* @param {Object | string} message
* @param {Object | undefined} args
* @param {Array} errors
* @returns {?string}
* @param {Pattern} pattern
* @param {?Object} args
* @param {?Array.<Error>} errors
* @returns {string}
*/
format(message, args, errors) {
// optimize entities which are simple strings with no attributes
if (typeof message === "string") {
return this._transform(message);
formatPattern(pattern, args, errors) {
// Resolve a simple pattern without creating a scope. No error handling is
// required; by definition simple patterns don't have placeables.
if (typeof pattern === "string") {
return this._transform(pattern);
}
// optimize entities with null values
if (message === null || message.value === null) {
return null;
// Resolve a complex pattern.
let scope = new Scope(this, errors, args);
try {
let value = resolveComplexPattern(scope, pattern);
return value.toString(scope);
} catch (err) {
if (scope.errors) {
scope.errors.push(err);
return new FluentNone().toString(scope);
}
throw err;
}
// optimize simple-string entities with attributes
if (typeof message.value === "string") {
return this._transform(message.value);
}
return resolve(this, args, message, errors);
}
_memoizeIntlObject(ctor, opts) {
const cache = this._intls.get(ctor) || {};
const id = JSON.stringify(opts);
if (!cache[id]) {
cache[id] = new ctor(this.locales, opts);
this._intls.set(ctor, cache);
}
return cache[id];
}
}

@@ -1,2 +0,2 @@

/*
/**
* @module fluent

@@ -3,0 +3,0 @@ * @overview

@@ -6,4 +6,5 @@ /* global Intl */

*
* The role of the Fluent resolver is to format a translation object to an
* instance of `FluentType` or an array of instances.
* The role of the Fluent resolver is to format a `Pattern` to an instance of
* `FluentType`. For performance reasons, primitive strings are considered such
* instances, too.
*

@@ -13,8 +14,7 @@ * Translations can contain references to other messages or variables,

* grammatical features, and can use Fluent builtins which make use of the
* `Intl` formatters to format numbers, dates, lists and more into the
* bundle's language. See the documentation of the Fluent syntax for more
* information.
* `Intl` formatters to format numbers and dates into the bundle's languages.
* 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
* 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`.

@@ -25,14 +25,4 @@ *

*
* All functions in this file pass around a special object called `scope`.
* This object stores a set of elements used by all resolve functions:
*
* * {FluentBundle} bundle
* bundle 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.
* Functions in this file pass around an instance of the `Scope` class, which
* stores the data required for successful resolution and error recovery.
*/

@@ -54,3 +44,3 @@

// Helper: match a variant key to the given selector.
function match(bundle, selector, key) {
function match(scope, selector, key) {
if (key === selector) {

@@ -69,4 +59,4 @@ // Both are strings.

if (selector instanceof FluentNumber && typeof key === "string") {
let category = bundle
._memoizeIntlObject(Intl.PluralRules, selector.opts)
let category = scope
.memoizeIntlObject(Intl.PluralRules, selector.opts)
.select(selector.value);

@@ -84,6 +74,6 @@ if (key === category) {

if (variants[star]) {
return Type(scope, variants[star]);
return resolvePattern(scope, variants[star].value);
}
scope.errors.push(new RangeError("No default"));
scope.reportError(new RangeError("No default"));
return new FluentNone();

@@ -99,5 +89,5 @@ }

if (arg.type === "narg") {
named[arg.name] = Type(scope, arg.value);
named[arg.name] = resolveExpression(scope, arg.value);
} else {
positional.push(Type(scope, arg));
positional.push(resolveExpression(scope, arg));
}

@@ -110,21 +100,3 @@ }

// Resolve an expression to a Fluent type.
function Type(scope, expr) {
// A fast-path for strings which are the most common case. Since they
// natively have the `toString` method they can be used as if they were
// a FluentType instance without incurring the cost of creating one.
if (typeof expr === "string") {
return scope.bundle._transform(expr);
}
// A fast-path for `FluentNone` which doesn't require any additional logic.
if (expr instanceof FluentNone) {
return expr;
}
// The Runtime AST (Entries) encodes patterns (complex strings with
// placeables) as Arrays.
if (Array.isArray(expr)) {
return Pattern(scope, expr);
}
function resolveExpression(scope, expr) {
switch (expr.type) {

@@ -147,11 +119,2 @@ case "str":

return SelectExpression(scope, expr);
case undefined: {
// If it's a node with a value, resolve the value.
if (expr.value !== null && expr.value !== undefined) {
return Type(scope, expr.value);
}
scope.errors.push(new RangeError("No value"));
return new FluentNone();
}
default:

@@ -166,3 +129,3 @@ return new FluentNone();

if (scope.insideTermReference === false) {
scope.errors.push(new ReferenceError(`Unknown variable: ${name}`));
scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
}

@@ -187,7 +150,7 @@ return new FluentNone(`$${name}`);

if (arg instanceof Date) {
return new FluentDateTime(arg);
return new FluentDateTime(arg.getTime());
}
default:
scope.errors.push(
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
scope.reportError(
new TypeError(`Variable type not supported: $${name}, ${typeof arg}`)
);

@@ -202,4 +165,3 @@ return new FluentNone(`$${name}`);

if (!message) {
const err = new ReferenceError(`Unknown message: ${name}`);
scope.errors.push(err);
scope.reportError(new ReferenceError(`Unknown message: ${name}`));
return new FluentNone(name);

@@ -209,11 +171,16 @@ }

if (attr) {
const attribute = message.attrs && message.attrs[attr];
const attribute = message.attributes[attr];
if (attribute) {
return Type(scope, attribute);
return resolvePattern(scope, attribute);
}
scope.errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
return new FluentNone(`${name}.${attr}`);
}
return Type(scope, message);
if (message.value) {
return resolvePattern(scope, message.value);
}
scope.reportError(new ReferenceError(`No value: ${name}`));
return new FluentNone(name);
}

@@ -226,21 +193,20 @@

if (!term) {
const err = new ReferenceError(`Unknown term: ${id}`);
scope.errors.push(err);
scope.reportError(new ReferenceError(`Unknown term: ${id}`));
return new FluentNone(id);
}
// Every TermReference has its own args.
const [, keyargs] = getArguments(scope, args);
const local = {...scope, args: keyargs, insideTermReference: true};
// Every TermReference has its own variables.
const [, params] = getArguments(scope, args);
const local = scope.cloneForTermReference(params);
if (attr) {
const attribute = term.attrs && term.attrs[attr];
const attribute = term.attributes[attr];
if (attribute) {
return Type(local, attribute);
return resolvePattern(local, attribute);
}
scope.errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
return new FluentNone(`${id}.${attr}`);
}
return Type(local, term);
return resolvePattern(local, term.value);
}

@@ -254,3 +220,3 @@

if (!func) {
scope.errors.push(new ReferenceError(`Unknown function: ${name}()`));
scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
return new FluentNone(`${name}()`);

@@ -260,3 +226,3 @@ }

if (typeof func !== "function") {
scope.errors.push(new TypeError(`Function ${name}() is not callable`));
scope.reportError(new TypeError(`Function ${name}() is not callable`));
return new FluentNone(`${name}()`);

@@ -267,4 +233,4 @@ }

return func(...getArguments(scope, args));
} catch (e) {
// XXX Report errors.
} catch (err) {
scope.reportError(err);
return new FluentNone(`${name}()`);

@@ -276,6 +242,5 @@ }

function SelectExpression(scope, {selector, variants, star}) {
let sel = Type(scope, selector);
let sel = resolveExpression(scope, selector);
if (sel instanceof FluentNone) {
const variant = getDefault(scope, variants, star);
return Type(scope, variant);
return getDefault(scope, variants, star);
}

@@ -285,16 +250,15 @@

for (const variant of variants) {
const key = Type(scope, variant.key);
if (match(scope.bundle, sel, key)) {
return Type(scope, variant);
const key = resolveExpression(scope, variant.key);
if (match(scope, sel, key)) {
return resolvePattern(scope, variant.value);
}
}
const variant = getDefault(scope, variants, star);
return Type(scope, variant);
return getDefault(scope, variants, star);
}
// Resolve a pattern (a complex string with placeables).
function Pattern(scope, ptn) {
export function resolveComplexPattern(scope, ptn) {
if (scope.dirty.has(ptn)) {
scope.errors.push(new RangeError("Cyclic reference"));
scope.reportError(new RangeError("Cyclic reference"));
return new FluentNone();

@@ -317,3 +281,3 @@ }

const part = Type(scope, elem).toString(scope.bundle);
const part = resolveExpression(scope, elem).toString(scope);

@@ -325,13 +289,15 @@ if (useIsolating) {

if (part.length > MAX_PLACEABLE_LENGTH) {
scope.errors.push(
new RangeError(
"Too many characters in placeable " +
`(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
)
scope.dirty.delete(ptn);
// This is a fatal error which causes the resolver to instantly bail out
// on this pattern. The length check protects against excessive memory
// usage, and throwing protects against eating up the CPU when long
// placeables are deeply nested.
throw 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);
}
result.push(part);
if (useIsolating) {

@@ -346,24 +312,11 @@ result.push(PDI);

/**
* Format a translation into a string.
*
* @param {FluentBundle} bundle
* A FluentBundle instance which will be used to resolve the
* contextual information of the message.
* @param {Object} args
* List of arguments provided by the developer which can be accessed
* from the message.
* @param {Object} message
* An object with the Message to be resolved.
* @param {Array} errors
* An error array that any encountered errors will be appended to.
* @returns {FluentType}
*/
export default function resolve(bundle, args, message, errors = []) {
const scope = {
bundle, args, errors, dirty: new WeakSet(),
// TermReferences are resolved in a new scope.
insideTermReference: false,
};
return Type(scope, message).toString(bundle);
// Resolve a simple or a complex Pattern to a FluentString (which is really the
// string primitive).
function resolvePattern(scope, node) {
// Resolve a simple pattern.
if (typeof node === "string") {
return scope.bundle._transform(node);
}
return resolveComplexPattern(scope, node);
}

@@ -56,12 +56,13 @@ import FluentError from "./error.js";

/**
* Fluent Resource is a structure storing a map of parsed localization entries.
* Fluent Resource is a structure storing parsed localization entries.
*/
export default class FluentResource extends Map {
/**
* Create a new FluentResource from Fluent code.
*/
static fromString(source) {
export default class FluentResource {
constructor(source) {
this.body = this._parse(source);
}
_parse(source) {
RE_MESSAGE_START.lastIndex = 0;
let resource = new this();
let resource = [];
let cursor = 0;

@@ -79,3 +80,3 @@

try {
resource.set(next[1], parseMessage());
resource.push(parseMessage(next[1]));
} catch (err) {

@@ -93,3 +94,4 @@ if (err instanceof FluentError) {

// The parser implementation is inlined below for performance reasons.
// The parser implementation is inlined below for performance reasons,
// as well as for convenience of accessing `source` and `cursor`.

@@ -156,18 +158,15 @@ // The parser focuses on minimizing the number of false negatives at the

function parseMessage() {
function parseMessage(id) {
let value = parsePattern();
let attrs = parseAttributes();
let attributes = parseAttributes();
if (attrs === null) {
if (value === null) {
throw new FluentError("Expected message value or attributes");
}
return value;
if (value === null && Object.keys(attributes).length === 0) {
throw new FluentError("Expected message value or attributes");
}
return {value, attrs};
return {id, value, attributes};
}
function parseAttributes() {
let attrs = {};
let attrs = Object.create(null);

@@ -183,3 +182,3 @@ while (test(RE_ATTRIBUTE_START)) {

return Object.keys(attrs).length > 0 ? attrs : null;
return attrs;
}

@@ -266,5 +265,2 @@

element = element.value.slice(0, element.value.length - commonIndent);
} else if (element.type === "str") {
// Optimize StringLiterals into their value.
element = element.value;
}

@@ -399,3 +395,3 @@ if (element) {

? parseNumberLiteral()
: match1(RE_IDENTIFIER);
: {type: "str", value: match1(RE_IDENTIFIER)};
consumeToken(TOKEN_BRACKET_CLOSE, FluentError);

@@ -402,0 +398,0 @@ return key;

@@ -12,11 +12,10 @@ /* global Intl */

/**
* Create an `FluentType` instance.
* Create a `FluentType` instance.
*
* @param {Any} value - JavaScript value to wrap.
* @param {Object} opts - Configuration.
* @param {Any} value - JavaScript value to wrap.
* @returns {FluentType}
*/
constructor(value, opts) {
constructor(value) {
/** The wrapped native value. */
this.value = value;
this.opts = opts;
}

@@ -37,9 +36,10 @@

* Formatted values are suitable for use outside of the `FluentBundle`.
* This method can use `Intl` formatters memoized by the `FluentBundle`
* instance passed as an argument.
* This method can use `Intl` formatters available through the `scope`
* argument.
*
* @param {FluentBundle} [bundle]
* @abstract
* @param {Scope} scope
* @returns {string}
*/
toString() {
toString(scope) { // eslint-disable-line no-unused-vars
throw new Error("Subclasses of FluentType must implement toString.");

@@ -49,26 +49,53 @@ }

/**
* A `FluentType` representing no correct value.
*/
export class FluentNone extends FluentType {
valueOf() {
return null;
/**
* Create an instance of `FluentNone` with an optional fallback value.
* @param {string} value - The fallback value of this `FluentNone`.
* @returns {FluentType}
*/
constructor(value = "???") {
super(value);
}
/**
* Format this `FluentNone` to the fallback string.
* @returns {string}
*/
toString() {
return `{${this.value || "???"}}`;
return `{${this.value}}`;
}
}
/**
* A `FluentType` representing a number.
*/
export class FluentNumber extends FluentType {
/**
* Create an instance of `FluentNumber` with options to the
* `Intl.NumberFormat` constructor.
* @param {number} value
* @param {Intl.NumberFormatOptions} opts
* @returns {FluentType}
*/
constructor(value, opts) {
super(parseFloat(value), opts);
super(value);
/** Options passed to Intl.NumberFormat. */
this.opts = opts;
}
toString(bundle) {
/**
* Format this `FluentNumber` to a string.
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
try {
const nf = bundle._memoizeIntlObject(
Intl.NumberFormat, this.opts
);
const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts);
return nf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
} catch (err) {
scope.reportError(err);
return this.value.toString(10);
}

@@ -78,18 +105,33 @@ }

/**
* A `FluentType` representing a date and time.
*/
export class FluentDateTime extends FluentType {
/**
* Create an instance of `FluentDateTime` with options to the
* `Intl.DateTimeFormat` constructor.
* @param {number} value - timestamp in milliseconds
* @param {Intl.DateTimeFormatOptions} opts
* @returns {FluentType}
*/
constructor(value, opts) {
super(new Date(value), opts);
super(value);
/** Options passed to Intl.DateTimeFormat. */
this.opts = opts;
}
toString(bundle) {
/**
* Format this `FluentDateTime` to a string.
* @param {Scope} scope
* @returns {string}
*/
toString(scope) {
try {
const dtf = bundle._memoizeIntlObject(
Intl.DateTimeFormat, this.opts
);
const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
return dtf.format(this.value);
} catch (e) {
// XXX Report the error.
return this.value;
} catch (err) {
scope.reportError(err);
return (new Date(this.value)).toISOString();
}
}
}