Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint_ts.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 0 additions & 16 deletions fluent-langneg/.esdoc.json

This file was deleted.

1 change: 1 addition & 0 deletions fluent-langneg/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
esm/
/index.js
/compat.js
4 changes: 3 additions & 1 deletion fluent-langneg/.npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.nyc_output
coverage
docs
esm/.compiled
src
test
makefile
tsconfig.json
38 changes: 32 additions & 6 deletions fluent-langneg/makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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) \
Expand All @@ -22,14 +36,26 @@ 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) \
--name $(GLOBAL) \
--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"
7 changes: 0 additions & 7 deletions fluent-langneg/src/accepted_languages.js

This file was deleted.

7 changes: 7 additions & 0 deletions fluent-langneg/src/accepted_languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function acceptedLanguages(str: string = ""): Array<string> {
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]);
}
4 changes: 2 additions & 2 deletions fluent-langneg/src/index.js → fluent-langneg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
55 changes: 35 additions & 20 deletions fluent-langneg/src/locale.js → fluent-langneg/src/locale.ts
Original file line number Diff line number Diff line change
@@ -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}|\\*))";
Expand All @@ -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.
*
Expand All @@ -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;
Expand All @@ -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;
Expand Down
17 changes: 8 additions & 9 deletions fluent-langneg/src/matches.js → fluent-langneg/src/matches.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<string>,
availableLocales: Array<string>,
strategy: string
): Array<string> {
const supportedLocales: Set<string> = new Set();
const availableLocalesMap: Map<string, Locale> = new Map();

for (let locale of availableLocales) {
let newLocale = new Locale(locale);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
import filterMatches from "./matches";
import {filterMatches} from "./matches";

function GetOption(options, property, type, values, fallback) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this function because I don't think the implementation of @fluent/langneg needs to be 100% defensive. For TypeScript consumers, TS already gives us some of those guaranties. For non-TS consumers, we don't usually verify all inputs in other @fluent packages as thoroughly as this function did it.

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;
}

/**
Expand Down Expand Up @@ -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<string>,
availableLocales: Array<string>,
{
strategy = "filtering",
defaultLocale,
}: NegotiateLanguagesOptions = {}
): Array<string> {

const supportedLocales = filterMatches(
resolvedReqLoc,
resolvedAvailLoc, strategy
Array.from(Object(requestedLocales)).map(String),
Array.from(Object(availableLocales)).map(String),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to remove the string and array normalization here as well, because again, I think we can make reasonable assumptions about the quality of the input. There are tests which fail if I do that, however.

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;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Locale from "./locale";
import {Locale} from "./locale";

/**
* Below is a manually a list of likely subtags corresponding to Unicode
Expand All @@ -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<string, string> = {
"ar": "ar-arab-eg",
"az-arab": "az-arab-ir",
"az-ir": "az-arab-ir",
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion fluent-langneg/test/headers_test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
Loading