diff --git a/common.mk b/common.mk index 2d12a8f22..ba72acea2 100644 --- a/common.mk +++ b/common.mk @@ -14,34 +14,28 @@ all: lint test build # Used for pre-publishing. dist: clean lint test build html -lint: +_lint: @eslint --config $(ROOT)/eslint_src.json --max-warnings 0 src/ @eslint --config $(ROOT)/eslint_test.json --max-warnings 0 test/ - @echo -e " $(OK) $@" + @echo -e " $(OK) lint" -html: +_html: ifneq (,$(wildcard ./.esdoc.json)) @esdoc - @echo -e " $(OK) $@ built" + @echo -e " $(OK) html built" endif +_clean: + @rm -f index.js compat.js + @rm -rf .nyc_output coverage + @echo -e " $(OK) clean" + deps: @npm install - @echo -e " $(OK) $@ installed" + @echo -e " $(OK) deps installed" depsclean: @rm -rf node_modules - @echo -e " $(OK) $@" - -CHANGELOG.md: - @if [ -z "$(SINCE)" ]; \ - then echo 'Specify last version with SINCE=x.y.z' && exit 1; \ - fi - @git log $(PACKAGE)@$(SINCE) HEAD --pretty=format:' - (%h) %s' $(CURDIR) \ - | cat - <(echo -e "\n\n") CHANGELOG.md \ - | sponge CHANGELOG.md - @echo -e " $(OK) $@ updated; make sure to edit it" - -.PHONY: test html CHANGELOG.md + @echo -e " $(OK) deps clean" OK := \033[32;01m✓\033[0m diff --git a/fluent-bundle/.esdoc.json b/fluent-bundle/.esdoc.json deleted file mode 100644 index 80c856364..000000000 --- a/fluent-bundle/.esdoc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "source": "./src", - "destination": "../html/bundle", - "plugins": [ - { - "name": "esdoc-standard-plugin" - }, - { - "name": "esdoc-ecmascript-proposal-plugin", - "option": { - "objectRestSpread": true, - "asyncGenerators": true - } - } - ] -} diff --git a/fluent-bundle/.gitignore b/fluent-bundle/.gitignore index 2ddc85064..1e865dd80 100644 --- a/fluent-bundle/.gitignore +++ b/fluent-bundle/.gitignore @@ -1,2 +1,3 @@ +esm/ index.js compat.js diff --git a/fluent-bundle/.npmignore b/fluent-bundle/.npmignore index f520b36f5..abad613ff 100644 --- a/fluent-bundle/.npmignore +++ b/fluent-bundle/.npmignore @@ -1,5 +1,7 @@ .nyc_output coverage -docs +esm/.compiled +src test makefile +tsconfig.json diff --git a/fluent-bundle/eslint_src.json b/fluent-bundle/eslint_src.json new file mode 100644 index 000000000..2aa340b27 --- /dev/null +++ b/fluent-bundle/eslint_src.json @@ -0,0 +1,26 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "env": { + "es6": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "../eslint_src.json" + ], + "rules": { + "prefer-const": "off", + "no-unused-vars": ["error", {"args": "none"}], + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-unused-vars": ["error", {"args": "none"}], + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/explicit-function-return-type": "error" + } +} diff --git a/fluent-bundle/makefile b/fluent-bundle/makefile index 776f953f6..956d41dda 100644 --- a/fluent-bundle/makefile +++ b/fluent-bundle/makefile @@ -3,17 +3,22 @@ GLOBAL := FluentBundle include ../common.mk -build: index.js compat.js +lint: + @eslint --config ./eslint_src.json --max-warnings 0 src/*.ts + @eslint --config $(ROOT)/eslint_test.json --max-warnings 0 test/ + @echo -e " $(OK) lint" -test: - @nyc --reporter=text --reporter=html mocha \ - --recursive --ui tdd \ - --require esm \ - --require ./test/index \ - test/**/*_test.js +compile: esm/.compiled + +esm/.compiled: $(SOURCES) + @tsc + @touch $@ + @echo -e " $(OK) esm/ compiled" + +build: index.js compat.js -index.js: $(SOURCES) - @rollup $(CURDIR)/src/index.js \ +index.js: esm/.compiled + @rollup $(CURDIR)/esm/index.js \ --config $(ROOT)/bundle_config.js \ --banner "/* $(PACKAGE)@$(VERSION) */" \ --amd.id $(PACKAGE) \ @@ -21,8 +26,8 @@ index.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -compat.js: $(SOURCES) - @rollup $(CURDIR)/src/index.js \ +compat.js: esm/.compiled + @rollup $(CURDIR)/esm/index.js \ --config $(ROOT)/compat_config.js \ --banner "/* $(PACKAGE)@$(VERSION) */" \ --amd.id $(PACKAGE) \ @@ -30,7 +35,25 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" +.PHONY: test +test: esm/.compiled + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require esm \ + test/**/*_test.js + +html: + @typedoc src \ + --out ../html/bundle \ + --mode file \ + --excludeNotExported \ + --excludePrivate \ + --logger none \ + --hideGenerator + @echo -e " $(OK) html build" + clean: + @rm -f esm/*.js esm/*.d.ts esm/.compiled @rm -f index.js compat.js @rm -rf .nyc_output coverage @echo -e " $(OK) clean" diff --git a/fluent-bundle/package.json b/fluent-bundle/package.json index 586a60744..089c84815 100644 --- a/fluent-bundle/package.json +++ b/fluent-bundle/package.json @@ -15,11 +15,10 @@ "email": "stas@mozilla.com" } ], - "directories": { - "lib": "./src" - }, + "type": "commonjs", "main": "./index.js", - "module": "./src/index.js", + "module": "./esm/index.js", + "types": "./esm/index.d.ts", "repository": { "type": "git", "url": "https://github.com/projectfluent/fluent.js.git" diff --git a/fluent-bundle/src/ast.ts b/fluent-bundle/src/ast.ts new file mode 100644 index 000000000..6208d7200 --- /dev/null +++ b/fluent-bundle/src/ast.ts @@ -0,0 +1,80 @@ +export type Message = { + id: string; + value: Pattern | null; + attributes: Record; +}; + +export type Term = { + id: string; + value: Pattern; + attributes: Record; +}; + +export type Pattern = string | ComplexPattern; + +export type ComplexPattern = Array; + +export type PatternElement = string | Expression; + +export type Expression = + | SelectExpression + | VariableReference + | TermReference + | MessageReference + | FunctionReference + | Literal; + +export type SelectExpression = { + type: "select"; + selector: Expression; + variants: Array; + star: number; +}; + +export type VariableReference = { + type: "var"; + name: string; +}; + +export type TermReference = { + type: "term"; + name: string; + attr: string | null; + args: Array; +}; + +export type MessageReference = { + type: "mesg"; + name: string; + attr: string | null; +}; + +export type FunctionReference = { + type: "func"; + name: string; + args: Array; +}; + +export type Variant = { + key: Literal; + value: Pattern; +}; + +export type NamedArgument = { + type: "narg"; + name: string; + value: Literal; +}; + +export type Literal = StringLiteral | NumberLiteral; + +export type StringLiteral = { + type: "str"; + value: string; +}; + +export type NumberLiteral = { + type: "num"; + value: number; + precision: number; +}; diff --git a/fluent-bundle/src/builtins.js b/fluent-bundle/src/builtins.js deleted file mode 100644 index 3eb051d28..000000000 --- a/fluent-bundle/src/builtins.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @overview - * - * The FTL resolver ships with a number of functions built-in. - * - * Each function take two arguments: - * - args - an array of positional args - * - opts - an object of key-value args - * - * Arguments to functions are guaranteed to already be instances of - * `FluentType`. Functions must return `FluentType` objects as well. - */ - -import { FluentNone, FluentNumber, FluentDateTime } from "./types.js"; - -function merge(argopts, opts) { - return Object.assign({}, argopts, values(opts)); -} - -function values(opts) { - const unwrapped = {}; - for (const [name, opt] of Object.entries(opts)) { - unwrapped[name] = opt.valueOf(); - } - return unwrapped; -} - -export -function NUMBER([arg], opts) { - if (arg instanceof FluentNone) { - return new FluentNone(`NUMBER(${arg.valueOf()})`); - } - - let value = Number(arg.valueOf()); - if (Number.isNaN(value)) { - throw new TypeError("Invalid argument to NUMBER"); - } - - return new FluentNumber(value, merge(arg.opts, opts)); -} - -export -function DATETIME([arg], opts) { - if (arg instanceof FluentNone) { - return new FluentNone(`DATETIME(${arg.valueOf()})`); - } - - let value = Number(arg.valueOf()); - if (Number.isNaN(value)) { - throw new TypeError("Invalid argument to DATETIME"); - } - - return new FluentDateTime(value, merge(arg.opts, opts)); -} diff --git a/fluent-bundle/src/builtins.ts b/fluent-bundle/src/builtins.ts new file mode 100644 index 000000000..a2167d027 --- /dev/null +++ b/fluent-bundle/src/builtins.ts @@ -0,0 +1,61 @@ +/** + * @overview + * + * The FTL resolver ships with a number of functions built-in. + * + * Each function take two arguments: + * - args - an array of positional args + * - opts - an object of key-value args + * + * Arguments to functions are guaranteed to already be instances of + * `FluentValue`. Functions must return `FluentValues` as well. + */ + +import { + FluentValue, + FluentNone, + FluentNumber, + FluentDateTime +} from "./types.js"; + +function values(opts: Record): Record { + const unwrapped: Record = {}; + for (const [name, opt] of Object.entries(opts)) { + unwrapped[name] = opt.valueOf(); + } + return unwrapped; +} + +export function NUMBER( + args: Array, + opts: Record +): FluentValue { + let arg = args[0]; + + if (arg instanceof FluentNone) { + return new FluentNone(`NUMBER(${arg.valueOf()})`); + } + + if (arg instanceof FluentNumber || arg instanceof FluentDateTime) { + return new FluentNumber(arg.valueOf(), { ...arg.opts, ...values(opts) }); + } + + throw new TypeError("Invalid argument to NUMBER"); +} + +export function DATETIME( + args: Array, + opts: Record +): FluentValue { + let arg = args[0]; + + if (arg instanceof FluentNone) { + return new FluentNone(`DATETIME(${arg.valueOf()})`); + } + + if (arg instanceof FluentNumber || arg instanceof FluentDateTime) { + return new FluentDateTime(arg.valueOf(), { ...arg.opts, ...values(opts) }); + } + + throw new TypeError("Invalid argument to DATETIME"); +} diff --git a/fluent-bundle/src/bundle.js b/fluent-bundle/src/bundle.ts similarity index 68% rename from fluent-bundle/src/bundle.js rename to fluent-bundle/src/bundle.ts index 682d550af..33220f096 100644 --- a/fluent-bundle/src/bundle.js +++ b/fluent-bundle/src/bundle.ts @@ -1,12 +1,29 @@ -import {FluentNone} from "./types.js"; -import {resolveComplexPattern} from "./resolver.js"; -import Scope from "./scope.js"; +import { resolveComplexPattern } from "./resolver.js"; +import { Scope } from "./scope.js"; +import { FluentResource } from "./resource.js"; +import { FluentValue, FluentNone, FluentFunction } from "./types.js"; +import { Message, Term, Pattern } from "./ast.js"; +import { NUMBER, DATETIME } from "./builtins.js"; + +export type TextTransform = (text: string) => string; + +type NativeArgument = string | number | Date; +export type FluentArgument = FluentValue | NativeArgument; /** * Message bundles are single-language stores of translation resources. They are * responsible for formatting message values and attributes to strings. */ -export default class FluentBundle { +export class FluentBundle { + public locales: Array; + + public _terms: Map = new Map(); + public _messages: Map = new Map(); + public _functions: Record; + public _useIsolating: boolean; + public _transform: TextTransform; + public _intls: WeakMap> = new WeakMap(); + /** * Create an instance of `FluentBundle`. * @@ -35,33 +52,35 @@ export default class FluentBundle { * marks (FSI, PDI) for bidi interpolations. Default: `true`. * * - `transform` - a function used to transform string parts of patterns. - * - * @param {(string|Array.)} locales - The locales of the bundle - * @param {Object} [options] - * @returns {FluentBundle} */ - constructor(locales, { - functions = {}, - useIsolating = true, - transform = v => v, - } = {}) { + constructor( + locales: string | Array, + { + functions, + useIsolating = true, + transform = (v: string): string => v + }: { + functions?: Record; + useIsolating?: boolean; + transform?: TextTransform; + } = {} + ) { this.locales = Array.isArray(locales) ? locales : [locales]; - - this._terms = new Map(); - this._messages = new Map(); - this._functions = functions; + this._functions = { + NUMBER, + DATETIME, + ...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} + * @param id - The identifier of the message to check. */ - hasMessage(id) { + hasMessage(id: string): boolean { return this._messages.has(id); } @@ -72,15 +91,9 @@ export default class FluentBundle { * 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; - * } - * - * @param {string} id - The identifier of the message to check. - * @returns {{value: ?Pattern, attributes: Object.}} + * @param id - The identifier of the message to check. */ - getMessage(id) { + getMessage(id: string): Message | undefined { return this._messages.get(id); } @@ -99,13 +112,13 @@ export default class FluentBundle { * - `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.} + * @param res - FluentResource object. + * @param options */ - addResource(res, { - allowOverrides = false, - } = {}) { + addResource( + res: FluentResource, + { allowOverrides = false }: { allowOverrides?: boolean } = {} + ): Array { const errors = []; for (let i = 0; i < res.body.length; i++) { @@ -114,13 +127,17 @@ export default class FluentBundle { // 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}"`); + errors.push( + new Error(`Attempt to override an existing term: "${entry.id}"`) + ); continue; } - this._terms.set(entry.id, entry); + this._terms.set(entry.id, entry as Term); } else { if (allowOverrides === false && this._messages.has(entry.id)) { - errors.push(`Attempt to override an existing message: "${entry.id}"`); + errors.push( + new Error(`Attempt to override an existing message: "${entry.id}"`) + ); continue; } this._messages.set(entry.id, entry); @@ -156,13 +173,12 @@ export default class FluentBundle { * } * * If `errors` is omitted, the first encountered error will be thrown. - * - * @param {Pattern} pattern - * @param {?Object} args - * @param {?Array.} errors - * @returns {string} */ - formatPattern(pattern, args, errors) { + formatPattern( + pattern: Pattern, + args: Record | null = null, + errors: Array | null = null + ): string { // 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") { diff --git a/fluent-bundle/src/error.js b/fluent-bundle/src/error.js deleted file mode 100644 index 63ee0f04e..000000000 --- a/fluent-bundle/src/error.js +++ /dev/null @@ -1 +0,0 @@ -export default class FluentError extends Error {} diff --git a/fluent-bundle/src/index.js b/fluent-bundle/src/index.js deleted file mode 100644 index 894be0ad2..000000000 --- a/fluent-bundle/src/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @module fluent - * @overview - * - * `fluent` is a JavaScript implementation of Project Fluent, a localization - * framework designed to unleash the expressive power of the natural language. - * - */ - -export { default as FluentBundle } from "./bundle.js"; -export { default as FluentResource } from "./resource.js"; -export { default as FluentError } from "./error.js"; -export { FluentType, FluentNumber, FluentDateTime } from "./types.js"; diff --git a/fluent-bundle/src/index.ts b/fluent-bundle/src/index.ts new file mode 100644 index 000000000..856655337 --- /dev/null +++ b/fluent-bundle/src/index.ts @@ -0,0 +1,22 @@ +/** + * @module fluent + * @overview + * + * `fluent` is a JavaScript implementation of Project Fluent, a localization + * framework designed to unleash the expressive power of the natural language. + * + */ + +export { + FluentBundle, + FluentArgument, + TextTransform +} from "./bundle.js"; +export { FluentResource } from "./resource.js"; +export { + FluentValue, + FluentType, + FluentFunction, + FluentNumber, + FluentDateTime +} from "./types.js"; diff --git a/fluent-bundle/src/resolver.js b/fluent-bundle/src/resolver.ts similarity index 75% rename from fluent-bundle/src/resolver.js rename to fluent-bundle/src/resolver.ts index 6c9acc84c..c51e21dd9 100644 --- a/fluent-bundle/src/resolver.js +++ b/fluent-bundle/src/resolver.ts @@ -4,8 +4,8 @@ * @overview * * 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. + * `FluentValue`. For performance reasons, primitive strings are considered + * such instances, too. * * Translations can contain references to other messages or variables, * conditional logic in form of select expressions, traits which describe their @@ -17,17 +17,34 @@ * 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`. * - * All expressions resolve to an instance of `FluentType`. The caller should + * All expressions resolve to an instance of `FluentValue`. The caller should * use the `toString` method to convert the instance to a native value. * * Functions in this file pass around an instance of the `Scope` class, which * stores the data required for successful resolution and error recovery. */ - -import { FluentType, FluentNone, FluentNumber, FluentDateTime } - from "./types.js"; -import * as builtins from "./builtins.js"; +import { + FluentValue, + FluentType, + FluentNone, + FluentNumber, + FluentDateTime +} from "./types.js"; +import { Scope } from "./scope.js"; +import { + Variant, + Expression, + NamedArgument, + VariableReference, + MessageReference, + TermReference, + FunctionReference, + SelectExpression, + ComplexPattern, + Pattern +} from "./ast.js"; +import { FluentArgument } 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 @@ -38,24 +55,28 @@ const MAX_PLACEABLES = 100; const FSI = "\u2068"; const PDI = "\u2069"; - // Helper: match a variant key to the given selector. -function match(scope, selector, key) { +function match(scope: Scope, selector: FluentValue, key: FluentValue): boolean { if (key === selector) { // Both are strings. return true; } // XXX Consider comparing options too, e.g. minimumFractionDigits. - if (key instanceof FluentNumber - && selector instanceof FluentNumber - && key.value === selector.value) { + if ( + key instanceof FluentNumber && + selector instanceof FluentNumber && + key.value === selector.value + ) { return true; } if (selector instanceof FluentNumber && typeof key === "string") { let category = scope - .memoizeIntlObject(Intl.PluralRules, selector.opts) + .memoizeIntlObject( + Intl.PluralRules, + selector.opts as Intl.PluralRulesOptions + ) .select(selector.value); if (key === category) { return true; @@ -66,7 +87,11 @@ function match(scope, selector, key) { } // Helper: resolve the default variant from a list of variants. -function getDefault(scope, variants, star) { +function getDefault( + scope: Scope, + variants: Array, + star: number +): FluentValue { if (variants[star]) { return resolvePattern(scope, variants[star].value); } @@ -75,10 +100,18 @@ function getDefault(scope, variants, star) { return new FluentNone(); } +interface Arguments { + positional: Array; + named: Record; +} + // Helper: resolve arguments to a call expression. -function getArguments(scope, args) { - const positional = []; - const named = Object.create(null); +function getArguments( + scope: Scope, + args: Array +): Arguments { + const positional: Array = []; + const named: Record = Object.create(null); for (const arg of args) { if (arg.type === "narg") { @@ -88,36 +121,39 @@ function getArguments(scope, args) { } } - return {positional, named}; + return { positional, named }; } // Resolve an expression to a Fluent type. -function resolveExpression(scope, expr) { +function resolveExpression(scope: Scope, expr: Expression): FluentValue { switch (expr.type) { case "str": return expr.value; case "num": return new FluentNumber(expr.value, { - minimumFractionDigits: expr.precision, + minimumFractionDigits: expr.precision }); case "var": - return VariableReference(scope, expr); + return resolveVariableReference(scope, expr); case "mesg": - return MessageReference(scope, expr); + return resolveMessageReference(scope, expr); case "term": - return TermReference(scope, expr); + return resolveTermReference(scope, expr); case "func": - return FunctionReference(scope, expr); + return resolveFunctionReference(scope, expr); case "select": - return SelectExpression(scope, expr); + return resolveSelectExpression(scope, expr); default: return new FluentNone(); } } // Resolve a reference to a variable. -function VariableReference(scope, {name}) { - let arg; +function resolveVariableReference( + scope: Scope, + { name }: VariableReference +): FluentValue { + let arg: FluentArgument; if (scope.params) { // We're inside a TermReference. It's OK to reference undefined parameters. if (Object.prototype.hasOwnProperty.call(scope.params, name)) { @@ -152,6 +188,7 @@ function VariableReference(scope, {name}) { if (arg instanceof Date) { return new FluentDateTime(arg.getTime()); } + // eslint-disable-next-line no-fallthrough default: scope.reportError( new TypeError(`Variable type not supported: $${name}, ${typeof arg}`) @@ -161,7 +198,10 @@ function VariableReference(scope, {name}) { } // Resolve a reference to another message. -function MessageReference(scope, {name, attr}) { +function resolveMessageReference( + scope: Scope, + { name, attr }: MessageReference +): FluentValue { const message = scope.bundle._messages.get(name); if (!message) { scope.reportError(new ReferenceError(`Unknown message: ${name}`)); @@ -186,7 +226,10 @@ function MessageReference(scope, {name, attr}) { } // Resolve a call to a Term with key-value arguments. -function TermReference(scope, {name, attr, args}) { +function resolveTermReference( + scope: Scope, + { name, attr, args }: TermReference +): FluentValue { const id = `-${name}`; const term = scope.bundle._terms.get(id); if (!term) { @@ -214,10 +257,13 @@ function TermReference(scope, {name, attr, args}) { } // Resolve a call to a Function with positional and key-value arguments. -function FunctionReference(scope, {name, args}) { +function resolveFunctionReference( + scope: Scope, + { name, args }: FunctionReference +): FluentValue { // Some functions are built-in. Others may be provided by the runtime via // the `FluentBundle` constructor. - const func = scope.bundle._functions[name] || builtins[name]; + let func = scope.bundle._functions[name]; if (!func) { scope.reportError(new ReferenceError(`Unknown function: ${name}()`)); return new FluentNone(`${name}()`); @@ -238,7 +284,10 @@ function FunctionReference(scope, {name, args}) { } // Resolve a select expression to the member object. -function SelectExpression(scope, {selector, variants, star}) { +function resolveSelectExpression( + scope: Scope, + { selector, variants, star }: SelectExpression +): FluentValue { let sel = resolveExpression(scope, selector); if (sel instanceof FluentNone) { return getDefault(scope, variants, star); @@ -256,7 +305,10 @@ function SelectExpression(scope, {selector, variants, star}) { } // Resolve a pattern (a complex string with placeables). -export function resolveComplexPattern(scope, ptn) { +export function resolveComplexPattern( + scope: Scope, + ptn: ComplexPattern +): FluentValue { if (scope.dirty.has(ptn)) { scope.reportError(new RangeError("Cyclic reference")); return new FluentNone(); @@ -306,11 +358,11 @@ export function resolveComplexPattern(scope, ptn) { // Resolve a simple or a complex Pattern to a FluentString (which is really the // string primitive). -function resolvePattern(scope, node) { +function resolvePattern(scope: Scope, value: Pattern): FluentValue { // Resolve a simple pattern. - if (typeof node === "string") { - return scope.bundle._transform(node); + if (typeof value === "string") { + return scope.bundle._transform(value); } - return resolveComplexPattern(scope, node); + return resolveComplexPattern(scope, value); } diff --git a/fluent-bundle/src/resource.js b/fluent-bundle/src/resource.ts similarity index 70% rename from fluent-bundle/src/resource.js rename to fluent-bundle/src/resource.ts index ae37ee024..aac7f34f4 100644 --- a/fluent-bundle/src/resource.js +++ b/fluent-bundle/src/resource.ts @@ -1,8 +1,25 @@ -import FluentError from "./error.js"; +import { + Message, + PatternElement, + Literal, + SelectExpression, + Variant, + NamedArgument, + Expression, + Pattern, + VariableReference, + TermReference, + FunctionReference, + MessageReference, + Term, + ComplexPattern, + NumberLiteral, + StringLiteral +} from "./ast.js"; // This regex is used to iterate through the beginnings of messages and terms. // With the /m flag, the ^ matches at the beginning of every line. -const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */mg; +const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */gm; // Both Attributes and Variants are parsed in while loops. These regexes are // used to break out of them. @@ -51,15 +68,13 @@ const TOKEN_BLANK = /\s+/y; /** * Fluent Resource is a structure storing parsed localization entries. */ -export default class FluentResource { - constructor(source) { - this.body = this._parse(source); - } +export class FluentResource { + public body: Array; - _parse(source) { - RE_MESSAGE_START.lastIndex = 0; + constructor(source: string) { + this.body = []; - let resource = []; + RE_MESSAGE_START.lastIndex = 0; let cursor = 0; // Iterate over the beginnings of messages and terms to efficiently skip @@ -72,9 +87,9 @@ export default class FluentResource { cursor = RE_MESSAGE_START.lastIndex; try { - resource.push(parseMessage(next[1])); + this.body.push(parseMessage(next[1])); } catch (err) { - if (err instanceof FluentError) { + if (err instanceof SyntaxError) { // Don't report any Fluent syntax errors. Skip directly to the // beginning of the next message or term. continue; @@ -83,8 +98,6 @@ export default class FluentResource { } } - return resource; - // The parser implementation is inlined below for performance reasons, // as well as for convenience of accessing `source` and `cursor`. @@ -101,14 +114,17 @@ export default class FluentResource { // to any offset of the source string without slicing it. Errors are thrown // to bail out of parsing of ill-formed messages. - function test(re) { + function test(re: RegExp): boolean { re.lastIndex = cursor; return re.test(source); } // Advance the cursor by the char if it matches. May be used as a predicate // (was the match found?) or, if errorClass is passed, as an assertion. - function consumeChar(char, errorClass) { + function consumeChar( + char: string, + errorClass?: typeof SyntaxError + ): boolean { if (source[cursor] === char) { cursor++; return true; @@ -121,7 +137,10 @@ export default class FluentResource { // Advance the cursor by the token if it matches. May be used as a predicate // (was the match found?) or, if errorClass is passed, as an assertion. - function consumeToken(re, errorClass) { + function consumeToken( + re: RegExp, + errorClass?: typeof SyntaxError + ): boolean { if (test(re)) { cursor = re.lastIndex; return true; @@ -133,40 +152,40 @@ export default class FluentResource { } // Execute a regex, advance the cursor, and return all capture groups. - function match(re) { + function match(re: RegExp): RegExpExecArray { re.lastIndex = cursor; let result = re.exec(source); if (result === null) { - throw new FluentError(`Expected ${re.toString()}`); + throw new SyntaxError(`Expected ${re.toString()}`); } cursor = re.lastIndex; return result; } // Execute a regex, advance the cursor, and return the capture group. - function match1(re) { + function match1(re: RegExp): string { return match(re)[1]; } - function parseMessage(id) { + function parseMessage(id: string): Message { let value = parsePattern(); let attributes = parseAttributes(); if (value === null && Object.keys(attributes).length === 0) { - throw new FluentError("Expected message value or attributes"); + throw new SyntaxError("Expected message value or attributes"); } - return {id, value, attributes}; + return { id, value, attributes }; } - function parseAttributes() { - let attrs = Object.create(null); + function parseAttributes(): Record { + let attrs: Record = Object.create(null); while (test(RE_ATTRIBUTE_START)) { let name = match1(RE_ATTRIBUTE_START); let value = parsePattern(); if (value === null) { - throw new FluentError("Expected attribute value"); + throw new SyntaxError("Expected attribute value"); } attrs[name] = value; } @@ -174,10 +193,11 @@ export default class FluentResource { return attrs; } - function parsePattern() { + function parsePattern(): Pattern | null { + let first; // First try to parse any simple text on the same line as the id. if (test(RE_TEXT_RUN)) { - var first = match1(RE_TEXT_RUN); + first = match1(RE_TEXT_RUN); } // If there's a placeable on the first line, parse a complex pattern. @@ -211,7 +231,10 @@ export default class FluentResource { } // Parse a complex pattern as an array of elements. - function parsePatternElements(elements = [], commonIndent) { + function parsePatternElements( + elements: Array = [], + commonIndent: number + ): ComplexPattern { while (true) { if (test(RE_TEXT_RUN)) { elements.push(match1(RE_TEXT_RUN)); @@ -224,7 +247,7 @@ export default class FluentResource { } if (source[cursor] === "}") { - throw new FluentError("Unbalanced closing brace"); + throw new SyntaxError("Unbalanced closing brace"); } let indent = parseIndent(); @@ -238,14 +261,15 @@ export default class FluentResource { } let lastIndex = elements.length - 1; + let lastElement = elements[lastIndex]; // Trim the trailing spaces in the last element if it's a TextElement. - if (typeof elements[lastIndex] === "string") { - elements[lastIndex] = trim(elements[lastIndex], RE_TRAILING_SPACES); + if (typeof lastElement === "string") { + elements[lastIndex] = trim(lastElement, RE_TRAILING_SPACES); } - let baked = []; + let baked: Array = []; for (let element of elements) { - if (element.type === "indent") { + if (element instanceof Indent) { // Dedent indented lines by the maximum common indent. element = element.value.slice(0, element.value.length - commonIndent); } @@ -256,8 +280,8 @@ export default class FluentResource { return baked; } - function parsePlaceable() { - consumeToken(TOKEN_BRACE_OPEN, FluentError); + function parsePlaceable(): Expression { + consumeToken(TOKEN_BRACE_OPEN, SyntaxError); let selector = parseInlineExpression(); if (consumeToken(TOKEN_BRACE_CLOSE)) { @@ -266,14 +290,18 @@ export default class FluentResource { if (consumeToken(TOKEN_ARROW)) { let variants = parseVariants(); - consumeToken(TOKEN_BRACE_CLOSE, FluentError); - return {type: "select", selector, ...variants}; + consumeToken(TOKEN_BRACE_CLOSE, SyntaxError); + return { + type: "select", + selector, + ...variants + } as SelectExpression; } - throw new FluentError("Unclosed placeable"); + throw new SyntaxError("Unclosed placeable"); } - function parseInlineExpression() { + function parseInlineExpression(): Expression { if (source[cursor] === "{") { // It's a nested placeable. return parsePlaceable(); @@ -283,7 +311,7 @@ export default class FluentResource { let [, sigil, name, attr = null] = match(RE_REFERENCE); if (sigil === "$") { - return {type: "var", name}; + return { type: "var", name } as VariableReference; } if (consumeToken(TOKEN_PAREN_OPEN)) { @@ -291,36 +319,41 @@ export default class FluentResource { if (sigil === "-") { // A parameterized term: -term(...). - return {type: "term", name, attr, args}; + return { type: "term", name, attr, args } as TermReference; } if (RE_FUNCTION_NAME.test(name)) { - return {type: "func", name, args}; + return { type: "func", name, args } as FunctionReference; } - throw new FluentError("Function names must be all upper-case"); + throw new SyntaxError("Function names must be all upper-case"); } if (sigil === "-") { // A non-parameterized term: -term. - return {type: "term", name, attr, args: []}; + return { + type: "term", + name, + attr, + args: [] + } as TermReference; } - return {type: "mesg", name, attr}; + return { type: "mesg", name, attr } as MessageReference; } return parseLiteral(); } - function parseArguments() { - let args = []; + function parseArguments(): Array { + let args: Array = []; while (true) { switch (source[cursor]) { case ")": // End of the argument list. cursor++; return args; case undefined: // EOF - throw new FluentError("Unclosed argument list"); + throw new SyntaxError("Unclosed argument list"); } args.push(parseArgument()); @@ -329,7 +362,7 @@ export default class FluentResource { } } - function parseArgument() { + function parseArgument(): Expression | NamedArgument { let expr = parseInlineExpression(); if (expr.type !== "mesg") { return expr; @@ -337,15 +370,22 @@ export default class FluentResource { if (consumeToken(TOKEN_COLON)) { // The reference is the beginning of a named argument. - return {type: "narg", name: expr.name, value: parseLiteral()}; + return { + type: "narg", + name: expr.name, + value: parseLiteral() + } as NamedArgument; } // It's a regular message reference. return expr; } - function parseVariants() { - let variants = []; + function parseVariants(): { + variants: Array; + star: number; + } | null { + let variants: Array = []; let count = 0; let star; @@ -357,9 +397,9 @@ export default class FluentResource { let key = parseVariantKey(); let value = parsePattern(); if (value === null) { - throw new FluentError("Expected variant value"); + throw new SyntaxError("Expected variant value"); } - variants[count++] = {key, value}; + variants[count++] = { key, value }; } if (count === 0) { @@ -367,41 +407,51 @@ export default class FluentResource { } if (star === undefined) { - throw new FluentError("Expected default variant"); + throw new SyntaxError("Expected default variant"); } - return {variants, star}; + return { variants, star }; } - function parseVariantKey() { - consumeToken(TOKEN_BRACKET_OPEN, FluentError); - let key = test(RE_NUMBER_LITERAL) - ? parseNumberLiteral() - : {type: "str", value: match1(RE_IDENTIFIER)}; - consumeToken(TOKEN_BRACKET_CLOSE, FluentError); + function parseVariantKey(): Literal { + consumeToken(TOKEN_BRACKET_OPEN, SyntaxError); + let key; + if (test(RE_NUMBER_LITERAL)) { + key = parseNumberLiteral(); + } else { + key = { + type: "str", + value: match1(RE_IDENTIFIER) + } as StringLiteral; + } + consumeToken(TOKEN_BRACKET_CLOSE, SyntaxError); return key; } - function parseLiteral() { + function parseLiteral(): Literal { if (test(RE_NUMBER_LITERAL)) { return parseNumberLiteral(); } - if (source[cursor] === "\"") { + if (source[cursor] === '"') { return parseStringLiteral(); } - throw new FluentError("Invalid expression"); + throw new SyntaxError("Invalid expression"); } - function parseNumberLiteral() { + function parseNumberLiteral(): NumberLiteral { let [, value, fraction = ""] = match(RE_NUMBER_LITERAL); let precision = fraction.length; - return {type: "num", value: parseFloat(value), precision}; + return { + type: "num", + value: parseFloat(value), + precision + } as NumberLiteral; } - function parseStringLiteral() { - consumeChar("\"", FluentError); + function parseStringLiteral(): StringLiteral { + consumeChar('"', SyntaxError); let value = ""; while (true) { value += match1(RE_STRING_RUN); @@ -411,17 +461,17 @@ export default class FluentResource { continue; } - if (consumeChar("\"")) { - return {type: "str", value}; + if (consumeChar('"')) { + return { type: "str", value } as StringLiteral; } // We've reached an EOL of EOF. - throw new FluentError("Unclosed string literal"); + throw new SyntaxError("Unclosed string literal"); } } // Unescape known escape sequences. - function parseEscapeSequence() { + function parseEscapeSequence(): string { if (test(RE_STRING_ESCAPE)) { return match1(RE_STRING_ESCAPE); } @@ -429,7 +479,7 @@ export default class FluentResource { if (test(RE_UNICODE_ESCAPE)) { let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE); let codepoint = parseInt(codepoint4 || codepoint6, 16); - return codepoint <= 0xD7FF || 0xE000 <= codepoint + return codepoint <= 0xd7ff || 0xe000 <= codepoint // It's a Unicode scalar value. ? String.fromCodePoint(codepoint) // Lonely surrogates can cause trouble when the parsing result is @@ -437,12 +487,12 @@ export default class FluentResource { : "�"; } - throw new FluentError("Unknown escape sequence"); + throw new SyntaxError("Unknown escape sequence"); } // Parse blank space. Return it if it looks like indent before a pattern // line. Skip it othwerwise. - function parseIndent() { + function parseIndent(): Indent | false { let start = cursor; consumeToken(TOKEN_BLANK); @@ -476,15 +526,20 @@ export default class FluentResource { } // Trim blanks in text according to the given regex. - function trim(text, re) { + function trim(text: string, re: RegExp): string { return text.replace(re, ""); } // Normalize a blank block and extract the indent details. - function makeIndent(blank) { + function makeIndent(blank: string): Indent { let value = blank.replace(RE_BLANK_LINES, "\n"); - let length = RE_INDENT.exec(blank)[1].length; - return {type: "indent", value, length}; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + let length = RE_INDENT.exec(blank)![1].length; + return new Indent(value, length); } } } + +class Indent { + constructor(public value: string, public length: number) { } +} diff --git a/fluent-bundle/src/scope.js b/fluent-bundle/src/scope.js deleted file mode 100644 index f43fbb3fe..000000000 --- a/fluent-bundle/src/scope.js +++ /dev/null @@ -1,39 +0,0 @@ -export default class Scope { - constructor(bundle, errors, args) { - /** 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; - - /** The Set of patterns already encountered during this resolution. - * Used to detect and prevent cyclic resolutions. */ - this.dirty = new WeakSet(); - /** A dict of parameters passed to a TermReference. */ - this.params = null; - /** The running count of placeables resolved so far. Used to detect the - * Billion Laughs and Quadratic Blowup attacks. */ - this.placeables = 0; - } - - 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]; - } -} diff --git a/fluent-bundle/src/scope.ts b/fluent-bundle/src/scope.ts new file mode 100644 index 000000000..ea9099ae6 --- /dev/null +++ b/fluent-bundle/src/scope.ts @@ -0,0 +1,52 @@ +import { FluentBundle, FluentArgument } from "./bundle.js"; +import { ComplexPattern } from "./ast.js"; + +export class Scope { + /** The bundle for which the given resolution is happening. */ + public bundle: FluentBundle; + /** The list of errors collected while resolving. */ + public errors: Array | null; + /** A dict of developer-provided variables. */ + public args: Record | null; + /** The Set of patterns already encountered during this resolution. + * Used to detect and prevent cyclic resolutions. */ + public dirty: WeakSet = new WeakSet(); + /** A dict of parameters passed to a TermReference. */ + public params: Record | null = null; + /** The running count of placeables resolved so far. Used to detect the + * Billion Laughs and Quadratic Blowup attacks. */ + public placeables: number = 0; + + constructor( + bundle: FluentBundle, + errors: Array | null, + args: Record | null, + ) { + this.bundle = bundle; + this.errors = errors; + this.args = args; + } + + reportError(error: Error): void { + if (!this.errors) { + throw error; + } + this.errors.push(error); + } + + memoizeIntlObject( + ctor: new (locales: Array, opts: OptionsT) => ObjectT, + opts: OptionsT + ): ObjectT { + 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] as ObjectT; + } +} diff --git a/fluent-bundle/src/types.js b/fluent-bundle/src/types.ts similarity index 62% rename from fluent-bundle/src/types.js rename to fluent-bundle/src/types.ts index 75acb09fe..f4aa9e32e 100644 --- a/fluent-bundle/src/types.js +++ b/fluent-bundle/src/types.ts @@ -1,5 +1,14 @@ +import { Scope } from "./scope.js"; + /* global Intl */ +export type FluentValue = FluentType | string; + +export type FluentFunction = ( + positional: Array, + named: Record +) => FluentValue; + /** * The `FluentType` class is the base of Fluent's type system. * @@ -7,24 +16,23 @@ * them, which can then be used in the `toString` method together with a proper * `Intl` formatter. */ -export class FluentType { +export class FluentType { + /** The wrapped native value. */ + public value: T; + /** * Create a `FluentType` instance. * - * @param {Any} value - JavaScript value to wrap. - * @returns {FluentType} + * @param value - JavaScript value to wrap. */ - constructor(value) { - /** The wrapped native value. */ + constructor(value: T) { this.value = value; } /** * Unwrap the raw value stored by this `FluentType`. - * - * @returns {Any} */ - valueOf() { + valueOf(): T { return this.value; } @@ -36,10 +44,8 @@ export class FluentType { * argument. * * @abstract - * @param {Scope} scope - * @returns {string} */ - toString(scope) { // eslint-disable-line no-unused-vars + toString(scope: Scope): string { throw new Error("Subclasses of FluentType must implement toString."); } } @@ -47,11 +53,10 @@ export class FluentType { /** * A `FluentType` representing no correct value. */ -export class FluentNone extends FluentType { +export 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} + * @param value - The fallback value of this `FluentNone`. */ constructor(value = "???") { super(value); @@ -59,9 +64,8 @@ export class FluentNone extends FluentType { /** * Format this `FluentNone` to the fallback string. - * @returns {string} */ - toString() { + toString(scope: Scope): string { return `{${this.value}}`; } } @@ -69,26 +73,23 @@ export class FluentNone extends FluentType { /** * A `FluentType` representing a number. */ -export class FluentNumber extends FluentType { +export class FluentNumber extends FluentType { + /** Options passed to Intl.NumberFormat. */ + public opts: Intl.NumberFormatOptions; + /** * Create an instance of `FluentNumber` with options to the * `Intl.NumberFormat` constructor. - * @param {number} value - * @param {Intl.NumberFormatOptions} opts - * @returns {FluentType} */ - constructor(value, opts) { + constructor(value: number, opts: Intl.NumberFormatOptions = {}) { super(value); - /** Options passed to Intl.NumberFormat. */ this.opts = opts; } /** * Format this `FluentNumber` to a string. - * @param {Scope} scope - * @returns {string} */ - toString(scope) { + toString(scope: Scope): string { try { const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts); return nf.format(this.value); @@ -102,32 +103,31 @@ export class FluentNumber extends FluentType { /** * A `FluentType` representing a date and time. */ -export class FluentDateTime extends FluentType { +export class FluentDateTime extends FluentType { + /** Options passed to Intl.DateTimeFormat. */ + public opts: Intl.DateTimeFormatOptions; + /** * Create an instance of `FluentDateTime` with options to the * `Intl.DateTimeFormat` constructor. - * @param {number} value - timestamp in milliseconds - * @param {Intl.DateTimeFormatOptions} opts - * @returns {FluentType} + * @param value - timestamp in milliseconds + * @param opts */ - constructor(value, opts) { + constructor(value: number, opts: Intl.DateTimeFormatOptions = {}) { super(value); - /** Options passed to Intl.DateTimeFormat. */ this.opts = opts; } /** * Format this `FluentDateTime` to a string. - * @param {Scope} scope - * @returns {string} */ - toString(scope) { + toString(scope: Scope): string { 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(); + return new Date(this.value).toISOString(); } } } diff --git a/fluent-bundle/test/arguments_test.js b/fluent-bundle/test/arguments_test.js index b8af15e20..b99ba5bd4 100644 --- a/fluent-bundle/test/arguments_test.js +++ b/fluent-bundle/test/arguments_test.js @@ -3,9 +3,9 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; -import { FluentType } from '../src/types'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; +import {FluentType} from '../esm/types'; suite('Variables', function() { let bundle, errs; diff --git a/fluent-bundle/test/attributes_test.js b/fluent-bundle/test/attributes_test.js index 7545e66b8..86b5cc90b 100644 --- a/fluent-bundle/test/attributes_test.js +++ b/fluent-bundle/test/attributes_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Attributes', function() { let bundle, args, errs; diff --git a/fluent-bundle/test/bomb_test.js b/fluent-bundle/test/bomb_test.js index 5e6191777..9e3c207bf 100644 --- a/fluent-bundle/test/bomb_test.js +++ b/fluent-bundle/test/bomb_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Reference bombs', function() { let bundle, args, errs; diff --git a/fluent-bundle/test/constructor_test.js b/fluent-bundle/test/constructor_test.js index 509c92b99..c80f31d4b 100644 --- a/fluent-bundle/test/constructor_test.js +++ b/fluent-bundle/test/constructor_test.js @@ -4,8 +4,8 @@ import assert from 'assert'; import sinon from 'sinon'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('FluentBundle constructor', function() { setup(function() { diff --git a/fluent-bundle/test/context_test.js b/fluent-bundle/test/context_test.js index 450806f5a..698f207eb 100644 --- a/fluent-bundle/test/context_test.js +++ b/fluent-bundle/test/context_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Bundle', function() { let bundle; diff --git a/fluent-bundle/test/errors_test.js b/fluent-bundle/test/errors_test.js index ecabb81f2..93e611c2e 100644 --- a/fluent-bundle/test/errors_test.js +++ b/fluent-bundle/test/errors_test.js @@ -3,8 +3,8 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import FluentBundle from "../src/bundle"; -import FluentResource from '../src/resource'; +import {FluentBundle} from "../esm/bundle"; +import {FluentResource} from '../esm/resource'; suite("Errors", function() { let bundle; diff --git a/fluent-bundle/test/functions_builtin_test.js b/fluent-bundle/test/functions_builtin_test.js index 6e6f605bf..9f04911c4 100644 --- a/fluent-bundle/test/functions_builtin_test.js +++ b/fluent-bundle/test/functions_builtin_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Built-in functions', function() { let bundle, errors; diff --git a/fluent-bundle/test/functions_runtime_test.js b/fluent-bundle/test/functions_runtime_test.js index 3403ee355..b3510ba70 100644 --- a/fluent-bundle/test/functions_runtime_test.js +++ b/fluent-bundle/test/functions_runtime_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Runtime-specific functions', function() { let bundle, args, errs; diff --git a/fluent-bundle/test/functions_test.js b/fluent-bundle/test/functions_test.js index 848a0b99c..1974d239c 100644 --- a/fluent-bundle/test/functions_test.js +++ b/fluent-bundle/test/functions_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Functions', function() { let bundle, args, errs; diff --git a/fluent-bundle/test/index.js b/fluent-bundle/test/index.js deleted file mode 100644 index da277e950..000000000 --- a/fluent-bundle/test/index.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const fs = require('fs'); -require('intl-pluralrules'); - -exports.readfile = function readfile(path) { - return new Promise(function(resolve, reject) { - fs.readFile(path, function(err, file) { - return err ? reject(err) : resolve(file.toString()); - }); - }); -}; diff --git a/fluent-bundle/test/isolating_test.js b/fluent-bundle/test/isolating_test.js index d89601981..4a1907edb 100644 --- a/fluent-bundle/test/isolating_test.js +++ b/fluent-bundle/test/isolating_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; // Unicode bidi isolation characters. const FSI = '\u2068'; diff --git a/fluent-bundle/test/literals_test.js b/fluent-bundle/test/literals_test.js index 6b15fb51f..7fa7fbc0a 100644 --- a/fluent-bundle/test/literals_test.js +++ b/fluent-bundle/test/literals_test.js @@ -2,8 +2,8 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import FluentBundle from "../src/bundle"; -import FluentResource from '../src/resource'; +import {FluentBundle} from "../esm/bundle"; +import {FluentResource} from '../esm/resource'; suite('Literals as selectors', function() { let bundle, errs; diff --git a/fluent-bundle/test/macros_test.js b/fluent-bundle/test/macros_test.js index 641b708f2..5e1c00436 100644 --- a/fluent-bundle/test/macros_test.js +++ b/fluent-bundle/test/macros_test.js @@ -3,8 +3,8 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import FluentBundle from "../src/bundle"; -import FluentResource from '../src/resource'; +import {FluentBundle} from "../esm/bundle"; +import {FluentResource} from '../esm/resource'; suite("Macros", function() { let bundle, errs; diff --git a/fluent-bundle/test/object_prototype_test.js b/fluent-bundle/test/object_prototype_test.js index 8a3b42278..af3e7ecd6 100644 --- a/fluent-bundle/test/object_prototype_test.js +++ b/fluent-bundle/test/object_prototype_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Interesting Object properties', function() { let bundle, errs; diff --git a/fluent-bundle/test/parser_reference_test.js b/fluent-bundle/test/parser_reference_test.js index 55fbd42fd..116ebadc1 100644 --- a/fluent-bundle/test/parser_reference_test.js +++ b/fluent-bundle/test/parser_reference_test.js @@ -1,36 +1,29 @@ import assert from 'assert'; import { join } from 'path'; -import { readdir } from 'fs'; -import { readfile } from './index'; +import { readdirSync, readFileSync} from 'fs'; -import FluentResource from '../src/resource'; +import {FluentResource} from '../esm/resource'; const ftlFixtures = join( __dirname, '..', '..', 'fluent-syntax', 'test', 'fixtures_reference' ); const jsonFixtures = join(__dirname, 'fixtures_reference'); -readdir(ftlFixtures, function(err, filenames) { - if (err) { - throw err; - } - - const ftlnames = filenames.filter( - filename => filename.endsWith('.ftl') - ); +let filenames = readdirSync(ftlFixtures); +let ftlnames = filenames.filter( + filename => filename.endsWith('.ftl') +); - suite('Reference tests', function() { - for (const ftlfilename of ftlnames) { - const jsonfilename = ftlfilename.replace(/ftl$/, 'json'); - const ftlpath = join(ftlFixtures, ftlfilename); - const jsonpath = join(jsonFixtures, jsonfilename); - test(ftlfilename, async function() { - const [ftl, expected] = await Promise.all( - [ftlpath, jsonpath].map(readfile) - ); - const resource = new FluentResource(ftl); - assert.deepEqual(resource, JSON.parse(expected)); - }); - } - }); +suite('Reference tests', function() { + for (const ftlfilename of ftlnames) { + const jsonfilename = ftlfilename.replace(/ftl$/, 'json'); + const ftlpath = join(ftlFixtures, ftlfilename); + const jsonpath = join(jsonFixtures, jsonfilename); + test(ftlfilename, function() { + let ftl = readFileSync(ftlpath, "utf8"); + let expected = readFileSync(jsonpath, "utf8"); + let resource = new FluentResource(ftl); + assert.deepEqual(resource, JSON.parse(expected)); + }); + } }); diff --git a/fluent-bundle/test/parser_structure_test.js b/fluent-bundle/test/parser_structure_test.js index 70207222c..9f75d7e40 100644 --- a/fluent-bundle/test/parser_structure_test.js +++ b/fluent-bundle/test/parser_structure_test.js @@ -1,36 +1,29 @@ import assert from 'assert'; import { join } from 'path'; -import { readdir } from 'fs'; -import { readfile } from './index'; +import { readdirSync, readFileSync} from 'fs'; -import FluentResource from '../src/resource'; +import {FluentResource} from '../esm/resource'; const ftlFixtures = join( __dirname, '..', '..', 'fluent-syntax', 'test', 'fixtures_structure' ); const jsonFixtures = join(__dirname, 'fixtures_structure'); -readdir(ftlFixtures, function(err, filenames) { - if (err) { - throw err; - } - - const ftlnames = filenames.filter( - filename => filename.endsWith('.ftl') - ); +let filenames = readdirSync(ftlFixtures); +let ftlnames = filenames.filter( + filename => filename.endsWith('.ftl') +); - suite('Structure tests', function() { - for (const ftlfilename of ftlnames) { - const jsonfilename = ftlfilename.replace(/ftl$/, 'json'); - const ftlpath = join(ftlFixtures, ftlfilename); - const jsonpath = join(jsonFixtures, jsonfilename); - test(ftlfilename, async function() { - const [ftl, expected] = await Promise.all( - [ftlpath, jsonpath].map(readfile) - ); - const resource = new FluentResource(ftl); - assert.deepEqual(resource, JSON.parse(expected)); - }); - } - }); +suite('Structure tests', function() { + for (const ftlfilename of ftlnames) { + const jsonfilename = ftlfilename.replace(/ftl$/, 'json'); + const ftlpath = join(ftlFixtures, ftlfilename); + const jsonpath = join(jsonFixtures, jsonfilename); + test(ftlfilename, function() { + let ftl = readFileSync(ftlpath, "utf8"); + let expected = readFileSync(jsonpath, "utf8"); + const resource = new FluentResource(ftl); + assert.deepEqual(resource, JSON.parse(expected)); + }); + } }); diff --git a/fluent-bundle/test/patterns_test.js b/fluent-bundle/test/patterns_test.js index 03918a3da..5a507cb88 100644 --- a/fluent-bundle/test/patterns_test.js +++ b/fluent-bundle/test/patterns_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Patterns', function(){ let bundle, args, errs; diff --git a/fluent-bundle/test/primitives_test.js b/fluent-bundle/test/primitives_test.js index 760549033..48a96e502 100644 --- a/fluent-bundle/test/primitives_test.js +++ b/fluent-bundle/test/primitives_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Primitives', function() { let bundle, args, errs; diff --git a/fluent-bundle/test/select_expressions_test.js b/fluent-bundle/test/select_expressions_test.js index 10d06c481..0a7f80e03 100644 --- a/fluent-bundle/test/select_expressions_test.js +++ b/fluent-bundle/test/select_expressions_test.js @@ -2,8 +2,8 @@ import assert from "assert"; import ftl from "@fluent/dedent"; -import FluentBundle from "../src/bundle"; -import FluentResource from '../src/resource'; +import {FluentBundle} from "../esm/bundle"; +import {FluentResource} from '../esm/resource'; suite("Select expressions", function() { let bundle, errs; diff --git a/fluent-bundle/test/transform_test.js b/fluent-bundle/test/transform_test.js index 1eaf798e2..619fed66e 100644 --- a/fluent-bundle/test/transform_test.js +++ b/fluent-bundle/test/transform_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Transformations', function(){ let bundle, errs; diff --git a/fluent-bundle/test/values_format_test.js b/fluent-bundle/test/values_format_test.js index 6c4f7b0f1..a08631f51 100644 --- a/fluent-bundle/test/values_format_test.js +++ b/fluent-bundle/test/values_format_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Formatting values', function(){ let bundle, args, errs; diff --git a/fluent-bundle/test/values_ref_test.js b/fluent-bundle/test/values_ref_test.js index 35f950e30..95e0b4e28 100644 --- a/fluent-bundle/test/values_ref_test.js +++ b/fluent-bundle/test/values_ref_test.js @@ -3,8 +3,8 @@ import assert from 'assert'; import ftl from "@fluent/dedent"; -import FluentBundle from '../src/bundle'; -import FluentResource from '../src/resource'; +import {FluentBundle} from '../esm/bundle'; +import {FluentResource} from '../esm/resource'; suite('Referencing values', function(){ let bundle, args, errs; diff --git a/fluent-bundle/tsconfig.json b/fluent-bundle/tsconfig.json new file mode 100644 index 000000000..45fefaee8 --- /dev/null +++ b/fluent-bundle/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "es2015", + "strict": true, + "allowJs": false, + "esModuleInterop": true, + "moduleResolution": "node", + "noEmitHelpers": true, + "declaration": true, + "outDir": "./esm", + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/fluent-dedent/makefile b/fluent-dedent/makefile index eab9b602e..c6585be16 100644 --- a/fluent-dedent/makefile +++ b/fluent-dedent/makefile @@ -29,7 +29,6 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -clean: - @rm -f $(PACKAGE).js compat.js - @rm -rf .nyc_output coverage - @echo -e " $(OK) clean" +lint: _lint +html: _html +clean: _clean diff --git a/fluent-dom/makefile b/fluent-dom/makefile index 3813f82ec..e08df310a 100644 --- a/fluent-dom/makefile +++ b/fluent-dom/makefile @@ -33,7 +33,6 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -clean: - @rm -f index.js compat.js - @rm -rf .nyc_output coverage - @echo -e " $(OK) clean" +lint: _lint +html: _html +clean: _clean diff --git a/fluent-dom/package.json b/fluent-dom/package.json index 5d10a4c07..d2d6e4a48 100644 --- a/fluent-dom/package.json +++ b/fluent-dom/package.json @@ -39,6 +39,7 @@ "node": ">=10.0.0" }, "devDependencies": { + "@fluent/bundle": "^0.14.0", "jsdom": "^15.1.0" }, "dependencies": { diff --git a/fluent-dom/test/dom_localization_test.js b/fluent-dom/test/dom_localization_test.js index 26b498b08..1a48d2238 100644 --- a/fluent-dom/test/dom_localization_test.js +++ b/fluent-dom/test/dom_localization_test.js @@ -1,5 +1,5 @@ import assert from "assert"; -import { FluentBundle, FluentResource } from "../../fluent-bundle/src/index"; +import { FluentBundle, FluentResource } from "@fluent/bundle/index"; import DOMLocalization from "../src/dom_localization"; async function* mockGenerateMessages(resourceIds) { diff --git a/fluent-dom/test/localization_test.js b/fluent-dom/test/localization_test.js index d36d8cf6d..a36797d22 100644 --- a/fluent-dom/test/localization_test.js +++ b/fluent-dom/test/localization_test.js @@ -1,5 +1,5 @@ import assert from "assert"; -import { FluentBundle, FluentResource } from "../../fluent-bundle/src/index"; +import { FluentBundle, FluentResource } from "@fluent/bundle/index"; import Localization from "../src/localization"; async function* mockGenerateMessages(resourceIds) { diff --git a/fluent-gecko/makefile b/fluent-gecko/makefile index 4dfded46c..9c7ca97c3 100644 --- a/fluent-gecko/makefile +++ b/fluent-gecko/makefile @@ -15,6 +15,7 @@ test: build: Fluent.jsm FluentSyntax.jsm Localization.jsm DOMLocalization.jsm l10n.js fluent-react.js Fluent.jsm: $(SOURCES) + $(MAKE) -sC ../fluent-bundle compile @rollup $(CURDIR)/src/fluent.js \ --config ./xpcom_config.js \ --output.intro "/* $(call version,fluent-bundle) */" \ @@ -61,3 +62,6 @@ clean: @rm -rf dist @rm -rf .nyc_output coverage @echo -e " $(OK) clean" + +lint: _lint +html: _html diff --git a/fluent-gecko/src/fluent.js b/fluent-gecko/src/fluent.js index 65c6b8007..7a746ce28 100644 --- a/fluent-gecko/src/fluent.js +++ b/fluent-gecko/src/fluent.js @@ -1,17 +1,15 @@ import { FluentBundle, FluentResource, - FluentError, FluentType, FluentNumber, FluentDateTime, -} from "../../fluent-bundle/src/index"; +} from "../../fluent-bundle/esm/index.js"; this.EXPORTED_SYMBOLS = [ ...Object.keys({ FluentBundle, FluentResource, - FluentError, FluentType, FluentNumber, FluentDateTime, diff --git a/fluent-langneg/makefile b/fluent-langneg/makefile index b80edb608..e66f498b3 100644 --- a/fluent-langneg/makefile +++ b/fluent-langneg/makefile @@ -29,7 +29,6 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -clean: - @rm -f index.js compat.js - @rm -rf .nyc_output coverage - @echo -e " $(OK) clean" +lint: _lint +html: _html +clean: _clean diff --git a/fluent-react/makefile b/fluent-react/makefile index 193d57f46..4a262dce0 100644 --- a/fluent-react/makefile +++ b/fluent-react/makefile @@ -29,7 +29,6 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -clean: - @rm -f index.js compat.js - @rm -rf .nyc_output coverage - @echo -e " $(OK) clean" +lint: _lint +html: _html +clean: _clean diff --git a/fluent-react/package.json b/fluent-react/package.json index 9c4927c62..3007924b0 100644 --- a/fluent-react/package.json +++ b/fluent-react/package.json @@ -57,6 +57,7 @@ "devDependencies": { "@babel/preset-env": "^7.5.5", "@babel/preset-react": "7.0.0", + "@fluent/bundle": "^0.14.0", "babel-jest": "^24.8.0", "babel-plugin-transform-rename-import": "^2.2.0", "jest": "^24.8.0", diff --git a/fluent-sequence/makefile b/fluent-sequence/makefile index 24530d9b5..8d79d8a6c 100644 --- a/fluent-sequence/makefile +++ b/fluent-sequence/makefile @@ -29,7 +29,6 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -clean: - @rm -f index.js compat.js - @rm -rf .nyc_output coverage - @echo -e " $(OK) clean" +lint: _lint +html: _html +clean: _clean diff --git a/fluent-syntax/makefile b/fluent-syntax/makefile index 32d8d0c5d..69d766cd7 100644 --- a/fluent-syntax/makefile +++ b/fluent-syntax/makefile @@ -29,10 +29,9 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -clean: - @rm -f index.js compat.js - @rm -rf .nyc_output coverage - @echo -e " $(OK) clean" +lint: _lint +html: _html +clean: _clean STRUCTURE_FTL := $(wildcard test/fixtures_structure/*.ftl) STRUCTURE_AST := $(STRUCTURE_FTL:.ftl=.json) diff --git a/package.json b/package.json index 136f9acd0..65c00fc6d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", + "@typescript-eslint/eslint-plugin": "^2.10.0", + "@typescript-eslint/parser": "^2.10.0", "babel-eslint": "^10.0.1", "colors": "^1.3.3", "commander": "^2.20", @@ -27,6 +29,8 @@ "prettyjson": "^1.2.1", "rollup": "^1.9.3", "rollup-plugin-babel": "^4.3.2", - "rollup-plugin-node-resolve": "^4.2.2" + "rollup-plugin-node-resolve": "^4.2.2", + "typedoc": "^0.15.3", + "typescript": "^3.7.2" } }