Skip to content

Commit 415b90e

Browse files
authored
feat: Implement gettext plurals for PO files (#677)
1 parent d4c24bf commit 415b90e

10 files changed

Lines changed: 1089 additions & 6 deletions

File tree

docs/ref/catalog-formats.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,63 @@ The advantages of this format are:
3636
3737
.. _gettext: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
3838

39+
.. _po-gettext:
40+
41+
PO File with gettext Plurals
42+
============================
43+
44+
When using localization backends that don't understand the ICU plural syntax exported by the default `po` formatter,
45+
**po-gettext** can be used to read and write to PO files using gettext-native plurals.
46+
47+
This is how the regular PO format exports plurals:
48+
49+
.. code-block:: po
50+
51+
msgid "{count, plural, one {Message} other {Messages}}"
52+
msgstr "{count, plural, one {Message} other {Messages}}"
53+
54+
With `po-gettext`, plural messages are exported in the following way, depending on wheter an explicit ID is set:
55+
56+
.. code-block:: po
57+
58+
# Message with custom ID "my_message" that is pluralized on property "someCount".
59+
#
60+
# Notice that 'msgid_plural' was generad by appending a '_plural' suffix.
61+
#. js-lingui:pluralize_on=someCount
62+
msgid "my_message"
63+
msgid_plural "my_message_plural"
64+
msgstr[0] "Singular case"
65+
msgstr[1] "Case number {someCount}"
66+
67+
# Message without custom ID that is pluralized on property "anotherCount".
68+
#
69+
# Notice how 'msgid' and 'msgid_plural' were extracted from original message.
70+
#
71+
# To allow matching this PO item to the appropriate catalog entry when deserializing,
72+
# the original ICU message is also stored in the generated comment.
73+
#. js-lingui:icu=%7BanotherCount%2C+plural%2C+one+%7BSingular+case%7D+other+%7BCase+number+%7BanotherCount%7D%7D%7D&pluralize_on=anotherCount
74+
msgid "Singular case"
75+
msgid_plural "Case number {anotherCount}"
76+
msgstr[0] "Singular case"
77+
msgstr[1] "Case number {anotherCount}"
78+
79+
Note that this format comes with several caveats and should therefore only be used if using ICU plurals in PO files is
80+
not an option:
81+
82+
- Nested/multiple plurals in one message as shown in :jsmacro:`plural` are not supported as they cannot be expressed
83+
with gettext plurals. Messages containing nested/multiple formats will not be output correctly.
84+
85+
- :jsmacro:`select` and :jsmacro:`selectOrdinal` cannot be expressed with gettext plurals, but the original ICU format
86+
is still saved to the `msgid`/`msgstr` properties. To disable the warning that this might not be the expected
87+
behavior, include :code:`{ disableSelectWarning: true }` in the :conf:`formatOptions`.
88+
89+
- Source/development languages with more than two plurals could experience difficulties when no custom IDs are used,
90+
as gettext cannot have more than two plurals cases identifying an item (:code:`msgid` and :code:`msgid_plural`).
91+
92+
- Gettext doesn't support plurals for negative and fractional numbers even though some languages have special rules
93+
for these cases.
94+
95+
3996
JSON
4097
====
4198

docs/ref/conf.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ Gettext PO file:
295295
msgid "MessageID"
296296
msgstr "Translated Message"
297297
298+
po-gettext
299+
^^^^^^^^^^
300+
301+
Uses PO files but with gettext-style plurals, see :ref:`po-gettext`.
302+
298303
minimal
299304
^^^^^^^
300305

packages/cli/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,20 @@
5454
"messageformat-parser": "^4.1.3",
5555
"micromatch": "4.0.2",
5656
"mkdirp": "^1.0.4",
57+
"node-gettext": "^3.0.0",
5758
"normalize-path": "^3.0.0",
5859
"ora": "^5.1.0",
5960
"papaparse": "^5.3.0",
60-
"pofile": "^1.0.11",
61+
"plurals-cldr": "^1.0.4",
62+
"pofile": "^1.1.0",
6163
"pseudolocale": "^1.1.0",
6264
"ramda": "^0.27.1"
6365
},
6466
"devDependencies": {
6567
"@types/micromatch": "^4.0.1",
6668
"@types/normalize-path": "^3.0.0",
6769
"@types/papaparse": "^5.2.3",
70+
"@types/plurals-cldr": "^1.0.1",
6871
"mockdate": "^3.0.2",
6972
"typescript": "^4.0.3"
7073
},
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`po-gettext format should convert ICU plural messages to gettext plurals 1`] = `
4+
msgid ""
5+
msgstr ""
6+
"POT-Creation-Date: 2018-08-27 10:00+0000\\n"
7+
"Mime-Version: 1.0\\n"
8+
"Content-Type: text/plain; charset=utf-8\\n"
9+
"Content-Transfer-Encoding: 8bit\\n"
10+
"X-Generator: @lingui/cli\\n"
11+
"Language: en\\n"
12+
13+
#. js-lingui:pluralize_on=count
14+
msgid "message_with_id_and_octothorpe"
15+
msgid_plural "message_with_id_and_octothorpe_plural"
16+
msgstr[0] "Singular"
17+
msgstr[1] "Number is #"
18+
19+
#. This is a comment by the developers about how the content must be localized.
20+
#. js-lingui:pluralize_on=someCount
21+
msgid "message_with_id"
22+
msgid_plural "message_with_id_plural"
23+
msgstr[0] "Singular case with id"
24+
msgstr[1] "Case number {someCount} with id"
25+
26+
#. js-lingui:icu=%7BanotherCount%2C+plural%2C+one+%7BSingular+case%7D+other+%7BCase+number+%7BanotherCount%7D%7D%7D&pluralize_on=anotherCount
27+
msgid "Singular case"
28+
msgid_plural "Case number {anotherCount}"
29+
msgstr[0] "Singular case"
30+
msgstr[1] "Case number {anotherCount}"
31+
32+
#. js-lingui:pluralize_on=count
33+
msgid "message_with_id_but_without_translation"
34+
msgid_plural "message_with_id_but_without_translation_plural"
35+
msgstr[0] ""
36+
msgstr[1] ""
37+
38+
#. js-lingui:icu=%7Bcount%2C+plural%2C+one+%7BSingular+automatic+id+no+translation%7D+other+%7BPlural+%7Bcount%7D+automatic+id+no+translation%7D%7D&pluralize_on=count
39+
msgid "Singular automatic id no translation"
40+
msgid_plural "Plural {count} automatic id no translation"
41+
msgstr[0] ""
42+
msgstr[1] ""
43+
44+
`;
45+
46+
exports[`po-gettext format should convert gettext plurals to ICU plural messages 1`] = `
47+
Object {
48+
message_with_id: Object {
49+
comments: Array [],
50+
extractedComments: Array [],
51+
flags: Array [],
52+
obsolete: false,
53+
origin: Array [],
54+
translation: {someCount, plural, one {Singular case} other {Case number {someCount}}},
55+
},
56+
message_with_id_but_without_translation: Object {
57+
comments: Array [],
58+
extractedComments: Array [
59+
Comment made by the developers.,
60+
],
61+
flags: Array [],
62+
obsolete: false,
63+
origin: Array [],
64+
translation: ,
65+
},
66+
{anotherCount, plural, one {Singular case} other {Case number {anotherCount}}}: Object {
67+
comments: Array [],
68+
extractedComments: Array [],
69+
flags: Array [],
70+
obsolete: false,
71+
origin: Array [],
72+
translation: {anotherCount, plural, one {Singular case} other {Case number {anotherCount}}},
73+
},
74+
{count, plural, one {Singular} other {Plural}}: Object {
75+
comments: Array [],
76+
extractedComments: Array [],
77+
flags: Array [],
78+
obsolete: false,
79+
origin: Array [],
80+
translation: ,
81+
},
82+
}
83+
`;
84+
85+
exports[`po-gettext format should correct badly used comments 1`] = `
86+
Object {
87+
withDescriptionAndComments: Object {
88+
comments: Array [
89+
Translator comment,
90+
],
91+
extractedComments: Array [
92+
Single description only,
93+
Second description?,
94+
],
95+
flags: Array [],
96+
obsolete: false,
97+
origin: Array [],
98+
translation: Second description joins translator comments,
99+
},
100+
withMultipleDescriptions: Object {
101+
comments: Array [],
102+
extractedComments: Array [
103+
First description,
104+
Second comment,
105+
Third comment,
106+
],
107+
flags: Array [],
108+
obsolete: false,
109+
origin: Array [],
110+
translation: Extra comments are separated from the first description line,
111+
},
112+
}
113+
`;
114+
115+
exports[`po-gettext format should read catalog in pofile format 1`] = `
116+
Object {
117+
obsolete: Object {
118+
comments: Array [],
119+
extractedComments: Array [],
120+
flags: Array [],
121+
obsolete: true,
122+
origin: Array [],
123+
translation: Is marked as obsolete,
124+
},
125+
static: Object {
126+
comments: Array [],
127+
extractedComments: Array [],
128+
flags: Array [],
129+
obsolete: false,
130+
origin: Array [],
131+
translation: Static message,
132+
},
133+
veryLongString: Object {
134+
comments: Array [],
135+
extractedComments: Array [],
136+
flags: Array [],
137+
obsolete: false,
138+
origin: Array [],
139+
translation: One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked. "What's happened to me?" he thought. It wasn't a dream. His room, a proper human,
140+
},
141+
withComments: Object {
142+
comments: Array [
143+
Translator comment,
144+
This one might come from developer,
145+
],
146+
extractedComments: Array [],
147+
flags: Array [],
148+
obsolete: false,
149+
origin: Array [],
150+
translation: Support translator comments separately,
151+
},
152+
withDescription: Object {
153+
comments: Array [],
154+
extractedComments: Array [
155+
Description is comment from developers to translators,
156+
],
157+
flags: Array [],
158+
obsolete: false,
159+
origin: Array [],
160+
translation: Message with description,
161+
},
162+
withFlags: Object {
163+
comments: Array [],
164+
extractedComments: Array [],
165+
flags: Array [
166+
fuzzy,
167+
otherFlag,
168+
],
169+
obsolete: false,
170+
origin: Array [],
171+
translation: Keeps any flags that are defined,
172+
},
173+
withMultipleOrigins: Object {
174+
comments: Array [],
175+
extractedComments: Array [],
176+
flags: Array [],
177+
obsolete: false,
178+
origin: Array [
179+
Array [
180+
src/App.js,
181+
4,
182+
],
183+
Array [
184+
src/Component.js,
185+
2,
186+
],
187+
],
188+
translation: Message with multiple origin,
189+
},
190+
withOrigin: Object {
191+
comments: Array [],
192+
extractedComments: Array [],
193+
flags: Array [],
194+
obsolete: false,
195+
origin: Array [
196+
Array [
197+
src/App.js,
198+
4,
199+
],
200+
],
201+
translation: Message with origin,
202+
},
203+
}
204+
`;
205+
206+
exports[`po-gettext format should throw away additional msgstr if present 1`] = `
207+
Object {
208+
withMultipleTranslations: Object {
209+
comments: Array [],
210+
extractedComments: Array [],
211+
flags: Array [],
212+
obsolete: false,
213+
origin: Array [],
214+
translation: This is just fine,
215+
},
216+
}
217+
`;
218+
219+
exports[`po-gettext format should write catalog in pofile format 1`] = `
220+
msgid ""
221+
msgstr ""
222+
"POT-Creation-Date: 2018-08-27 10:00+0000\\n"
223+
"Mime-Version: 1.0\\n"
224+
"Content-Type: text/plain; charset=utf-8\\n"
225+
"Content-Transfer-Encoding: 8bit\\n"
226+
"X-Generator: @lingui/cli\\n"
227+
"Language: en\\n"
228+
229+
msgid "static"
230+
msgstr "Static message"
231+
232+
#: src/App.js:4
233+
msgid "withOrigin"
234+
msgstr "Message with origin"
235+
236+
#: src/App.js:4
237+
#: src/Component.js:2
238+
msgid "withMultipleOrigins"
239+
msgstr "Message with multiple origin"
240+
241+
#. Description is comment from developers to translators
242+
msgid "withDescription"
243+
msgstr "Message with description"
244+
245+
# Translator comment
246+
# This one might come from developer
247+
msgid "withComments"
248+
msgstr "Support translator comments separately"
249+
250+
#~ msgid "obsolete"
251+
#~ msgstr "Obsolete message"
252+
253+
#, fuzzy,otherFlag
254+
msgid "withFlags"
255+
msgstr "Keeps any flags that are defined"
256+
257+
msgid "veryLongString"
258+
msgstr "One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked. \\"What's happened to me?\\" he thought. It wasn't a dream. His room, a proper human"
259+
260+
`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
msgid ""
2+
msgstr ""
3+
"POT-Creation-Date: 2018-08-27 10:00+0000\n"
4+
"Mime-Version: 1.0\n"
5+
"Content-Type: text/plain; charset=utf-8\n"
6+
"Content-Transfer-Encoding: 8bit\n"
7+
"X-Generator: @lingui/cli\n"
8+
"Language: en\n"
9+
10+
#. js-lingui:pluralize_on=someCount
11+
msgid "message_with_id"
12+
msgid_plural "message_with_id_plural"
13+
msgstr[0] "Singular case"
14+
msgstr[1] "Case number {someCount}"
15+
16+
#. js-lingui:icu=%7BanotherCount%2C+plural%2C+one+%7BSingular+case%7D+other+%7BCase+number+%7BanotherCount%7D%7D%7D&pluralize_on=anotherCount
17+
msgid "Singular case"
18+
msgid_plural "Case number {anotherCount}"
19+
msgstr[0] "Singular case"
20+
msgstr[1] "Case number {anotherCount}"
21+
22+
#. Comment made by the developers.
23+
#. js-lingui:pluralize_on=count
24+
msgid "message_with_id_but_without_translation"
25+
msgid_plural "message_with_id_but_without_translation_plural"
26+
msgstr[0] ""
27+
msgstr[1] ""
28+
29+
#. js-lingui:icu=%7Bcount%2C+plural%2C+one+%7BSingular%7D+other+%7BPlural%7D%7D&pluralize_on=count
30+
msgid "Singular"
31+
msgid_plural "Plural"
32+
msgstr[0] ""
33+
msgstr[1] ""

0 commit comments

Comments
 (0)