Skip to content

Commit e6f11dd

Browse files
authored
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
1 parent 2b8d554 commit e6f11dd

7 files changed

+183
-5
lines changed

.github/dependabot.yml

-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ updates:
99
# update-package-lock workflow already handles minor/patch updates
1010
- dependency-name: "*"
1111
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
12-
- dependency-name: "@formatjs/intl-pluralrules"

lib/PluralRules.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const localeData = {
2+
mi: {
3+
aliases: ['mri', 'mao'],
4+
options: {
5+
cardinal: {
6+
locale: 'mi',
7+
pluralCategories : [ 'one', 'other' ],
8+
shim: true
9+
},
10+
ordinal: {
11+
locale: 'mi',
12+
pluralCategories : [ 'other' ],
13+
shim: true
14+
},
15+
},
16+
select(n, ord) {
17+
return !ord && n === 1 ? 'one' : 'other';
18+
}
19+
}
20+
};
21+
22+
function getCanonicalLocales(locales) {
23+
const mappedLocales = [locales].flat().map(locale => {
24+
for (const canonicalLocale in localeData) {
25+
if (localeData[canonicalLocale].aliases.includes(locale)) {
26+
return canonicalLocale;
27+
}
28+
}
29+
return locale;
30+
});
31+
return Intl.getCanonicalLocales(mappedLocales);
32+
}
33+
34+
class PluralRules extends Intl.PluralRules {
35+
36+
static shim = true;
37+
static supportedLocalesOf(locales) {
38+
return [locales].flat().map(l => {
39+
const canonicalLocale = getCanonicalLocales(l)[0];
40+
if (localeData[canonicalLocale]) {
41+
return canonicalLocale;
42+
}
43+
return super.supportedLocalesOf(l);
44+
}).flat();
45+
}
46+
#localeData;
47+
#locale;
48+
#type;
49+
50+
constructor(locales, options = {}) {
51+
super(locales, options);
52+
this.#locale = PluralRules.supportedLocalesOf(locales)[0];
53+
this.#type = options.type ?? 'cardinal';
54+
if (localeData[this.#locale]) {
55+
this.#localeData = localeData[this.#locale];
56+
}
57+
}
58+
59+
resolvedOptions() {
60+
return { ...super.resolvedOptions(), ...this.#localeData?.options[this.#type] };
61+
}
62+
63+
select(n) {
64+
if (this.#localeData) {
65+
return this.#localeData.select(n, this.#type === 'ordinal');
66+
} else {
67+
return super.select(n);
68+
}
69+
}
70+
71+
}
72+
73+
Object.defineProperty(Intl, 'PluralRules', {
74+
value: PluralRules,
75+
writable: true,
76+
enumerable: false,
77+
configurable: true,
78+
});

lib/localize.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import '@formatjs/intl-pluralrules/dist-es6/polyfill-locales.js';
2-
import { defaultLocale as fallbackLang, getDocumentLocaleSettings, supportedLangpacks } from '../lib/common.js';
1+
import './PluralRules.js';
2+
import { defaultLocale as fallbackLang, getDocumentLocaleSettings, supportedLangpacks } from './common.js';
33
import { getLocalizeOverrideResources } from '../helpers/getLocalizeResources.js';
44
import IntlMessageFormat from 'intl-messageformat';
55

package-lock.json

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"sinon": "^19.0.2"
4545
},
4646
"dependencies": {
47-
"@formatjs/intl-pluralrules": "^1",
4847
"intl-messageformat": "^10"
4948
}
5049
}

