diff --git a/fluent-langneg/CHANGELOG.md b/fluent-langneg/CHANGELOG.md index f9f53372..0d0beaa2 100644 --- a/fluent-langneg/CHANGELOG.md +++ b/fluent-langneg/CHANGELOG.md @@ -4,7 +4,7 @@ ### Bug Fixes -* Adjust for updated linter rules ([#590](https://github.com/projectfluent/fluent.js/pull/590)) +- Adjust for updated linter rules ([#590](https://github.com/projectfluent/fluent.js/pull/590)) ## [@fluent/langneg 0.6.1](https://github.com/projectfluent/fluent.js/compare/@fluent/langneg@0.6.0...@fluent/langneg@0.6.1) (2021-12-21) diff --git a/fluent-langneg/src/locale.ts b/fluent-langneg/src/locale.ts index 6b8fef80..8b89a544 100644 --- a/fluent-langneg/src/locale.ts +++ b/fluent-langneg/src/locale.ts @@ -1,12 +1,15 @@ /* eslint no-magic-numbers: 0 */ const languageCodeRe = "([a-z]{2,3}|\\*)"; +const extendedCodeRe = "((?:-(?:[a-z]{3})){1,3})"; const scriptCodeRe = "(?:-([a-z]{4}|\\*))"; -const regionCodeRe = "(?:-([a-z]{2}|\\*))"; +const regionCodeRe = "(?:-([a-z]{2}|[0-9]{3}|\\*))"; const variantCodeRe = "(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))"; +const extensionCodeRe = "(-(?:[a-wy-z])(?:-[a-z]{2,8})+)"; +const privateCodeRe = "(?:-x((?:-[a-z]{2,8})+))"; /** - * Regular expression splitting locale id into four pieces: + * Regular expression splitting locale id into multiple pieces: * * Example: `en-Latn-US-macos` * @@ -18,16 +21,20 @@ const variantCodeRe = "(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))"; * It can also accept a range `*` character on any position. */ const localeRe = new RegExp( - `^${languageCodeRe}${scriptCodeRe}?${regionCodeRe}?${variantCodeRe}?$`, + `^${languageCodeRe}${extendedCodeRe}?${scriptCodeRe}?` + + `${regionCodeRe}?${variantCodeRe}?${extensionCodeRe}*${privateCodeRe}?$`, "i" ); export class Locale { isWellFormed: boolean; language?: string; + extended: string[] = []; script?: string; region?: string; variant?: string; + extension: Map = new Map(); + priv?: string; /** * Parses a locale id using the localeRe into an array with four elements. @@ -45,17 +52,31 @@ export class Locale { return; } - let [, language, script, region, variant] = result; + let [, language, extended, script, region, variant, extension, priv] = + result; if (language) { this.language = language.toLowerCase(); } + if (extended) { + this.extended = extended.substring(1).toLowerCase().split("-"); + } if (script) { this.script = script[0].toUpperCase() + script.slice(1); } if (region) { this.region = region.toUpperCase(); } + if (extension) { + for (const [, type, code] of extension.matchAll( + /(?:-([a-wy-z])((?:-[a-z]{2,8})+))/g + )) { + this.extension.set(type.toLowerCase(), code.substring(1).toLowerCase()); + } + } + if (priv) { + this.priv = priv.substring(1).toLowerCase(); + } this.variant = variant; this.isWellFormed = true; } @@ -63,9 +84,12 @@ export class Locale { isEqual(other: Locale): boolean { return ( this.language === other.language && + this.extended.every((v, i) => v === other.extended[i]) && this.script === other.script && this.region === other.region && - this.variant === other.variant + this.variant === other.variant && + compareMap(this.extension, other.extension) && + this.priv === other.priv ); } @@ -74,6 +98,9 @@ export class Locale { (this.language === other.language || (thisRange && this.language === undefined) || (otherRange && other.language === undefined)) && + (this.extended.every((v, i) => v === other.extended[i]) || + (thisRange && this.extended.length === 0) || + (otherRange && other.extended.length === 0)) && (this.script === other.script || (thisRange && this.script === undefined) || (otherRange && other.script === undefined)) && @@ -82,12 +109,27 @@ export class Locale { (otherRange && other.region === undefined)) && (this.variant === other.variant || (thisRange && this.variant === undefined) || - (otherRange && other.variant === undefined)) + (otherRange && other.variant === undefined)) && + (compareMap(this.extension, other.extension) || + (thisRange && this.extension.size === 0) || + (otherRange && other.extension.size === 0)) && + (this.priv === other.priv || + (thisRange && this.priv === undefined) || + (otherRange && other.priv === undefined)) ); } toString(): string { - return [this.language, this.script, this.region, this.variant] + const xSubtag = this.priv === undefined ? undefined : `x-${this.priv}`; + return [ + this.language, + ...this.extended, + this.script, + this.region, + this.variant, + ...[...this.extension.entries()].flat(), + xSubtag, + ] .filter(part => part !== undefined) .join("-"); } @@ -104,9 +146,12 @@ export class Locale { const newLocale = getLikelySubtagsMin(this.toString().toLowerCase()); if (newLocale) { this.language = newLocale.language; + this.extended = newLocale.extended; this.script = newLocale.script; this.region = newLocale.region; this.variant = newLocale.variant; + this.extension = newLocale.extension; + this.priv = newLocale.priv; return true; } return false; @@ -177,3 +222,18 @@ function getLikelySubtagsMin(loc: string): Locale | null { } return null; } + +function compareMap(map1: Map, map2: Map): boolean { + if (map1.size !== map2.size) { + return false; + } + + for (const [key, value] of map1) { + const other = map2.get(key); + if (other !== value || (other === undefined && !map2.has(key))) { + return false; + } + } + + return true; +} diff --git a/fluent-langneg/test/locale_test.js b/fluent-langneg/test/locale_test.js index fc9300a9..4c567a1f 100644 --- a/fluent-langneg/test/locale_test.js +++ b/fluent-langneg/test/locale_test.js @@ -3,7 +3,12 @@ import { Locale } from "../esm/locale.js"; function isLocaleEqual(str, ref) { const locale = new Locale(str); - return locale.isEqual(ref); + return locale.isEqual({ + ...{ + extended: [], + }, + ref, + }); } suite("Parses simple locales", () => { @@ -21,6 +26,15 @@ suite("Parses simple locales", () => { ); }); + test("extended part", () => { + assert.ok( + isLocaleEqual("zh-gan", { + language: "zh", + extended: ["gan"], + }) + ); + }); + test("script part", () => { assert.ok( isLocaleEqual("en-Latn", { @@ -53,6 +67,14 @@ suite("Parses simple locales", () => { region: "FA", }) ); + + assert.ok( + isLocaleEqual("es-Latn-419", { + language: "es", + script: "Latn", + region: "419", + }) + ); }); test("variant part", () => { diff --git a/fluent-react/example/src/l10n.tsx b/fluent-react/example/src/l10n.tsx index 9b0196c4..3cf46185 100644 --- a/fluent-react/example/src/l10n.tsx +++ b/fluent-react/example/src/l10n.tsx @@ -5,8 +5,9 @@ import { FluentBundle, FluentResource } from "@fluent/bundle"; import { ReactLocalization, LocalizationProvider } from "@fluent/react"; const ftl: Record = { - 'en-US': new URL( "../public/en-US.ftl", import.meta.url), - pl: new URL( "../public/pl.ftl", import.meta.url) } + "en-US": new URL("../public/en-US.ftl", import.meta.url), + pl: new URL("../public/pl.ftl", import.meta.url), +}; const DEFAULT_LOCALE = "en-US"; const AVAILABLE_LOCALES = { diff --git a/fluent-syntax/CHANGELOG.md b/fluent-syntax/CHANGELOG.md index c4ca6790..7f13f5df 100644 --- a/fluent-syntax/CHANGELOG.md +++ b/fluent-syntax/CHANGELOG.md @@ -4,8 +4,8 @@ ### Bug Fixes -* Use correct TS type for `clone()` return ([#589](https://github.com/projectfluent/fluent.js/issues/589)) -* Adjust for updated linter rules ([#590](https://github.com/projectfluent/fluent.js/pull/590)) +- Use correct TS type for `clone()` return ([#589](https://github.com/projectfluent/fluent.js/issues/589)) +- Adjust for updated linter rules ([#590](https://github.com/projectfluent/fluent.js/pull/590)) ## @fluent/syntax 0.18.0 (September 13, 2021)