Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dependencies & API docs #607

Merged
merged 12 commits into from
Mar 8, 2023
6 changes: 3 additions & 3 deletions .github/workflows/legacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [12.x, 14.x]
node: ['14.18']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{matrix.node}}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{matrix.node}}
- run: npm install --global npm@7
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16.x]
node: [16, 18, latest]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{matrix.node}}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{matrix.node}}
- run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Hacking on `fluent.js` is easy! To quickly get started clone the repo:
$ git clone https://github.com/projectfluent/fluent.js.git
$ cd fluent.js

You'll need at least **Node.js 12** and **npm v7**.
You'll need at least **Node.js 14.18** and **npm 7**.
Older versions are not supported.

Install the dependencies used by all packages, which are managed as
Expand Down
2 changes: 1 addition & 1 deletion fluent-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"test": "mocha 'test/*_test.js'"
},
"engines": {
"node": ">=12.0.0",
"node": ">=14.0.0",
"npm": ">=7.0.0"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions fluent-bundle/src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* 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`.
*/
export type Message = {
id: string;
value: Pattern | null;
Expand Down
117 changes: 64 additions & 53 deletions fluent-bundle/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import { NUMBER, DATETIME } from "./builtins.js";
import { getMemoizerForLocale, IntlCache } from "./memoizer.js";

export type TextTransform = (text: string) => string;

type NativeValue = string | number | Date;
export type FluentVariable = FluentValue | NativeValue;
export type FluentVariable = FluentValue | string | number | Date;

/**
* Message bundles are single-language stores of translation resources. They are
Expand All @@ -18,41 +16,38 @@ export type FluentVariable = FluentValue | NativeValue;
export class FluentBundle {
public locales: Array<string>;

/** @ignore */
public _terms: Map<string, Term> = new Map();
/** @ignore */
public _messages: Map<string, Message> = new Map();
/** @ignore */
public _functions: Record<string, FluentFunction>;
/** @ignore */
public _useIsolating: boolean;
/** @ignore */
public _transform: TextTransform;
/** @ignore */
public _intls: IntlCache;

/**
* 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});
* @example
* ```js
* let bundle = new FluentBundle(["en-US", "en"]);
*
* let bundle = new FluentBundle(locales, {
* useIsolating: true,
* functions: {
* NODE_ENV: () => process.env.NODE_ENV
* }
* });
* let bundle = new FluentBundle(locales, {useIsolating: false});
*
* Available options:
* let bundle = new FluentBundle(locales, {
* useIsolating: true,
* functions: {
* NODE_ENV: () => process.env.NODE_ENV
* }
* });
* ```
*
* - `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 locales - Used to instantiate `Intl` formatters used by translations.
* @param options - Optional configuration for the bundle.
*/
constructor(
locales: string | Array<string>,
Expand All @@ -61,8 +56,15 @@ export class FluentBundle {
useIsolating = true,
transform = (v: string): string => v,
}: {
/** Additional functions available to translations as builtins. */
functions?: Record<string, FluentFunction>;
/**
* Whether to use Unicode isolation marks (FSI, PDI) for bidi interpolations.
*
* Default: `true`.
*/
useIsolating?: boolean;
/** A function used to transform string parts of patterns. */
transform?: TextTransform;
} = {}
) {
Expand Down Expand Up @@ -102,24 +104,30 @@ export class FluentBundle {
/**
* 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:
* @example
* ```js
* let res = new FluentResource("foo = Foo");
* bundle.addResource(res);
* bundle.getMessage("foo");
* // → {value: .., attributes: {..}}
* ```
*
* - `allowOverrides` - boolean specifying whether it's allowed to override
* an existing message or term with a new value. Default: `false`.
*
* @param res - FluentResource object.
* @param options
* @param res
* @param options
*/
addResource(
res: FluentResource,
{ allowOverrides = false }: { allowOverrides?: boolean } = {}
{
allowOverrides = false,
}: {
/**
* Boolean specifying whether it's allowed to override
* an existing message or term with a new value.
*
* Default: `false`.
*/
allowOverrides?: boolean;
} = {}
): Array<Error> {
const errors = [];

Expand Down Expand Up @@ -160,21 +168,24 @@ export class FluentBundle {
* 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.
*
* @example
* ```js
* 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>]
* }
* ```
*/
formatPattern(
pattern: Pattern,
Expand Down
2 changes: 2 additions & 0 deletions fluent-bundle/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
*
*/

export type { Message } from "./ast.js";
export { FluentBundle, FluentVariable, TextTransform } from "./bundle.js";
export { FluentResource } from "./resource.js";
export type { Scope } from "./scope.js";
export {
FluentValue,
FluentType,
Expand Down
36 changes: 20 additions & 16 deletions fluent-bundle/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,18 @@ import {
} from "./ast.js";
import { FluentVariable } from "./bundle.js";

// The maximum number of placeables which can be expanded in a single call to
// `formatPattern`. The limit protects against the Billion Laughs and Quadratic
// Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
/**
* The maximum number of placeables which can be expanded in a single call to
* `formatPattern`. The limit protects against the Billion Laughs and Quadratic
* Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
*/
const MAX_PLACEABLES = 100;

// Unicode bidi isolation characters.
/** Unicode bidi isolation characters. */
const FSI = "\u2068";
const PDI = "\u2069";

// Helper: match a variant key to the given selector.
/** Helper: match a variant key to the given selector. */
function match(scope: Scope, selector: FluentValue, key: FluentValue): boolean {
if (key === selector) {
// Both are strings.
Expand Down Expand Up @@ -86,7 +88,7 @@ function match(scope: Scope, selector: FluentValue, key: FluentValue): boolean {
return false;
}

// Helper: resolve the default variant from a list of variants.
/** Helper: resolve the default variant from a list of variants. */
function getDefault(
scope: Scope,
variants: Array<Variant>,
Expand All @@ -105,7 +107,7 @@ interface Arguments {
named: Record<string, FluentValue>;
}

// Helper: resolve arguments to a call expression.
/** Helper: resolve arguments to a call expression. */
function getArguments(
scope: Scope,
args: Array<Expression | NamedArgument>
Expand All @@ -124,7 +126,7 @@ function getArguments(
return { positional, named };
}

// Resolve an expression to a Fluent type.
/** Resolve an expression to a Fluent type. */
function resolveExpression(scope: Scope, expr: Expression): FluentValue {
switch (expr.type) {
case "str":
Expand All @@ -148,7 +150,7 @@ function resolveExpression(scope: Scope, expr: Expression): FluentValue {
}
}

// Resolve a reference to a variable.
/** Resolve a reference to a variable. */
function resolveVariableReference(
scope: Scope,
{ name }: VariableReference
Expand Down Expand Up @@ -197,7 +199,7 @@ function resolveVariableReference(
}
}

// Resolve a reference to another message.
/** Resolve a reference to another message. */
function resolveMessageReference(
scope: Scope,
{ name, attr }: MessageReference
Expand Down Expand Up @@ -225,7 +227,7 @@ function resolveMessageReference(
return new FluentNone(name);
}

// Resolve a call to a Term with key-value arguments.
/** Resolve a call to a Term with key-value arguments. */
function resolveTermReference(
scope: Scope,
{ name, attr, args }: TermReference
Expand Down Expand Up @@ -256,7 +258,7 @@ function resolveTermReference(
return resolved;
}

// Resolve a call to a Function with positional and key-value arguments.
/** Resolve a call to a Function with positional and key-value arguments. */
function resolveFunctionReference(
scope: Scope,
{ name, args }: FunctionReference
Expand All @@ -283,7 +285,7 @@ function resolveFunctionReference(
}
}

// Resolve a select expression to the member object.
/** Resolve a select expression to the member object. */
function resolveSelectExpression(
scope: Scope,
{ selector, variants, star }: SelectExpression
Expand All @@ -304,7 +306,7 @@ function resolveSelectExpression(
return getDefault(scope, variants, star);
}

// Resolve a pattern (a complex string with placeables).
/** Resolve a pattern (a complex string with placeables). */
export function resolveComplexPattern(
scope: Scope,
ptn: ComplexPattern
Expand Down Expand Up @@ -356,8 +358,10 @@ export function resolveComplexPattern(
return result.join("");
}

// Resolve a simple or a complex Pattern to a FluentString (which is really the
// string primitive).
/**
* Resolve a simple or a complex Pattern to a FluentString
* (which is really the string primitive).
*/
function resolvePattern(scope: Scope, value: Pattern): FluentValue {
// Resolve a simple pattern.
if (typeof value === "string") {
Expand Down
1 change: 1 addition & 0 deletions fluent-bundle/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const TOKEN_BLANK = /\s+/y;
* Fluent Resource is a structure storing parsed localization entries.
*/
export class FluentResource {
/** @ignore */
public body: Array<Message | Term>;

constructor(source: string) {
Expand Down
14 changes: 10 additions & 4 deletions fluent-bundle/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ export class Scope {
public errors: Array<Error> | null;
/** A dict of developer-provided variables. */
public args: Record<string, FluentVariable> | null;
/** The Set of patterns already encountered during this resolution.
* Used to detect and prevent cyclic resolutions. */
/**
* The Set of patterns already encountered during this resolution.
* Used to detect and prevent cyclic resolutions.
* @ignore
*/
public dirty: WeakSet<ComplexPattern> = new WeakSet();
/** A dict of parameters passed to a TermReference. */
public params: Record<string, FluentVariable> | null = null;
/** The running count of placeables resolved so far. Used to detect the
* Billion Laughs and Quadratic Blowup attacks. */
/**
* The running count of placeables resolved so far.
* Used to detect the Billion Laughs and Quadratic Blowup attacks.
* @ignore
*/
public placeables: number = 0;

constructor(
Expand Down
2 changes: 0 additions & 2 deletions fluent-bundle/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Scope } from "./scope.js";

/* global Intl */

export type FluentValue = FluentType<unknown> | string;

export type FluentFunction = (
Expand Down