@lingui/format-po-gettext 4.12.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  > **Warning**
15
15
  > This formatter is made for compatibility with translation management systems, which do not support ICU expressions in PO files.
16
- >
16
+ >
17
17
  > It does not support all features of LinguiJS and should be carefully considered over other formats.
18
18
  >
19
19
  > Not supported features (native gettext doesn't support this):
@@ -72,10 +72,17 @@ export type PoGettextFormatterOptions = {
72
72
 
73
73
  /**
74
74
  * Disable warning about unsupported `Select` feature encountered in catalogs
75
- *
75
+ *
76
76
  * @default false
77
77
  */
78
78
  disableSelectWarning?: boolean
79
+
80
+ /**
81
+ * Overrides the default prefix for icu and plural comments in the final PO catalog.
82
+ *
83
+ * @default "js-lingui:"
84
+ */
85
+ customICUPrefix?: string
79
86
  }
80
87
  ```
81
88
 
@@ -6,13 +6,72 @@ const PO = require('pofile');
6
6
  const gettextPlurals = require('node-gettext/lib/plurals');
7
7
  const generateMessageId = require('@lingui/message-utils/generateMessageId');
8
8
  const formatPo = require('@lingui/format-po');
9
+ const cardinals = require('cldr-core/supplemental/plurals.json');
9
10
 
10
11
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
11
12
 
12
13
  const pluralsCldr__default = /*#__PURE__*/_interopDefaultCompat(pluralsCldr);
13
14
  const PO__default = /*#__PURE__*/_interopDefaultCompat(PO);
14
15
  const gettextPlurals__default = /*#__PURE__*/_interopDefaultCompat(gettextPlurals);
16
+ const cardinals__default = /*#__PURE__*/_interopDefaultCompat(cardinals);
15
17
 
18
+ function renameKeys(rules) {
19
+ const result = {};
20
+ Object.keys(rules).forEach((k) => {
21
+ const newKey = k.match(/[^-]+$/)[0];
22
+ result[newKey] = rules[k];
23
+ });
24
+ return result;
25
+ }
26
+ function fillRange(value) {
27
+ let [start, end] = value.split("~");
28
+ const decimals = (start.split(".")[1] || "").length;
29
+ let mult = Math.pow(10, decimals);
30
+ const startNum = Number(start);
31
+ const endNum = Number(end);
32
+ let range = Array(Math.ceil(endNum * mult - startNum * mult + 1)).fill(0).map((v, idx) => (idx + startNum * mult) / mult);
33
+ let last = range[range.length - 1];
34
+ if (endNum !== last) {
35
+ throw new Error(`Range create error for ${value}: last value is ${last}`);
36
+ }
37
+ return range.map((v) => Number(v));
38
+ }
39
+ function createSamples(src) {
40
+ let result = [];
41
+ src.replace(/…/, "").trim().replace(/,$/, "").split(",").map(function(val) {
42
+ return val.trim();
43
+ }).forEach((val) => {
44
+ if (val.indexOf("~") !== -1) {
45
+ result = result.concat(fillRange(val));
46
+ } else {
47
+ result.push(Number(val));
48
+ }
49
+ });
50
+ return result;
51
+ }
52
+ function createLocaleTest(rules) {
53
+ let result = {};
54
+ Object.keys(rules).forEach((form) => {
55
+ let samples = rules[form].split(/@integer|@decimal/).slice(1);
56
+ result[form] = [];
57
+ samples.forEach((sample) => {
58
+ result[form] = result[form].concat(createSamples(sample));
59
+ });
60
+ });
61
+ return result;
62
+ }
63
+ function getCldrPluralSamples() {
64
+ const pluralRules = {};
65
+ Object.entries(cardinals__default.supplemental["plurals-type-cardinal"]).forEach(
66
+ ([loc, ruleset]) => {
67
+ let rules = renameKeys(ruleset);
68
+ pluralRules[loc.toLowerCase()] = createLocaleTest(rules);
69
+ }
70
+ );
71
+ return pluralRules;
72
+ }
73
+
74
+ const cldrSamples = getCldrPluralSamples();
16
75
  function stringifyICUCase(icuCase) {
17
76
  return icuCase.tokens.map((token) => {
18
77
  if (token.type === "content") {
@@ -32,9 +91,10 @@ function stringifyICUCase(icuCase) {
32
91
  const ICU_PLURAL_REGEX = /^{.*, plural, .*}$/;
33
92
  const ICU_SELECT_REGEX = /^{.*, select(Ordinal)?, .*}$/;
34
93
  const LINE_ENDINGS = /\r?\n/g;
35
- const CTX_PREFIX = "js-lingui:";
94
+ const DEFAULT_CTX_PREFIX = "js-lingui:";
36
95
  function serializePlurals(item, message, id, isGeneratedId, options) {
37
96
  const icuMessage = message.message;
97
+ const ctxPrefix = options.customICUPrefix || DEFAULT_CTX_PREFIX;
38
98
  if (!icuMessage) {
39
99
  return item;
40
100
  }
@@ -63,7 +123,7 @@ function serializePlurals(item, message, id, isGeneratedId, options) {
63
123
  item.msgid_plural = id + "_plural";
64
124
  }
65
125
  ctx.sort();
66
- item.extractedComments.push(CTX_PREFIX + ctx.toString());
126
+ item.extractedComments.push(ctxPrefix + ctx.toString());
67
127
  if (message.translation?.length > 0) {
68
128
  const ast = parser.parse(message.translation)[0];
69
129
  if (ast.cases == null) {
@@ -89,14 +149,53 @@ function serializePlurals(item, message, id, isGeneratedId, options) {
89
149
  }
90
150
  return item;
91
151
  }
92
- const getPluralCases = (lang) => {
152
+ const getPluralCases = (lang, pluralFormsHeader) => {
153
+ let gettextPluralsInfo;
154
+ if (pluralFormsHeader) {
155
+ gettextPluralsInfo = parsePluralFormsFn(pluralFormsHeader);
156
+ }
93
157
  const [correctLang] = lang.split(/[-_]/g);
94
- const gettextPluralsInfo = gettextPlurals__default[correctLang];
95
- return gettextPluralsInfo?.examples.map(
96
- (pluralCase) => pluralsCldr__default(correctLang, pluralCase.sample)
97
- );
158
+ if (!gettextPluralsInfo) {
159
+ gettextPluralsInfo = gettextPlurals__default[correctLang];
160
+ }
161
+ if (!gettextPluralsInfo) {
162
+ if (lang !== "pseudo") {
163
+ console.warn(
164
+ `No plural rules found for language "${lang}". Please add a Plural-Forms header.`
165
+ );
166
+ }
167
+ return void 0;
168
+ }
169
+ const cases = [...Array(pluralsCldr__default.forms(correctLang).length)];
170
+ for (let form of pluralsCldr__default.forms(correctLang)) {
171
+ const samples = cldrSamples[correctLang][form];
172
+ const pluralForm = Number(
173
+ gettextPluralsInfo.pluralsFunc(Number(samples[0]))
174
+ );
175
+ cases[pluralForm] = form;
176
+ }
177
+ return cases;
98
178
  };
99
- const convertPluralsToICU = (item, pluralForms, lang) => {
179
+ function parsePluralFormsFn(pluralFormsHeader) {
180
+ const [npluralsExpr, expr] = pluralFormsHeader.split(";");
181
+ try {
182
+ const nplurals = new Function(npluralsExpr + "; return nplurals;")();
183
+ const pluralsFunc = new Function(
184
+ "n",
185
+ expr + "; return plural;"
186
+ );
187
+ return {
188
+ nplurals,
189
+ pluralsFunc
190
+ };
191
+ } catch (e) {
192
+ console.warn(
193
+ `Plural-Forms header has incorrect value: ${pluralFormsHeader}`
194
+ );
195
+ return void 0;
196
+ }
197
+ }
198
+ const convertPluralsToICU = (item, pluralForms, lang, ctxPrefix = DEFAULT_CTX_PREFIX) => {
100
199
  const translationCount = item.msgstr.length;
101
200
  const messageKey = item.msgid;
102
201
  if (translationCount <= 1 && !item.msgid_plural) {
@@ -109,11 +208,11 @@ const convertPluralsToICU = (item, pluralForms, lang) => {
109
208
  );
110
209
  return;
111
210
  }
112
- const contextComment = item.extractedComments.find((comment) => comment.startsWith(CTX_PREFIX))?.substr(CTX_PREFIX.length);
211
+ const contextComment = item.extractedComments.find((comment) => comment.startsWith(ctxPrefix))?.substring(ctxPrefix.length);
113
212
  const ctx = new URLSearchParams(contextComment);
114
213
  if (contextComment != null) {
115
214
  item.extractedComments = item.extractedComments.filter(
116
- (comment) => !comment.startsWith(CTX_PREFIX)
215
+ (comment) => !comment.startsWith(ctxPrefix)
117
216
  );
118
217
  }
119
218
  const storedICU = ctx.get("icu");
@@ -137,11 +236,13 @@ const convertPluralsToICU = (item, pluralForms, lang) => {
137
236
  messageKey
138
237
  );
139
238
  }
140
- const pluralClauses = item.msgstr.map((str, index) => pluralForms[index] + " {" + str + "}").join(" ");
239
+ const pluralClauses = item.msgstr.map(
240
+ (str, index) => pluralForms[index] ? pluralForms[index] + " {" + str + "}" : ""
241
+ ).join(" ");
141
242
  let pluralizeOn = ctx.get("pluralize_on");
142
243
  if (!pluralizeOn) {
143
244
  console.warn(
144
- `Unable to determine plural placeholder name for item with key "%s" in language "${lang}" (should be stored in a comment starting with "#. ${CTX_PREFIX}"), assuming "count".`,
245
+ `Unable to determine plural placeholder name for item with key "%s" in language "${lang}" (should be stored in a comment starting with "#. ${ctxPrefix}"), assuming "count".`,
145
246
  messageKey
146
247
  );
147
248
  pluralizeOn = "count";
@@ -160,9 +261,17 @@ function formatter(options = {}) {
160
261
  templateExtension: ".pot",
161
262
  parse(content, ctx) {
162
263
  const po = PO__default.parse(content);
163
- let pluralForms = getPluralCases(po.headers.Language);
264
+ let pluralForms = getPluralCases(
265
+ po.headers.Language,
266
+ po.headers["Plural-Forms"]
267
+ );
164
268
  po.items.forEach((item) => {
165
- convertPluralsToICU(item, pluralForms, po.headers.Language);
269
+ convertPluralsToICU(
270
+ item,
271
+ pluralForms,
272
+ po.headers.Language,
273
+ options.customICUPrefix
274
+ );
166
275
  });
167
276
  return formatter2.parse(po.toString(), ctx);
168
277
  },
@@ -3,6 +3,7 @@ import { PoFormatterOptions } from '@lingui/format-po';
3
3
 
4
4
  type PoGettextFormatterOptions = PoFormatterOptions & {
5
5
  disableSelectWarning?: boolean;
6
+ customICUPrefix?: string;
6
7
  };
7
8
  declare function formatter(options?: PoGettextFormatterOptions): CatalogFormatter;
8
9
 
@@ -3,6 +3,7 @@ import { PoFormatterOptions } from '@lingui/format-po';
3
3
 
4
4
  type PoGettextFormatterOptions = PoFormatterOptions & {
5
5
  disableSelectWarning?: boolean;
6
+ customICUPrefix?: string;
6
7
  };
7
8
  declare function formatter(options?: PoGettextFormatterOptions): CatalogFormatter;
8
9
 
@@ -3,6 +3,7 @@ import { PoFormatterOptions } from '@lingui/format-po';
3
3
 
4
4
  type PoGettextFormatterOptions = PoFormatterOptions & {
5
5
  disableSelectWarning?: boolean;
6
+ customICUPrefix?: string;
6
7
  };
7
8
  declare function formatter(options?: PoGettextFormatterOptions): CatalogFormatter;
8
9
 
@@ -4,7 +4,65 @@ import PO from 'pofile';
4
4
  import gettextPlurals from 'node-gettext/lib/plurals';
5
5
  import { generateMessageId } from '@lingui/message-utils/generateMessageId';
6
6
  import { formatter as formatter$1 } from '@lingui/format-po';
7
+ import cardinals from 'cldr-core/supplemental/plurals.json';
7
8
 
9
+ function renameKeys(rules) {
10
+ const result = {};
11
+ Object.keys(rules).forEach((k) => {
12
+ const newKey = k.match(/[^-]+$/)[0];
13
+ result[newKey] = rules[k];
14
+ });
15
+ return result;
16
+ }
17
+ function fillRange(value) {
18
+ let [start, end] = value.split("~");
19
+ const decimals = (start.split(".")[1] || "").length;
20
+ let mult = Math.pow(10, decimals);
21
+ const startNum = Number(start);
22
+ const endNum = Number(end);
23
+ let range = Array(Math.ceil(endNum * mult - startNum * mult + 1)).fill(0).map((v, idx) => (idx + startNum * mult) / mult);
24
+ let last = range[range.length - 1];
25
+ if (endNum !== last) {
26
+ throw new Error(`Range create error for ${value}: last value is ${last}`);
27
+ }
28
+ return range.map((v) => Number(v));
29
+ }
30
+ function createSamples(src) {
31
+ let result = [];
32
+ src.replace(/…/, "").trim().replace(/,$/, "").split(",").map(function(val) {
33
+ return val.trim();
34
+ }).forEach((val) => {
35
+ if (val.indexOf("~") !== -1) {
36
+ result = result.concat(fillRange(val));
37
+ } else {
38
+ result.push(Number(val));
39
+ }
40
+ });
41
+ return result;
42
+ }
43
+ function createLocaleTest(rules) {
44
+ let result = {};
45
+ Object.keys(rules).forEach((form) => {
46
+ let samples = rules[form].split(/@integer|@decimal/).slice(1);
47
+ result[form] = [];
48
+ samples.forEach((sample) => {
49
+ result[form] = result[form].concat(createSamples(sample));
50
+ });
51
+ });
52
+ return result;
53
+ }
54
+ function getCldrPluralSamples() {
55
+ const pluralRules = {};
56
+ Object.entries(cardinals.supplemental["plurals-type-cardinal"]).forEach(
57
+ ([loc, ruleset]) => {
58
+ let rules = renameKeys(ruleset);
59
+ pluralRules[loc.toLowerCase()] = createLocaleTest(rules);
60
+ }
61
+ );
62
+ return pluralRules;
63
+ }
64
+
65
+ const cldrSamples = getCldrPluralSamples();
8
66
  function stringifyICUCase(icuCase) {
9
67
  return icuCase.tokens.map((token) => {
10
68
  if (token.type === "content") {
@@ -24,9 +82,10 @@ function stringifyICUCase(icuCase) {
24
82
  const ICU_PLURAL_REGEX = /^{.*, plural, .*}$/;
25
83
  const ICU_SELECT_REGEX = /^{.*, select(Ordinal)?, .*}$/;
26
84
  const LINE_ENDINGS = /\r?\n/g;
27
- const CTX_PREFIX = "js-lingui:";
85
+ const DEFAULT_CTX_PREFIX = "js-lingui:";
28
86
  function serializePlurals(item, message, id, isGeneratedId, options) {
29
87
  const icuMessage = message.message;
88
+ const ctxPrefix = options.customICUPrefix || DEFAULT_CTX_PREFIX;
30
89
  if (!icuMessage) {
31
90
  return item;
32
91
  }
@@ -55,7 +114,7 @@ function serializePlurals(item, message, id, isGeneratedId, options) {
55
114
  item.msgid_plural = id + "_plural";
56
115
  }
57
116
  ctx.sort();
58
- item.extractedComments.push(CTX_PREFIX + ctx.toString());
117
+ item.extractedComments.push(ctxPrefix + ctx.toString());
59
118
  if (message.translation?.length > 0) {
60
119
  const ast = parse(message.translation)[0];
61
120
  if (ast.cases == null) {
@@ -81,14 +140,53 @@ function serializePlurals(item, message, id, isGeneratedId, options) {
81
140
  }
82
141
  return item;
83
142
  }
84
- const getPluralCases = (lang) => {
143
+ const getPluralCases = (lang, pluralFormsHeader) => {
144
+ let gettextPluralsInfo;
145
+ if (pluralFormsHeader) {
146
+ gettextPluralsInfo = parsePluralFormsFn(pluralFormsHeader);
147
+ }
85
148
  const [correctLang] = lang.split(/[-_]/g);
86
- const gettextPluralsInfo = gettextPlurals[correctLang];
87
- return gettextPluralsInfo?.examples.map(
88
- (pluralCase) => pluralsCldr(correctLang, pluralCase.sample)
89
- );
149
+ if (!gettextPluralsInfo) {
150
+ gettextPluralsInfo = gettextPlurals[correctLang];
151
+ }
152
+ if (!gettextPluralsInfo) {
153
+ if (lang !== "pseudo") {
154
+ console.warn(
155
+ `No plural rules found for language "${lang}". Please add a Plural-Forms header.`
156
+ );
157
+ }
158
+ return void 0;
159
+ }
160
+ const cases = [...Array(pluralsCldr.forms(correctLang).length)];
161
+ for (let form of pluralsCldr.forms(correctLang)) {
162
+ const samples = cldrSamples[correctLang][form];
163
+ const pluralForm = Number(
164
+ gettextPluralsInfo.pluralsFunc(Number(samples[0]))
165
+ );
166
+ cases[pluralForm] = form;
167
+ }
168
+ return cases;
90
169
  };
91
- const convertPluralsToICU = (item, pluralForms, lang) => {
170
+ function parsePluralFormsFn(pluralFormsHeader) {
171
+ const [npluralsExpr, expr] = pluralFormsHeader.split(";");
172
+ try {
173
+ const nplurals = new Function(npluralsExpr + "; return nplurals;")();
174
+ const pluralsFunc = new Function(
175
+ "n",
176
+ expr + "; return plural;"
177
+ );
178
+ return {
179
+ nplurals,
180
+ pluralsFunc
181
+ };
182
+ } catch (e) {
183
+ console.warn(
184
+ `Plural-Forms header has incorrect value: ${pluralFormsHeader}`
185
+ );
186
+ return void 0;
187
+ }
188
+ }
189
+ const convertPluralsToICU = (item, pluralForms, lang, ctxPrefix = DEFAULT_CTX_PREFIX) => {
92
190
  const translationCount = item.msgstr.length;
93
191
  const messageKey = item.msgid;
94
192
  if (translationCount <= 1 && !item.msgid_plural) {
@@ -101,11 +199,11 @@ const convertPluralsToICU = (item, pluralForms, lang) => {
101
199
  );
102
200
  return;
103
201
  }
104
- const contextComment = item.extractedComments.find((comment) => comment.startsWith(CTX_PREFIX))?.substr(CTX_PREFIX.length);
202
+ const contextComment = item.extractedComments.find((comment) => comment.startsWith(ctxPrefix))?.substring(ctxPrefix.length);
105
203
  const ctx = new URLSearchParams(contextComment);
106
204
  if (contextComment != null) {
107
205
  item.extractedComments = item.extractedComments.filter(
108
- (comment) => !comment.startsWith(CTX_PREFIX)
206
+ (comment) => !comment.startsWith(ctxPrefix)
109
207
  );
110
208
  }
111
209
  const storedICU = ctx.get("icu");
@@ -129,11 +227,13 @@ const convertPluralsToICU = (item, pluralForms, lang) => {
129
227
  messageKey
130
228
  );
131
229
  }
132
- const pluralClauses = item.msgstr.map((str, index) => pluralForms[index] + " {" + str + "}").join(" ");
230
+ const pluralClauses = item.msgstr.map(
231
+ (str, index) => pluralForms[index] ? pluralForms[index] + " {" + str + "}" : ""
232
+ ).join(" ");
133
233
  let pluralizeOn = ctx.get("pluralize_on");
134
234
  if (!pluralizeOn) {
135
235
  console.warn(
136
- `Unable to determine plural placeholder name for item with key "%s" in language "${lang}" (should be stored in a comment starting with "#. ${CTX_PREFIX}"), assuming "count".`,
236
+ `Unable to determine plural placeholder name for item with key "%s" in language "${lang}" (should be stored in a comment starting with "#. ${ctxPrefix}"), assuming "count".`,
137
237
  messageKey
138
238
  );
139
239
  pluralizeOn = "count";
@@ -152,9 +252,17 @@ function formatter(options = {}) {
152
252
  templateExtension: ".pot",
153
253
  parse(content, ctx) {
154
254
  const po = PO.parse(content);
155
- let pluralForms = getPluralCases(po.headers.Language);
255
+ let pluralForms = getPluralCases(
256
+ po.headers.Language,
257
+ po.headers["Plural-Forms"]
258
+ );
156
259
  po.items.forEach((item) => {
157
- convertPluralsToICU(item, pluralForms, po.headers.Language);
260
+ convertPluralsToICU(
261
+ item,
262
+ pluralForms,
263
+ po.headers.Language,
264
+ options.customICUPrefix
265
+ );
158
266
  });
159
267
  return formatter2.parse(po.toString(), ctx);
160
268
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingui/format-po-gettext",
3
- "version": "4.12.0",
3
+ "version": "4.14.0",
4
4
  "description": "Gettext PO format with gettext-style plurals for Lingui Catalogs",
5
5
  "main": "./dist/po-gettext.cjs",
6
6
  "module": "./dist/po-gettext.mjs",
@@ -41,10 +41,11 @@
41
41
  "dist/"
42
42
  ],
43
43
  "dependencies": {
44
- "@lingui/conf": "4.12.0",
45
- "@lingui/format-po": "4.12.0",
46
- "@lingui/message-utils": "4.12.0",
44
+ "@lingui/conf": "4.14.0",
45
+ "@lingui/format-po": "4.14.0",
46
+ "@lingui/message-utils": "4.14.0",
47
47
  "@messageformat/parser": "^5.0.0",
48
+ "cldr-core": "^45.0.0",
48
49
  "node-gettext": "^3.0.0",
49
50
  "plurals-cldr": "^2.0.1",
50
51
  "pofile": "^1.1.4"
@@ -55,5 +56,5 @@
55
56
  "tsd": "^0.28.0",
56
57
  "unbuild": "2.0.0"
57
58
  },
58
- "gitHead": "83a6ea03f22d6284bdcf067bde7b406c4e3520bc"
59
+ "gitHead": "8dcb749688e2f452f85fc0eaad4d033a44975b42"
59
60
  }