test/PluralRules.test.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import '../lib/PluralRules.js';
2+
import { expect } from '@brightspace-ui/testing';
3+
import { getDocumentLocaleSettings } from '../lib/common.js';
4+
5+
describe('PluralRules', () => {
6+
7+
const documentLocaleSettings = getDocumentLocaleSettings();
8+
9+
afterEach(() => documentLocaleSettings.reset());
10+
11+
it('extends native Intl.PluralRules', () => {
12+
const native = Object.getPrototypeOf(Intl.PluralRules);
13+
expect(Intl.PluralRules.shim).to.be.true;
14+
expect(native).to.have.property('name', 'PluralRules');
15+
expect(native).to.not.have.property('shim');
16+
});
17+
18+
it('uses native data by default', () => {
19+
const shim = new Intl.PluralRules('cy');
20+
const native = new (Object.getPrototypeOf(Intl.PluralRules))('cy');
21+
expect(shim.resolvedOptions()).to.deep.equal(native.resolvedOptions());
22+
expect(shim.select(2)).to.equal('two');
23+
});
24+
25+
it('resolves to canonical locales', () => {
26+
expect(new Intl.PluralRules('mao').resolvedOptions().locale).to.equal('mi');
27+
expect(new Intl.PluralRules('mri').resolvedOptions().locale).to.equal('mi');
28+
expect(new Intl.PluralRules(['abcdefg', 'mri']).resolvedOptions().locale).to.equal('mi');
29+
});
30+
31+
it('includes custom locales as supported', () => {
32+
expect(Intl.PluralRules.supportedLocalesOf(['abc', 'mao', 'en'])).to.deep.equal(['mi', 'en']);
33+
});
34+
35+
[
36+
{
37+
locale: 'mi',
38+
type: 'cardinal',
39+
options: {
40+
locale: 'mi',
41+
shim: true,
42+
pluralCategories: [ 'one', 'other' ]
43+
},
44+
select: {
45+
one: [1],
46+
other: [0, 2, 3, 11]
47+
}
48+
},
49+
{
50+
locale: 'mi',
51+
type: 'ordinal',
52+
options: {
53+
locale: 'mi',
54+
shim: true,
55+
pluralCategories: [ 'other' ]
56+
},
57+
select: {
58+
other: [0, 1, 2, 3, 11]
59+
}
60+
}
61+
].forEach(({ locale, type, options, select }) => {
62+
63+
documentLocaleSettings.language = locale;
64+
const pluralRules = new Intl.PluralRules(locale, { type });
65+
66+
it(`should use custom ${type} data for "${locale}"`, () => {
67+
expect(pluralRules.resolvedOptions()).to.deep.include(options);
68+
});
69+
70+
it(`should select the correct ${type} number categories for "${locale}"`, () => {
71+
options.pluralCategories.forEach(cat => {
72+
select[cat].forEach(num => {
73+
expect(pluralRules.select(num)).to.equal(cat);
74+
});
75+
});
76+
});
77+
});
78+
79+
});

test/localize.test.js

+22
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const resources = {
99
},
1010
'en-gb': {
1111
basic: '{employerName} is my employer, but British!'
12+
},
13+
mi: {
14+
plural: '{a, plural, one {one} other {other}}',
15+
ordinal: '{a, selectordinal, one {one} other {other}}'
1216
}
1317
};
1418

@@ -79,6 +83,24 @@ describe('Localize', () => {
7983
expect(localized).to.equal('This message has 2 arguments');
8084
});
8185

86+
it('should select the correct category for shimmed locales', async() => {
87+
await localizer.ready;
88+
document.documentElement.lang = 'mi';
89+
await updatePromise;
90+
91+
const pluralOne = localizer.localize('plural', { a: 1 });
92+
expect(pluralOne).to.equal('one');
93+
94+
const pluralTwo = localizer.localize('plural', { a: 2 });
95+
expect(pluralTwo).to.equal('other');
96+
97+
const ordinalOne = localizer.localize('ordinal', { a: 1 });
98+
expect(ordinalOne).to.equal('other');
99+
100+
const ordinalTwo = localizer.localize('ordinal', { a: 2 });
101+
expect(ordinalTwo).to.equal('other');
102+
});
103+
82104
});
83105

84106
describe('localizeHTML()', () => {

0 commit comments

Comments
 (0)