From d20d33bb1e0921453113d4afd537d545c7bb4202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Mon, 30 Mar 2020 14:40:32 +0200 Subject: [PATCH] Migrate @fluent/langneg to TypeScript --- eslint_ts.json | 1 + fluent-langneg/.esdoc.json | 16 ----- fluent-langneg/.gitignore | 1 + fluent-langneg/.npmignore | 4 +- fluent-langneg/makefile | 38 ++++++++++-- fluent-langneg/src/accepted_languages.js | 7 --- fluent-langneg/src/accepted_languages.ts | 7 +++ fluent-langneg/src/{index.js => index.ts} | 4 +- fluent-langneg/src/{locale.js => locale.ts} | 55 +++++++++++------ fluent-langneg/src/{matches.js => matches.ts} | 17 +++--- ...te_languages.js => negotiate_languages.ts} | 61 ++++++------------- fluent-langneg/src/{subtags.js => subtags.ts} | 8 +-- fluent-langneg/test/headers_test.js | 2 +- fluent-langneg/test/langneg_test.js | 14 ++--- fluent-langneg/test/locale_test.js | 2 +- fluent-langneg/tsconfig.json | 16 +++++ 16 files changed, 138 insertions(+), 115 deletions(-) delete mode 100644 fluent-langneg/.esdoc.json delete mode 100644 fluent-langneg/src/accepted_languages.js create mode 100644 fluent-langneg/src/accepted_languages.ts rename fluent-langneg/src/{index.js => index.ts} (57%) rename fluent-langneg/src/{locale.js => locale.ts} (54%) rename fluent-langneg/src/{matches.js => matches.ts} (96%) rename fluent-langneg/src/{negotiate_languages.js => negotiate_languages.ts} (60%) rename fluent-langneg/src/{subtags.js => subtags.ts} (84%) create mode 100644 fluent-langneg/tsconfig.json diff --git a/eslint_ts.json b/eslint_ts.json index 5d3433ddd..d92d7b9cd 100644 --- a/eslint_ts.json +++ b/eslint_ts.json @@ -16,6 +16,7 @@ "./eslint_src.json" ], "rules": { + "complexity": "off", "prefer-const": "off", "no-unused-vars": ["error", {"args": "none"}], "@typescript-eslint/no-inferrable-types": "off", diff --git a/fluent-langneg/.esdoc.json b/fluent-langneg/.esdoc.json deleted file mode 100644 index 83cabdfe7..000000000 --- a/fluent-langneg/.esdoc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "source": "./src", - "destination": "../html/langneg", - "plugins": [ - { - "name": "esdoc-standard-plugin" - }, - { - "name": "esdoc-ecmascript-proposal-plugin", - "option": { - "objectRestSpread": true, - "asyncGenerators": true - } - } - ] -} diff --git a/fluent-langneg/.gitignore b/fluent-langneg/.gitignore index 7d147bec1..99472bc4d 100644 --- a/fluent-langneg/.gitignore +++ b/fluent-langneg/.gitignore @@ -1,2 +1,3 @@ +esm/ /index.js /compat.js diff --git a/fluent-langneg/.npmignore b/fluent-langneg/.npmignore index f520b36f5..abad613ff 100644 --- a/fluent-langneg/.npmignore +++ b/fluent-langneg/.npmignore @@ -1,5 +1,7 @@ .nyc_output coverage -docs +esm/.compiled +src test makefile +tsconfig.json diff --git a/fluent-langneg/makefile b/fluent-langneg/makefile index 62a4621f4..e28f550f8 100644 --- a/fluent-langneg/makefile +++ b/fluent-langneg/makefile @@ -3,7 +3,21 @@ GLOBAL := FluentLangNeg include ../common.mk -test: +lint: + @eslint --config $(ROOT)/eslint_ts.json --max-warnings 0 src/*.ts + @eslint --config $(ROOT)/eslint_test.json --max-warnings 0 test/ + @echo -e " $(OK) lint" + +.PHONY: compile +compile: esm/.compiled + +esm/.compiled: $(SOURCES) + @tsc + @touch $@ + @echo -e " $(OK) esm/ compiled" + +.PHONY: test +test: esm/.compiled @nyc --reporter=text --reporter=html mocha \ --recursive --ui tdd \ --require esm \ @@ -13,7 +27,7 @@ test: build: index.js compat.js index.js: $(SOURCES) - @rollup $(CURDIR)/src/index.js \ + @rollup $(CURDIR)/esm/index.js \ --config $(ROOT)/bundle_config.js \ --banner "/* $(PACKAGE)@$(VERSION) */" \ --amd.id $(PACKAGE) \ @@ -22,7 +36,7 @@ index.js: $(SOURCES) @echo -e " $(OK) $@ built" compat.js: $(SOURCES) - @rollup $(CURDIR)/src/index.js \ + @rollup $(CURDIR)/esm/index.js \ --config $(ROOT)/compat_config.js \ --banner "/* $(PACKAGE)@$(VERSION) */" \ --amd.id $(PACKAGE) \ @@ -30,6 +44,18 @@ compat.js: $(SOURCES) --output.file $@ @echo -e " $(OK) $@ built" -lint: _lint -html: _html -clean: _clean +html: + @typedoc src \ + --out ../html/bundle \ + --mode file \ + --excludeNotExported \ + --excludePrivate \ + --logger none \ + --hideGenerator + @echo -e " $(OK) html built" + +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-langneg/src/accepted_languages.js b/fluent-langneg/src/accepted_languages.js deleted file mode 100644 index 854482bc7..000000000 --- a/fluent-langneg/src/accepted_languages.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function acceptedLanguages(string = "") { - if (typeof string !== "string") { - throw new TypeError("Argument must be a string"); - } - const tokens = string.split(",").map(t => t.trim()); - return tokens.filter(t => t !== "").map(t => t.split(";")[0]); -} diff --git a/fluent-langneg/src/accepted_languages.ts b/fluent-langneg/src/accepted_languages.ts new file mode 100644 index 000000000..80c03e878 --- /dev/null +++ b/fluent-langneg/src/accepted_languages.ts @@ -0,0 +1,7 @@ +export function acceptedLanguages(str: string = ""): Array { + if (typeof str !== "string") { + throw new TypeError("Argument must be a string"); + } + const tokens = str.split(",").map(t => t.trim()); + return tokens.filter(t => t !== "").map(t => t.split(";")[0]); +} diff --git a/fluent-langneg/src/index.js b/fluent-langneg/src/index.ts similarity index 57% rename from fluent-langneg/src/index.js rename to fluent-langneg/src/index.ts index e1ed44f04..671cd51e2 100644 --- a/fluent-langneg/src/index.js +++ b/fluent-langneg/src/index.ts @@ -7,5 +7,5 @@ * */ -export { default as negotiateLanguages } from "./negotiate_languages"; -export { default as acceptedLanguages } from "./accepted_languages"; +export {negotiateLanguages} from "./negotiate_languages"; +export {acceptedLanguages} from "./accepted_languages"; diff --git a/fluent-langneg/src/locale.js b/fluent-langneg/src/locale.ts similarity index 54% rename from fluent-langneg/src/locale.js rename to fluent-langneg/src/locale.ts index 194c510be..b26c753e5 100644 --- a/fluent-langneg/src/locale.js +++ b/fluent-langneg/src/locale.ts @@ -1,6 +1,6 @@ /* eslint no-magic-numbers: 0 */ -import { getLikelySubtagsMin } from "./subtags"; +import {getLikelySubtagsMin} from "./subtags"; const languageCodeRe = "([a-z]{2,3}|\\*)"; const scriptCodeRe = "(?:-([a-z]{4}|\\*))"; @@ -22,9 +22,13 @@ const variantCodeRe = "(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))"; const localeRe = new RegExp( `^${languageCodeRe}${scriptCodeRe}?${regionCodeRe}?${variantCodeRe}?$`, "i"); -export const localeParts = ["language", "script", "region", "variant"]; +export class Locale { + isWellFormed: boolean; + language?: string; + script?: string; + region?: string; + variant?: string; -export default class Locale { /** * Parses a locale id using the localeRe into an array with four elements. * @@ -34,7 +38,7 @@ export default class Locale { * It also allows skipping the script section of the id, so `en-US` is * properly parsed as `en-*-US-*`. */ - constructor(locale) { + constructor(locale: string) { const result = localeRe.exec(locale.replace(/_/g, "-")); if (!result) { this.isWellFormed = false; @@ -56,38 +60,49 @@ export default class Locale { this.isWellFormed = true; } - isEqual(locale) { - return localeParts.every(part => this[part] === locale[part]); + isEqual(other: Locale): boolean { + return this.language === other.language + && this.script === other.script + && this.region === other.region + && this.variant === other.variant; } - matches(locale, thisRange = false, otherRange = false) { - return localeParts.every(part => { - return ((thisRange && this[part] === undefined) || - (otherRange && locale[part] === undefined) || - this[part] === locale[part]); - }); + matches(other: Locale, thisRange = false, otherRange = false): boolean { + return (this.language === other.language + || thisRange && this.language === undefined + || otherRange && other.language === undefined) + && (this.script === other.script + || thisRange && this.script === undefined + || otherRange && other.script === undefined) + && (this.region === other.region + || thisRange && this.region === undefined + || otherRange && other.region === undefined) + && (this.variant === other.variant + || thisRange && this.variant === undefined + || otherRange && other.variant === undefined); } - toString() { - return localeParts - .map(part => this[part]) + toString(): string { + return [this.language, this.script, this.region, this.variant] .filter(part => part !== undefined) .join("-"); } - clearVariants() { + clearVariants(): void { this.variant = undefined; } - clearRegion() { + clearRegion(): void { this.region = undefined; } - addLikelySubtags() { + addLikelySubtags(): boolean { const newLocale = getLikelySubtagsMin(this.toString().toLowerCase()); - if (newLocale) { - localeParts.forEach(part => this[part] = newLocale[part]); + this.language = newLocale.language; + this.script = newLocale.script; + this.region = newLocale.region; + this.variant = newLocale.variant; return true; } return false; diff --git a/fluent-langneg/src/matches.js b/fluent-langneg/src/matches.ts similarity index 96% rename from fluent-langneg/src/matches.js rename to fluent-langneg/src/matches.ts index 3f1c07ff9..de9de4e47 100644 --- a/fluent-langneg/src/matches.js +++ b/fluent-langneg/src/matches.ts @@ -1,7 +1,6 @@ /* eslint no-magic-numbers: 0 */ -/* eslint complexity: ["error", { "max": 27 }] */ -import Locale from "./locale"; +import {Locale} from "./locale"; /** * Negotiates the languages between the list of requested locales against @@ -73,13 +72,13 @@ import Locale from "./locale"; * ignoring script ranges. That means that `sr-Cyrl` will never match * against `sr-Latn`. */ -export default function filterMatches( - requestedLocales, availableLocales, strategy -) { - /* eslint complexity: ["error", 31]*/ - const supportedLocales = new Set(); - - const availableLocalesMap = new Map(); +export function filterMatches( + requestedLocales: Array, + availableLocales: Array, + strategy: string +): Array { + const supportedLocales: Set = new Set(); + const availableLocalesMap: Map = new Map(); for (let locale of availableLocales) { let newLocale = new Locale(locale); diff --git a/fluent-langneg/src/negotiate_languages.js b/fluent-langneg/src/negotiate_languages.ts similarity index 60% rename from fluent-langneg/src/negotiate_languages.js rename to fluent-langneg/src/negotiate_languages.ts index 289f097e4..121590ea9 100644 --- a/fluent-langneg/src/negotiate_languages.js +++ b/fluent-langneg/src/negotiate_languages.ts @@ -1,23 +1,8 @@ -import filterMatches from "./matches"; +import {filterMatches} from "./matches"; -function GetOption(options, property, type, values, fallback) { - let value = options[property]; - - if (value !== undefined) { - if (type === "boolean") { - value = new Boolean(value); - } else if (type === "string") { - value = String(value); - } - - if (values !== undefined && values.indexOf(value) === -1) { - throw new Error("Invalid option value"); - } - - return value; - } - - return fallback; +interface NegotiateLanguagesOptions { + strategy?: "filtering" | "matching" | "lookup"; + defaultLocale?: string; } /** @@ -63,38 +48,32 @@ function GetOption(options, property, type, values, fallback) { * * This strategy requires defaultLocale option to be set. */ -export default function negotiateLanguages( - requestedLocales, - availableLocales, - options = {} -) { - - const defaultLocale = GetOption(options, "defaultLocale", "string"); - const strategy = GetOption(options, "strategy", "string", - ["filtering", "matching", "lookup"], "filtering"); - - if (strategy === "lookup" && !defaultLocale) { - throw new Error("defaultLocale cannot be undefined for strategy `lookup`"); - } - - const resolvedReqLoc = Array.from(Object(requestedLocales)).map(loc => { - return String(loc); - }); - const resolvedAvailLoc = Array.from(Object(availableLocales)).map(loc => { - return String(loc); - }); +export function negotiateLanguages( + requestedLocales: Array, + availableLocales: Array, + { + strategy = "filtering", + defaultLocale, + }: NegotiateLanguagesOptions = {} +): Array { const supportedLocales = filterMatches( - resolvedReqLoc, - resolvedAvailLoc, strategy + Array.from(Object(requestedLocales)).map(String), + Array.from(Object(availableLocales)).map(String), + strategy ); if (strategy === "lookup") { + if (defaultLocale === undefined) { + throw new Error( + "defaultLocale cannot be undefined for strategy `lookup`"); + } if (supportedLocales.length === 0) { supportedLocales.push(defaultLocale); } } else if (defaultLocale && !supportedLocales.includes(defaultLocale)) { supportedLocales.push(defaultLocale); } + return supportedLocales; } diff --git a/fluent-langneg/src/subtags.js b/fluent-langneg/src/subtags.ts similarity index 84% rename from fluent-langneg/src/subtags.js rename to fluent-langneg/src/subtags.ts index 7b726aeaf..b9bd995cd 100644 --- a/fluent-langneg/src/subtags.js +++ b/fluent-langneg/src/subtags.ts @@ -1,4 +1,4 @@ -import Locale from "./locale"; +import {Locale} from "./locale"; /** * Below is a manually a list of likely subtags corresponding to Unicode @@ -9,7 +9,7 @@ import Locale from "./locale"; * * This version of the list is based on CLDR 30.0.3. */ -const likelySubtagsMin = { +const likelySubtagsMin: Record = { "ar": "ar-arab-eg", "az-arab": "az-arab-ir", "az-ir": "az-arab-ir", @@ -51,12 +51,12 @@ const regionMatchingLangs = [ "ru", ]; -export function getLikelySubtagsMin(loc) { +export function getLikelySubtagsMin(loc: string): Locale | null { if (likelySubtagsMin.hasOwnProperty(loc)) { return new Locale(likelySubtagsMin[loc]); } const locale = new Locale(loc); - if (regionMatchingLangs.includes(locale.language)) { + if (locale.language && regionMatchingLangs.includes(locale.language)) { locale.region = locale.language.toUpperCase(); return locale; } diff --git a/fluent-langneg/test/headers_test.js b/fluent-langneg/test/headers_test.js index 4443f71a0..4931f229c 100644 --- a/fluent-langneg/test/headers_test.js +++ b/fluent-langneg/test/headers_test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import acceptedLanguages from '../src/accepted_languages'; +import {acceptedLanguages} from '../esm/accepted_languages.js'; suite('parse headers', () => { test('without quality values', () => { diff --git a/fluent-langneg/test/langneg_test.js b/fluent-langneg/test/langneg_test.js index d3d520034..4e91a585c 100644 --- a/fluent-langneg/test/langneg_test.js +++ b/fluent-langneg/test/langneg_test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import negotiateLanguages from '../src/negotiate_languages'; +import {negotiateLanguages} from '../esm/negotiate_languages.js'; const data = { "filtering": { @@ -111,11 +111,11 @@ const data = { const json = JSON.stringify; suite('Language Negotiation', () => { + for (const strategy in data) { + for (const groupName in data[strategy]) { + const group = data[strategy][groupName]; - test('test suite matches', () => { - for (const strategy in data) { - for (const groupName in data[strategy]) { - const group = data[strategy][groupName]; + test(`${strategy} - ${groupName}`, () => { for (const test of group) { const requested = test[0]; const available = test[1]; @@ -134,7 +134,7 @@ suite('Language Negotiation', () => { assert.deepEqual(result, supported, `\nExpected ${json(requested)} * ${json(available)} = ${json(supported)}.\n`); } - } + }); } - }); + } }); diff --git a/fluent-langneg/test/locale_test.js b/fluent-langneg/test/locale_test.js index 215aca235..53914c5cb 100644 --- a/fluent-langneg/test/locale_test.js +++ b/fluent-langneg/test/locale_test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import Locale from '../src/locale'; +import {Locale} from '../esm/locale.js'; function isLocaleEqual(str, ref) { const locale = new Locale(str); diff --git a/fluent-langneg/tsconfig.json b/fluent-langneg/tsconfig.json new file mode 100644 index 000000000..45fefaee8 --- /dev/null +++ b/fluent-langneg/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" + ] +}