Skip to content

Commit

Permalink
chore!: Update dependencies & API docs (#607)
Browse files Browse the repository at this point in the history
* chore!: Drop Node.js v12 support, add v18 & latest to CI tests
  - Require Node.js 14.18 for build & tests, to match Rollup v3 requirements
* chore: Refresh lockfile
* chore: Update dev dependencies to mocha@10 & sinon@15
* chore(react): Update dev dependencies to jest@29
* chore: Update to gh-pages@5
* chore: Update to rollup@3
* chore: Update to typedoc@0.23
* docs: Use /** comments */ where appropriate
  - Also drop a few unnecessary eslint directive comments
* docs: Update code comments to reduce typedoc warnings
* chore(react): Update dev dependencies to react@18
* chore(dom): Update dev dependencies to jsdom@21
  • Loading branch information
eemeli committed Mar 8, 2023
1 parent a6ccb2d commit 57b8a7e
Show file tree
Hide file tree
Showing 45 changed files with 4,185 additions and 4,565 deletions.
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

0 comments on commit 57b8a7e

Please sign in to comment.