Skip to content

Commit f8ad0e4

Browse files
committed
Parse plural-forms header into JS function, drop make-plural dependency
1 parent c6d51e8 commit f8ad0e4

File tree

4 files changed

+83
-33
lines changed

4 files changed

+83
-33
lines changed

README.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ object destructuring and arrow functions, you'll want to use a transpiler for th
2222

2323
```js
2424
const { parsePo, parseMo } = require('gettext-to-messageformat')
25-
const { headers, translations } = parsePo(`
25+
const { headers, pluralFunction, translations } = parsePo(`
2626
# Examples from http://pology.nedohodnik.net/doc/user/en_US/ch-poformat.html
2727
# Note that the given plural-form is incomplete
2828
msgid ""
@@ -48,7 +48,7 @@ msgstr "Nema zvezde po imenu %(starname)s."
4848
`)
4949

5050
const MessageFormat = require('messageformat')
51-
const mf = new MessageFormat(headers.language)
51+
const mf = new MessageFormat({ [headers.language]: pluralFunction })
5252
const messages = mf.compile(translations)
5353

5454
messages['Time: %1 second']([1])
@@ -80,20 +80,32 @@ the parser, including the following fields:
8080
If no context is set, by default this top-level key is not included unless
8181
`forceContext` is set to `true`.
8282

83-
- `pluralCategories` (array of strings) – If the Language header is not set in
84-
the input, or if its Plural-Forms `nplurals` value is not 1, 2, or 6, this
85-
needs to be set to the pluralization category names to be used for the input
86-
enumerated categories if any message includes a plural form.
83+
- `pluralFunction` (function) – If your input file does not include a Plural-Forms
84+
header, or if for whatever reason you'd prefer to use your own, set this to be
85+
a stringifiable function that takes in a single variable, and returns the
86+
appropriate pluralisation category. Following the model used internally in
87+
[messageformat], the function variable should also include `cardinal` as a
88+
member array of its possible categories, in the order corresponding to the
89+
gettext pluralisation categories. This is relevant if you'd like to avoid the
90+
`new Function` constructor otherwise used to generate `pluralFunction`, or to
91+
allow for more fine-tuned categories than gettext allows, e.g. differentiating
92+
between the categories of `'1.0'` and `'1'`.
8793

8894
- `verbose` (boolean, default `false`) – If set to `true`, missing translations
8995
will cause warnings.
9096

9197
For more options, take a look at the [source](./index.js).
9298

93-
Both functions return an object `{ headers, translations }` where `headers`
94-
contains the raw contents of the input file's headers, with keys lower-cased, and
95-
`translations` is an object containing the MessageFormat strings keyed by their
96-
`msgid` and if used, `msgctxt`.
99+
Both functions return an object containing the following fields:
100+
101+
- `headers` (object) – The raw contents of the input file's headers, with keys
102+
lower-cased
103+
- `pluralFunction` (function) – An appropriate pluralisation function to use for
104+
the output translations, suitable to be used directly by [messageformat]. May
105+
be `null` if none was set in `options` and if the input did not include a
106+
Plural-Forms header.
107+
- `translations` (object) – An object containing the MessageFormat strings keyed
108+
by their `msgid` and if used, `msgctxt`.
97109

98110
[messageformat]: https://messageformat.github.io/
99111
[gettext-parser]: https://github.com/smhg/gettext-parser

index.js

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
const { mo, po } = require('gettext-parser')
2-
const pluralCategories = require('make-plural/umd/pluralCategories')
2+
const getPluralFunction = require('./plural-forms')
33

