-
Notifications
You must be signed in to change notification settings - Fork 390
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(format-po-gettext): respect Plural-Forms header (#2070)
- Loading branch information
Showing
6 changed files
with
450 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { | ||
createLocaleTest, | ||
createSamples, | ||
fillRange, | ||
renameKeys, | ||
} from "./plural-samples" | ||
|
||
describe("Plural samples generation util", () => { | ||
test.each([ | ||
[{ "pluralRule-count-zero": null }, { zero: null }], | ||
[{ "pluralRule-count-one": null }, { one: null }], | ||
[{ "pluralRule-count-two": null }, { two: null }], | ||
[{ "pluralRule-count-few": null }, { few: null }], | ||
[{ "pluralRule-count-many": null }, { many: null }], | ||
[{ "pluralRule-count-other": null }, { other: null }], | ||
])("renameKeys", (original, expected) => { | ||
expect(renameKeys(original)).toEqual(expected) | ||
}) | ||
|
||
test("renameKeys multiple", () => { | ||
const original = { | ||
"pluralRule-count-zero": | ||
"n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000", | ||
"pluralRule-count-one": | ||
"n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000", | ||
"pluralRule-count-two": | ||
"n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000", | ||
"pluralRule-count-few": | ||
"n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …", | ||
"pluralRule-count-many": | ||
"n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …", | ||
"pluralRule-count-other": | ||
" @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …", | ||
} | ||
expect(renameKeys(original)).toEqual({ | ||
zero: "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000", | ||
one: "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000", | ||
two: "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000", | ||
few: "n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …", | ||
many: "n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …", | ||
other: | ||
" @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …", | ||
}) | ||
}) | ||
|
||
test.each([ | ||
["0~1", [0, 1]], | ||
["2~19", [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]], | ||
["100~102", [100, 101, 102]], | ||
])("fillRange - integer ranges", (range, values) => { | ||
expect(fillRange(range)).toEqual(values) | ||
}) | ||
|
||
test.each([ | ||
["0.0~1.0", [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]], | ||
// partials | ||
[ | ||
"0.4~1.6", | ||
[0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6], | ||
], | ||
["0.04~0.09", [0.04, 0.05, 0.06, 0.07, 0.08, 0.09]], | ||
[ | ||
"0.04~0.29", | ||
[ | ||
0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.11, 0.12, 0.13, 0.14, 0.15, | ||
0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, | ||
0.28, 0.29, | ||
], | ||
], | ||
])("fillRange - decimal ranges", (range, values) => { | ||
expect(fillRange(range)).toEqual(values) | ||
}) | ||
|
||
test("createSamples - single values", () => { | ||
expect(createSamples("0")).toEqual([0]) | ||
expect(createSamples("0, 1, 2")).toEqual([0, 1, 2]) | ||
expect(createSamples("0, 1.0, 2.0")).toEqual([0, 1, 2]) | ||
}) | ||
|
||
test("createSamples - integer ranges", () => { | ||
expect(createSamples("0~1")).toEqual([0, 1]) | ||
expect(createSamples("0~2")).toEqual([0, 1, 2]) | ||
expect(createSamples("0~10")).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) | ||
expect(createSamples("2~17, 100, 1000, 10000, 100000, 1000000")).toEqual([ | ||
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 100, 1000, 10000, | ||
100000, 1000000, | ||
]) | ||
}) | ||
|
||
test("createSamples - mixed src", () => { | ||
expect(createSamples("0.1~0.9")).toEqual([ | ||
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, | ||
]) | ||
// with ... | ||
expect( | ||
createSamples("0, 2~16, 100, 1000, 10000, 100000, 1000000, …") | ||
).toEqual([ | ||
0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 100, 1000, 10000, | ||
100000, 1000000, | ||
]) | ||
// mixed with integer ranges | ||
expect( | ||
createSamples("0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0") | ||
).toEqual([ | ||
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, | ||
1.7, 10, 100, 1000, 10000, 100000, | ||
]) | ||
// trailing comma | ||
expect( | ||
createSamples("0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0,") | ||
).toEqual([ | ||
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, | ||
1.7, 10, 100, 1000, 10000, 100000, | ||
]) | ||
}) | ||
|
||
test("Run on ruleset", () => { | ||
// ruleset for cs | ||
const ruleset = { | ||
"pluralRule-count-one": "i = 1 and v = 0 @integer 1", | ||
"pluralRule-count-few": "i = 2..4 and v = 0 @integer 2~4", | ||
"pluralRule-count-many": | ||
"v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …", | ||
"pluralRule-count-other": | ||
" @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …", | ||
} | ||
expect(createLocaleTest(ruleset)).toMatchInlineSnapshot(` | ||
{ | ||
pluralRule-count-few: [ | ||
2, | ||
3, | ||
4, | ||
], | ||
pluralRule-count-many: [ | ||
0, | ||
0.1, | ||
0.2, | ||
0.3, | ||
0.4, | ||
0.5, | ||
0.6, | ||
0.7, | ||
0.8, | ||
0.9, | ||
1, | ||
1.1, | ||
1.2, | ||
1.3, | ||
1.4, | ||
1.5, | ||
10, | ||
100, | ||
1000, | ||
10000, | ||
100000, | ||
1000000, | ||
], | ||
pluralRule-count-one: [ | ||
1, | ||
], | ||
pluralRule-count-other: [ | ||
0, | ||
5, | ||
6, | ||
7, | ||
8, | ||
9, | ||
10, | ||
11, | ||
12, | ||
13, | ||
14, | ||
15, | ||
16, | ||
17, | ||
18, | ||
19, | ||
100, | ||
1000, | ||
10000, | ||
100000, | ||
1000000, | ||
], | ||
} | ||
`) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import cardinals from "cldr-core/supplemental/plurals.json" | ||
|
||
/* | ||
This script is heavily influenced by one that is used to generate plural samples | ||
found here: https://github.com/nodeca/plurals-cldr/blob/master/support/generate.js | ||
Ordinals were removed, and the original script supported strings and numbers, | ||
but for the use case of lingui-gettext formatter, we only want numbers. | ||
*/ | ||
|
||
type PluralForm = "zero" | "one" | "two" | "few" | "many" | "other" | ||
type FormattedRuleset = Record<PluralForm, string> | ||
|
||
// Strip key prefixes to get clear names: zero / one / two / few / many / other | ||
// pluralRule-count-other -> other | ||
export function renameKeys(rules: Record<string, string>): FormattedRuleset { | ||
const result = {} | ||
Object.keys(rules).forEach((k) => { | ||
const newKey = k.match(/[^-]+$/)[0] | ||
result[newKey] = rules[k] | ||
}) | ||
return result as FormattedRuleset | ||
} | ||
|
||
// Create array of sample values for single range | ||
// 5~16, 0.04~0.09. Both string & integer forms (when possible) | ||
export function fillRange(value: string): number[] { | ||
let [start, end] = value.split("~") | ||
|
||
const decimals = (start.split(".")[1] || "").length | ||
// for example 0.1~0.9 has 10 values, need to add that many to list | ||
// 0.004~0.009 has 100 values | ||
let mult = Math.pow(10, decimals) | ||
|
||
const startNum = Number(start) | ||
const endNum = Number(end) | ||
|
||
let range = Array(Math.ceil(endNum * mult - startNum * mult + 1)) | ||
.fill(0) | ||
.map((v, idx) => (idx + startNum * mult) / mult) | ||
|
||
let last = range[range.length - 1] | ||
|
||
// Number defined in the range should be the last one, i.e. 5~16 should have 16 | ||
if (endNum !== last) { | ||
throw new Error(`Range create error for ${value}: last value is ${last}`) | ||
} | ||
|
||
return range.map((v) => Number(v)) | ||
} | ||
|
||
// Create array of test values for @integer or @decimal | ||
export function createSamples(src: string): number[] { | ||
let result: number[] = [] | ||
|
||
src | ||
.replace(/…/, "") | ||
.trim() | ||
.replace(/,$/, "") | ||
.split(",") | ||
.map(function (val) { | ||
return val.trim() | ||
}) | ||
.forEach((val) => { | ||
if (val.indexOf("~") !== -1) { | ||
result = result.concat(fillRange(val)) | ||
} else { | ||
result.push(Number(val)) | ||
} | ||
}) | ||
|
||
return result | ||
} | ||
|
||
// Create fixtures for single locale rules | ||
export function createLocaleTest(rules) { | ||
let result = {} | ||
|
||
Object.keys(rules).forEach((form) => { | ||
let samples = rules[form].split(/@integer|@decimal/).slice(1) | ||
|
||
result[form] = [] | ||
samples.forEach((sample) => { | ||
result[form] = result[form].concat(createSamples(sample)) | ||
}) | ||
}) | ||
|
||
return result | ||
} | ||
|
||
export function getCldrPluralSamples(): Record< | ||
string, | ||
Record<PluralForm, number[]> | ||
> { | ||
const pluralRules = {} | ||
|
||
// Parse plural rules | ||
Object.entries(cardinals.supplemental["plurals-type-cardinal"]).forEach( | ||
([loc, ruleset]) => { | ||
let rules = renameKeys(ruleset) | ||
|
||
pluralRules[loc.toLowerCase()] = createLocaleTest(rules) | ||
} | ||
) | ||
|
||
return pluralRules | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.