From e6f11ddbc0392f7b00a38263fe8c36eae6e2141d Mon Sep 17 00:00:00 2001 From: Danny Gleckler Date: Fri, 27 Sep 2024 16:24:06 -0400 Subject: [PATCH] feat: Add PluralRules locale data shimming (#200) * Remove @formatjs/intl-pluralrule * Add custom PluralRules * Add more tests; Fix supportedLocalesOf and getCanonicalLocales * Add localize test * Fix lint * Cleanup * Remove redundant getCanonicalLocales call --- .github/dependabot.yml | 1 - lib/PluralRules.js | 78 +++++++++++++++++++++++++++++++++++++++ lib/localize.js | 4 +- package-lock.json | 3 +- package.json | 1 - test/PluralRules.test.js | 79 ++++++++++++++++++++++++++++++++++++++++ test/localize.test.js | 22 +++++++++++ 7 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 lib/PluralRules.js create mode 100644 test/PluralRules.test.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fb24ce1..8406a63 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,3 @@ updates: # update-package-lock workflow already handles minor/patch updates - dependency-name: "*" update-types: ["version-update:semver-minor", "version-update:semver-patch"] - - dependency-name: "@formatjs/intl-pluralrules" diff --git a/lib/PluralRules.js b/lib/PluralRules.js new file mode 100644 index 0000000..b2bba60 --- /dev/null +++ b/lib/PluralRules.js @@ -0,0 +1,78 @@ +const localeData = { + mi: { + aliases: ['mri', 'mao'], + options: { + cardinal: { + locale: 'mi', + pluralCategories : [ 'one', 'other' ], + shim: true + }, + ordinal: { + locale: 'mi', + pluralCategories : [ 'other' ], + shim: true + }, + }, + select(n, ord) { + return !ord && n === 1 ? 'one' : 'other'; + } + } +}; + +function getCanonicalLocales(locales) { + const mappedLocales = [locales].flat().map(locale => { + for (const canonicalLocale in localeData) { + if (localeData[canonicalLocale].aliases.includes(locale)) { + return canonicalLocale; + } + } + return locale; + }); + return Intl.getCanonicalLocales(mappedLocales); +} + +class PluralRules extends Intl.PluralRules { + + static shim = true; + static supportedLocalesOf(locales) { + return [locales].flat().map(l => { + const canonicalLocale = getCanonicalLocales(l)[0]; + if (localeData[canonicalLocale]) { + return canonicalLocale; + } + return super.supportedLocalesOf(l); + }).flat(); + } + #localeData; + #locale; + #type; + + constructor(locales, options = {}) { + super(locales, options); + this.#locale = PluralRules.supportedLocalesOf(locales)[0]; + this.#type = options.type ?? 'cardinal'; + if (localeData[this.#locale]) { + this.#localeData = localeData[this.#locale]; + } + } + + resolvedOptions() { + return { ...super.resolvedOptions(), ...this.#localeData?.options[this.#type] }; + } + + select(n) { + if (this.#localeData) { + return this.#localeData.select(n, this.#type === 'ordinal'); + } else { + return super.select(n); + } + } + +} + +Object.defineProperty(Intl, 'PluralRules', { + value: PluralRules, + writable: true, + enumerable: false, + configurable: true, +}); diff --git a/lib/localize.js b/lib/localize.js index 66259f0..2b42e48 100644 --- a/lib/localize.js +++ b/lib/localize.js @@ -1,5 +1,5 @@ -import '@formatjs/intl-pluralrules/dist-es6/polyfill-locales.js'; -import { defaultLocale as fallbackLang, getDocumentLocaleSettings, supportedLangpacks } from '../lib/common.js'; +import './PluralRules.js'; +import { defaultLocale as fallbackLang, getDocumentLocaleSettings, supportedLangpacks } from './common.js'; import { getLocalizeOverrideResources } from '../helpers/getLocalizeResources.js'; import IntlMessageFormat from 'intl-messageformat'; diff --git a/package-lock.json b/package-lock.json index b7604bb..a8b0fc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "3.18.0", "license": "Apache-2.0", "dependencies": { - "@formatjs/intl-pluralrules": "^1", "intl-messageformat": "^10" }, "devDependencies": { @@ -583,6 +582,7 @@ "version": "1.5.9", "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-1.5.9.tgz", "integrity": "sha512-37E1ZG+Oqo3qrpUfumzNcFTV+V+NCExmTkkQ9Zw4FSlvJ4WhbbeYdieVapUVz9M0cLy8XrhCkfuM/Kn03iKReg==", + "dev": true, "license": "MIT", "dependencies": { "@formatjs/intl-utils": "^2.3.0" @@ -593,6 +593,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz", "integrity": "sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==", "deprecated": "the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package", + "dev": true, "license": "MIT" }, "node_modules/@hapi/bourne": { diff --git a/package.json b/package.json index e4aa8b6..d62471e 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "sinon": "^19.0.2" }, "dependencies": { - "@formatjs/intl-pluralrules": "^1", "intl-messageformat": "^10" } } diff --git a/test/PluralRules.test.js b/test/PluralRules.test.js new file mode 100644 index 0000000..ec95ad4 --- /dev/null +++ b/test/PluralRules.test.js @@ -0,0 +1,79 @@ +import '../lib/PluralRules.js'; +import { expect } from '@brightspace-ui/testing'; +import { getDocumentLocaleSettings } from '../lib/common.js'; + +describe('PluralRules', () => { + + const documentLocaleSettings = getDocumentLocaleSettings(); + + afterEach(() => documentLocaleSettings.reset()); + + it('extends native Intl.PluralRules', () => { + const native = Object.getPrototypeOf(Intl.PluralRules); + expect(Intl.PluralRules.shim).to.be.true; + expect(native).to.have.property('name', 'PluralRules'); + expect(native).to.not.have.property('shim'); + }); + + it('uses native data by default', () => { + const shim = new Intl.PluralRules('cy'); + const native = new (Object.getPrototypeOf(Intl.PluralRules))('cy'); + expect(shim.resolvedOptions()).to.deep.equal(native.resolvedOptions()); + expect(shim.select(2)).to.equal('two'); + }); + + it('resolves to canonical locales', () => { + expect(new Intl.PluralRules('mao').resolvedOptions().locale).to.equal('mi'); + expect(new Intl.PluralRules('mri').resolvedOptions().locale).to.equal('mi'); + expect(new Intl.PluralRules(['abcdefg', 'mri']).resolvedOptions().locale).to.equal('mi'); + }); + + it('includes custom locales as supported', () => { + expect(Intl.PluralRules.supportedLocalesOf(['abc', 'mao', 'en'])).to.deep.equal(['mi', 'en']); + }); + + [ + { + locale: 'mi', + type: 'cardinal', + options: { + locale: 'mi', + shim: true, + pluralCategories: [ 'one', 'other' ] + }, + select: { + one: [1], + other: [0, 2, 3, 11] + } + }, + { + locale: 'mi', + type: 'ordinal', + options: { + locale: 'mi', + shim: true, + pluralCategories: [ 'other' ] + }, + select: { + other: [0, 1, 2, 3, 11] + } + } + ].forEach(({ locale, type, options, select }) => { + + documentLocaleSettings.language = locale; + const pluralRules = new Intl.PluralRules(locale, { type }); + + it(`should use custom ${type} data for "${locale}"`, () => { + expect(pluralRules.resolvedOptions()).to.deep.include(options); + }); + + it(`should select the correct ${type} number categories for "${locale}"`, () => { + options.pluralCategories.forEach(cat => { + select[cat].forEach(num => { + expect(pluralRules.select(num)).to.equal(cat); + }); + }); + }); + }); + +}); diff --git a/test/localize.test.js b/test/localize.test.js index e9ba0a2..a45d0be 100644 --- a/test/localize.test.js +++ b/test/localize.test.js @@ -9,6 +9,10 @@ const resources = { }, 'en-gb': { basic: '{employerName} is my employer, but British!' + }, + mi: { + plural: '{a, plural, one {one} other {other}}', + ordinal: '{a, selectordinal, one {one} other {other}}' } }; @@ -79,6 +83,24 @@ describe('Localize', () => { expect(localized).to.equal('This message has 2 arguments'); }); + it('should select the correct category for shimmed locales', async() => { + await localizer.ready; + document.documentElement.lang = 'mi'; + await updatePromise; + + const pluralOne = localizer.localize('plural', { a: 1 }); + expect(pluralOne).to.equal('one'); + + const pluralTwo = localizer.localize('plural', { a: 2 }); + expect(pluralTwo).to.equal('other'); + + const ordinalOne = localizer.localize('ordinal', { a: 1 }); + expect(ordinalOne).to.equal('other'); + + const ordinalTwo = localizer.localize('ordinal', { a: 2 }); + expect(ordinalTwo).to.equal('other'); + }); + }); describe('localizeHTML()', () => {