44
const defaultOptions = {
55
defaultCharset: null,
66
forceContext: false,
7-
pluralCategories: null,
7+
pluralFunction: null,
88
pluralVariablePattern: /%(?:\((\w+)\))?\w/,
99
replacements: [
1010
{
@@ -32,22 +32,8 @@ const defaultOptions = {
3232
verbose: false
3333
}
3434

35-
const getPluralCategories = ({ language, 'plural-forms': pluralForms }) => {
36-
if (language) {
37-
const pc = pluralCategories[language.replace(/[-_].*/, '')]
38-
if (pc) return pc.cardinal
39-
}
40-
const m = pluralForms && pluralForms.match(/^nplurals=(\d);/)
41-
switch (m && m[1]) {
42-
case '1': return ['other']
43-
case '2': return ['one', 'other']
44-
case '6': return ['zero', 'one', 'two', 'few', 'many', 'other']
45-
default: return null
46-
}
47-
}
48-
4935
const getMessageFormat = (
50-
{ pluralCategories, pluralVariablePattern, replacements, verbose },
36+
{ pluralFunction, pluralVariablePattern, replacements, verbose },
5137
{ msgid, msgid_plural, msgstr }
5238
) => {
5339
if (!msgid || !msgstr) return null
@@ -56,8 +42,8 @@ const getMessageFormat = (
5642
msgstr[0] = msgid
5743
}
5844
if (msgid_plural) {
59-
if (!pluralCategories) throw new Error('Plural categories not identified')
60-
for (let i = 1; i < pluralCategories.length; ++i) {
45+
if (!pluralFunction) throw new Error('Plural-Forms not defined')
46+
for (let i = 1; i < pluralFunction.cardinal.length; ++i) {
6147
if (!msgstr[i]) {
6248
if (verbose) console.warn('Plural translation not found:', msgid, i)
6349
msgstr[i] = msgid_plural
@@ -73,7 +59,7 @@ const getMessageFormat = (
7359
if (msgid_plural) {
7460
const m = msgid_plural.match(pluralVariablePattern)
7561
const pv = m && m[1] || '0'
76-
const pc = pluralCategories.map((c, i) => `${c}{${msgstr[i]}}`)
62+
const pc = pluralFunction.cardinal.map((c, i) => `${c}{${msgstr[i]}}`)
7763
return `{${pv}, plural, ${pc.join(' ')}}`
7864
}
7965
return msgstr[0]
@@ -82,7 +68,9 @@ const getMessageFormat = (
8268
const convert = (parse, input, options) => {
8369
options = Object.assign(defaultOptions, options)
8470
const { headers, translations } = parse(input, options.defaultCharset)
85-
if (!options.pluralCategories) options.pluralCategories = getPluralCategories(headers)
71+
if (!options.pluralFunction) {
72+
options.pluralFunction = getPluralFunction(headers['plural-forms'])
73+
}
8674
let hasContext = false
8775
for (const context in translations) {
8876
if (context) hasContext = true
@@ -95,6 +83,7 @@ const convert = (parse, input, options) => {
9583
}
9684
return {
9785
headers,
86+
pluralFunction: options.pluralFunction,
9887
translations: hasContext || options.forceContext ? translations : translations['']
9988
}
10089
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
"messageformat",
77
"gettext",
88
"po",
9+
"plural-forms",
910
"i18n"
1011
],
1112
"main": "index.js",
1213
"repository": "https://github.com/eemeli/gettext-to-messageformat",
1314
"author": "Eemeli Aro <eemeli@gmail.com>",
1415
"license": "MIT",
1516
"dependencies": {
16-
"gettext-parser": "^1.3.0",
17-
"make-plural": "^4.0.1"
17+
"gettext-parser": "^1.3.0"
1818
}
1919
}

plural-forms.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Examples from https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
3+
4+
Note: true == 1, false == 0
5+
6+
Plural-Forms: nplurals=1; plural=0;
7+
Plural-Forms: nplurals=2; plural=n != 1;
8+
Plural-Forms: nplurals=2; plural=n>1;
9+
Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;
10+
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;
11+
Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;
12+
Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;
13+
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2;
14+
Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;
15+
Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;
16+
*/
17+
18+
const validPlural = (plural) => {
19+
const words = plural.match(/\b\w+\b/g)
20+
if (!words) return null
21+
for (let i = 0; i < words.length; ++i) {
22+
const word = words[i]
23+
if (word !== 'n' && isNaN(Number(word))) return null
24+
}
25+
return plural.trim()
26+
}
27+
28+
const getPluralFunction = (pluralForms) => {
29+
if (!pluralForms) return null
30+
let nplurals
31+
let plural
32+
pluralForms.split(';').forEach(part => {
33+
const m = part.match(/^\s*(\w+)\s*=(.*)/)
34+
switch (m && m[1]) {
35+
case 'nplurals':
36+
nplurals = Number(m[2])
37+
break
38+
case 'plural':
39+
plural = validPlural(m[2])
40+
break
41+
}
42+
})
43+
if (!nplurals || !plural) throw new Error('Invalid plural-forms: ' + pluralForms)
44+
const pluralFunc = new Function('n', `return 'p' + Number(${plural})`)
45+
pluralFunc.cardinal = new Array(nplurals).fill().map((_, i) => 'p' + i)
46+
return pluralFunc
47+
}
48+
49+
module.exports = getPluralFunction

0 commit comments

Comments
 (0)