@lingui/format-po-gettext 4.0.0-next.2

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 ADDED
@@ -0,0 +1,92 @@
1
+ [![License][badge-license]][license]
2
+ [![Version][badge-version]][package]
3
+ [![Downloads][badge-downloads]][package]
4
+
5
+ # @lingui/format-po-gettext
6
+
7
+ > Read and write message catalogs in Gettext PO format with gettext-style plurals
8
+ >
9
+ > Converts ICU Plural expressions into native gettext plurals
10
+
11
+ `@lingui/format-po-gettext` is part of [LinguiJS][linguijs]. See the
12
+ [documentation][documentation] for all information, tutorials and examples.
13
+
14
+ > **Warning**
15
+ > This formatter is made for compatibility with TMS, which does not support ICU expressions in PO files.
16
+ >
17
+ > It does not support all features of LinguiJS and should be carefully considered over other formats.
18
+ >
19
+ > Not supported features (native gettext doesn't support this):
20
+ > - SelectOrdinal
21
+ > - Select
22
+ > - Nested ICU Expressions
23
+ > - Signed digits and fractions (-5, and 0.15) in plurals
24
+
25
+ ## Catalog example
26
+
27
+ ```po
28
+ #. js-lingui-id: WGI12K
29
+ #. js-lingui:icu=%7BanotherCount%2C+plural%2C+one+%7BSingular+case%7D+other+%7BCase+number+%7BanotherCount%7D%7D%7D&pluralize_on=anotherCount
30
+ msgid "Singular case"
31
+ msgid_plural "Case number {anotherCount}"
32
+ msgstr[0] "Singular case"
33
+ msgstr[1] "Case number {anotherCount}"
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ ```sh
39
+ npm install --save-dev @lingui/format-po-gettext
40
+ # yarn add --dev @lingui/format-po-gettext
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```js
46
+ // lingui.config.{js,ts}
47
+ import {formatter} from "@lingui/format-po-gettext"
48
+
49
+ export default {
50
+ [...]
51
+ format: formatter({lineNumbers: false}),
52
+ }
53
+ ```
54
+
55
+ Possible options:
56
+
57
+ ```ts
58
+ export type PoGettextFormatterOptions = {
59
+ /**
60
+ * Print places where message is used
61
+ *
62
+ * @default true
63
+ */
64
+ origins?: boolean
65
+
66
+ /**
67
+ * Print line numbers in origins
68
+ *
69
+ * @default true
70
+ */
71
+ lineNumbers?: boolean
72
+
73
+ /**
74
+ * Disable warning about unsupported `Select` feature encountered in catalogs
75
+ *
76
+ * @default false
77
+ */
78
+ disableSelectWarning?: boolean
79
+ }
80
+ ```
81
+
82
+ ## License
83
+
84
+ This package is licensed under [MIT][license] license.
85
+
86
+ [license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
87
+ [linguijs]: https://github.com/lingui/js-lingui
88
+ [documentation]: https://lingui.dev
89
+ [package]: https://www.npmjs.com/package/@lingui/format-po-gettext
90
+ [badge-downloads]: https://img.shields.io/npm/dw/@lingui/format-po-gettext.svg
91
+ [badge-version]: https://img.shields.io/npm/v/@lingui/format-po-gettext.svg
92
+ [badge-license]: https://img.shields.io/npm/l/@lingui/format-po-gettext.svg
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const parser = require('@messageformat/parser');
4
+ const pluralsCldr = require('plurals-cldr');
5
+ const PO = require('pofile');
6
+ const gettextPlurals = require('node-gettext/lib/plurals');
7
+ const api = require('@lingui/cli/api');
8
+ const formatPo = require('@lingui/format-po');
9
+
10
+ function stringifyICUCase(icuCase) {
11
+ return icuCase.tokens.map((token) => {
12
+ if (token.type === "content") {
13
+ return token.value;
14
+ } else if (token.type === "octothorpe") {
15
+ return "#";
16
+ } else if (token.type === "argument") {
17
+ return "{" + token.arg + "}";
18
+ } else {
19
+ console.warn(
20
+ `Unexpected token "${token}" while stringifying plural case "${icuCase}". Token will be ignored.`
21
+ );
22
+ return "";
23
+ }
24
+ }).join("");
25
+ }
26
+ const ICU_PLURAL_REGEX = /^{.*, plural, .*}$/;
27
+ const ICU_SELECT_REGEX = /^{.*, select(Ordinal)?, .*}$/;
28
+ const LINE_ENDINGS = /\r?\n/g;
29
+ const CTX_PREFIX = "js-lingui:";
30
+ function serializePlurals(item, message, id, isGeneratedId, options) {
31
+ const icuMessage = message.message;
32
+ if (!icuMessage) {
33
+ return;
34
+ }
35
+ const _simplifiedMessage = icuMessage.replace(LINE_ENDINGS, " ");
36
+ if (ICU_PLURAL_REGEX.test(_simplifiedMessage)) {
37
+ try {
38
+ const messageAst = parser.parse(icuMessage)[0];
39
+ if (messageAst.cases.some(
40
+ (icuCase) => icuCase.tokens.some((token) => token.type === "plural")
41
+ )) {
42
+ console.warn(
43
+ `Nested plurals cannot be expressed with gettext plurals. Message with key "%s" will not be saved correctly.`,
44
+ id
45
+ );
46
+ }
47
+ const ctx = new URLSearchParams({
48
+ pluralize_on: messageAst.arg
49
+ });
50
+ if (isGeneratedId) {
51
+ item.msgid = stringifyICUCase(messageAst.cases[0]);
52
+ item.msgid_plural = stringifyICUCase(
53
+ messageAst.cases[messageAst.cases.length - 1]
54
+ );
55
+ ctx.set("icu", icuMessage);
56
+ } else {
57
+ item.msgid_plural = id + "_plural";
58
+ }
59
+ ctx.sort();
60
+ item.extractedComments.push(CTX_PREFIX + ctx.toString());
61
+ if (message.translation?.length > 0) {
62
+ const ast = parser.parse(message.translation)[0];
63
+ if (ast.cases == null) {
64
+ console.warn(
65
+ `Found translation without plural cases for key "${id}". This likely means that a translated .po file misses multiple msgstr[] entries for the key. Translation found: "${message.translation}"`
66
+ );
67
+ item.msgstr = [message.translation];
68
+ } else {
69
+ item.msgstr = ast.cases.map(stringifyICUCase);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ console.error(`Error parsing message ICU for key "${id}":`, e);
74
+ }
75
+ } else {
76
+ if (!options.disableSelectWarning && ICU_SELECT_REGEX.test(_simplifiedMessage)) {
77
+ console.warn(
78
+ `ICU 'select' and 'selectOrdinal' formats cannot be expressed natively in gettext format. Item with key "%s" will be included in the catalog as raw ICU message. To disable this warning, include '{ disableSelectWarning: true }' in the config's 'formatOptions'`,
79
+ id
80
+ );
81
+ }
82
+ item.msgstr = [message.translation];
83
+ }
84
+ return item;
85
+ }
86
+ const getPluralCases = (lang) => {
87
+ const [correctLang] = lang.split(/[-_]/g);
88
+ const gettextPluralsInfo = gettextPlurals[correctLang];
89
+ return gettextPluralsInfo?.examples.map(
90
+ (pluralCase) => pluralsCldr(correctLang, pluralCase.sample)
91
+ );
92
+ };
93
+ const convertPluralsToICU = (item, pluralForms, lang) => {
94
+ const translationCount = item.msgstr.length;
95
+ const messageKey = item.msgid;
96
+ if (translationCount <= 1 && !item.msgid_plural) {
97
+ return;
98
+ }
99
+ if (!item.msgid_plural) {
100
+ console.warn(
101
+ `Multiple translations for item with key "%s" but missing 'msgid_plural' in catalog "${lang}". This is not supported and the plural cases will be ignored.`,
102
+ messageKey
103
+ );
104
+ return;
105
+ }
106
+ const contextComment = item.extractedComments.find((comment) => comment.startsWith(CTX_PREFIX))?.substr(CTX_PREFIX.length);
107
+ const ctx = new URLSearchParams(contextComment);
108
+ if (contextComment != null) {
109
+ item.extractedComments = item.extractedComments.filter(
110
+ (comment) => !comment.startsWith(CTX_PREFIX)
111
+ );
112
+ }
113
+ const storedICU = ctx.get("icu");
114
+ if (storedICU != null) {
115
+ item.msgid = storedICU;
116
+ }
117
+ if (item.msgstr.every((str) => str.length === 0)) {
118
+ return;
119
+ }
120
+ if (pluralForms == null) {
121
+ console.warn(
122
+ `Multiple translations for item with key "%s" in language "${lang}", but no plural cases were found. This prohibits the translation of .po plurals into ICU plurals. Pluralization will not work for this key.`,
123
+ messageKey
124
+ );
125
+ return;
126
+ }
127
+ const pluralCount = pluralForms.length;
128
+ if (translationCount > pluralCount) {
129
+ console.warn(
130
+ `More translations provided (${translationCount}) for item with key "%s" in language "${lang}" than there are plural cases available (${pluralCount}). This will result in not all translations getting picked up.`,
131
+ messageKey
132
+ );
133
+ }
134
+ const pluralClauses = item.msgstr.map((str, index) => pluralForms[index] + " {" + str + "}").join(" ");
135
+ let pluralizeOn = ctx.get("pluralize_on");
136
+ if (!pluralizeOn) {
137
+ console.warn(
138
+ `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".`,
139
+ messageKey
140
+ );
141
+ pluralizeOn = "count";
142
+ }
143
+ item.msgstr = ["{" + pluralizeOn + ", plural, " + pluralClauses + "}"];
144
+ };
145
+ function formatter(options = {}) {
146
+ options = {
147
+ origins: true,
148
+ lineNumbers: true,
149
+ ...options
150
+ };
151
+ const formatter2 = formatPo.formatter(options);
152
+ return {
153
+ catalogExtension: ".po",
154
+ templateExtension: ".pot",
155
+ parse(content) {
156
+ const po = PO.parse(content);
157
+ let pluralForms = getPluralCases(po.headers.Language);
158
+ po.items.forEach((item) => {
159
+ convertPluralsToICU(item, pluralForms, po.headers.Language);
160
+ });
161
+ return formatter2.parse(po.toString());
162
+ },
163
+ serialize(catalog, ctx) {
164
+ const po = PO.parse(formatter2.serialize(catalog, ctx));
165
+ po.items = po.items.map((item) => {
166
+ const isGeneratedId = !item.flags["explicit-id"];
167
+ const id = isGeneratedId ? api.generateMessageId(item.msgid, item.msgctxt) : item.msgid;
168
+ const message = catalog[id];
169
+ return serializePlurals(item, message, id, isGeneratedId, options);
170
+ });
171
+ return po.toString();
172
+ }
173
+ };
174
+ }
175
+
176
+ exports.formatter = formatter;
@@ -0,0 +1,17 @@
1
+ import { CatalogType } from '@lingui/conf';
2
+ import { PoFormatterOptions } from '@lingui/format-po';
3
+
4
+ type PoGettextFormatterOptions = PoFormatterOptions & {
5
+ disableSelectWarning?: boolean;
6
+ };
7
+ declare function formatter(options?: PoGettextFormatterOptions): {
8
+ catalogExtension: string;
9
+ templateExtension: string;
10
+ parse(content: string): CatalogType;
11
+ serialize(catalog: CatalogType, ctx: {
12
+ locale: string;
13
+ existing: string;
14
+ }): string;
15
+ };
16
+
17
+ export { PoGettextFormatterOptions, formatter };
@@ -0,0 +1,174 @@
1
+ import { parse } from '@messageformat/parser';
2
+ import pluralsCldr from 'plurals-cldr';
3
+ import PO from 'pofile';
4
+ import gettextPlurals from 'node-gettext/lib/plurals';
5
+ import { generateMessageId } from '@lingui/cli/api';
6
+ import { formatter as formatter$1 } from '@lingui/format-po';
7
+
8
+ function stringifyICUCase(icuCase) {
9
+ return icuCase.tokens.map((token) => {
10
+ if (token.type === "content") {
11
+ return token.value;
12
+ } else if (token.type === "octothorpe") {
13
+ return "#";
14
+ } else if (token.type === "argument") {
15
+ return "{" + token.arg + "}";
16
+ } else {
17
+ console.warn(
18
+ `Unexpected token "${token}" while stringifying plural case "${icuCase}". Token will be ignored.`
19
+ );
20
+ return "";
21
+ }
22
+ }).join("");
23
+ }
24
+ const ICU_PLURAL_REGEX = /^{.*, plural, .*}$/;
25
+ const ICU_SELECT_REGEX = /^{.*, select(Ordinal)?, .*}$/;
26
+ const LINE_ENDINGS = /\r?\n/g;
27
+ const CTX_PREFIX = "js-lingui:";
28
+ function serializePlurals(item, message, id, isGeneratedId, options) {
29
+ const icuMessage = message.message;
30
+ if (!icuMessage) {
31
+ return;
32
+ }
33
+ const _simplifiedMessage = icuMessage.replace(LINE_ENDINGS, " ");
34
+ if (ICU_PLURAL_REGEX.test(_simplifiedMessage)) {
35
+ try {
36
+ const messageAst = parse(icuMessage)[0];
37
+ if (messageAst.cases.some(
38
+ (icuCase) => icuCase.tokens.some((token) => token.type === "plural")
39
+ )) {
40
+ console.warn(
41
+ `Nested plurals cannot be expressed with gettext plurals. Message with key "%s" will not be saved correctly.`,
42
+ id
43
+ );
44
+ }
45
+ const ctx = new URLSearchParams({
46
+ pluralize_on: messageAst.arg
47
+ });
48
+ if (isGeneratedId) {
49
+ item.msgid = stringifyICUCase(messageAst.cases[0]);
50
+ item.msgid_plural = stringifyICUCase(
51
+ messageAst.cases[messageAst.cases.length - 1]
52
+ );
53
+ ctx.set("icu", icuMessage);
54
+ } else {
55
+ item.msgid_plural = id + "_plural";
56
+ }
57
+ ctx.sort();
58
+ item.extractedComments.push(CTX_PREFIX + ctx.toString());
59
+ if (message.translation?.length > 0) {
60
+ const ast = parse(message.translation)[0];
61
+ if (ast.cases == null) {
62
+ console.warn(
63
+ `Found translation without plural cases for key "${id}". This likely means that a translated .po file misses multiple msgstr[] entries for the key. Translation found: "${message.translation}"`
64
+ );
65
+ item.msgstr = [message.translation];
66
+ } else {
67
+ item.msgstr = ast.cases.map(stringifyICUCase);
68
+ }
69
+ }
70
+ } catch (e) {
71
+ console.error(`Error parsing message ICU for key "${id}":`, e);
72
+ }
73
+ } else {
74
+ if (!options.disableSelectWarning && ICU_SELECT_REGEX.test(_simplifiedMessage)) {
75
+ console.warn(
76
+ `ICU 'select' and 'selectOrdinal' formats cannot be expressed natively in gettext format. Item with key "%s" will be included in the catalog as raw ICU message. To disable this warning, include '{ disableSelectWarning: true }' in the config's 'formatOptions'`,
77
+ id
78
+ );
79
+ }
80
+ item.msgstr = [message.translation];
81
+ }
82
+ return item;
83
+ }
84
+ const getPluralCases = (lang) => {
85
+ const [correctLang] = lang.split(/[-_]/g);
86
+ const gettextPluralsInfo = gettextPlurals[correctLang];
87
+ return gettextPluralsInfo?.examples.map(
88
+ (pluralCase) => pluralsCldr(correctLang, pluralCase.sample)
89
+ );
90
+ };
91
+ const convertPluralsToICU = (item, pluralForms, lang) => {
92
+ const translationCount = item.msgstr.length;
93
+ const messageKey = item.msgid;
94
+ if (translationCount <= 1 && !item.msgid_plural) {
95
+ return;
96
+ }
97
+ if (!item.msgid_plural) {
98
+ console.warn(
99
+ `Multiple translations for item with key "%s" but missing 'msgid_plural' in catalog "${lang}". This is not supported and the plural cases will be ignored.`,
100
+ messageKey
101
+ );
102
+ return;
103
+ }
104
+ const contextComment = item.extractedComments.find((comment) => comment.startsWith(CTX_PREFIX))?.substr(CTX_PREFIX.length);
105
+ const ctx = new URLSearchParams(contextComment);
106
+ if (contextComment != null) {
107
+ item.extractedComments = item.extractedComments.filter(
108
+ (comment) => !comment.startsWith(CTX_PREFIX)
109
+ );
110
+ }
111
+ const storedICU = ctx.get("icu");
112
+ if (storedICU != null) {
113
+ item.msgid = storedICU;
114
+ }
115
+ if (item.msgstr.every((str) => str.length === 0)) {
116
+ return;
117
+ }
118
+ if (pluralForms == null) {
119
+ console.warn(
120
+ `Multiple translations for item with key "%s" in language "${lang}", but no plural cases were found. This prohibits the translation of .po plurals into ICU plurals. Pluralization will not work for this key.`,
121
+ messageKey
122
+ );
123
+ return;
124
+ }
125
+ const pluralCount = pluralForms.length;
126
+ if (translationCount > pluralCount) {
127
+ console.warn(
128
+ `More translations provided (${translationCount}) for item with key "%s" in language "${lang}" than there are plural cases available (${pluralCount}). This will result in not all translations getting picked up.`,
129
+ messageKey
130
+ );
131
+ }
132
+ const pluralClauses = item.msgstr.map((str, index) => pluralForms[index] + " {" + str + "}").join(" ");
133
+ let pluralizeOn = ctx.get("pluralize_on");
134
+ if (!pluralizeOn) {
135
+ 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".`,
137
+ messageKey
138
+ );
139
+ pluralizeOn = "count";
140
+ }
141
+ item.msgstr = ["{" + pluralizeOn + ", plural, " + pluralClauses + "}"];
142
+ };
143
+ function formatter(options = {}) {
144
+ options = {
145
+ origins: true,
146
+ lineNumbers: true,
147
+ ...options
148
+ };
149
+ const formatter2 = formatter$1(options);
150
+ return {
151
+ catalogExtension: ".po",
152
+ templateExtension: ".pot",
153
+ parse(content) {
154
+ const po = PO.parse(content);
155
+ let pluralForms = getPluralCases(po.headers.Language);
156
+ po.items.forEach((item) => {
157
+ convertPluralsToICU(item, pluralForms, po.headers.Language);
158
+ });
159
+ return formatter2.parse(po.toString());
160
+ },
161
+ serialize(catalog, ctx) {
162
+ const po = PO.parse(formatter2.serialize(catalog, ctx));
163
+ po.items = po.items.map((item) => {
164
+ const isGeneratedId = !item.flags["explicit-id"];
165
+ const id = isGeneratedId ? generateMessageId(item.msgid, item.msgctxt) : item.msgid;
166
+ const message = catalog[id];
167
+ return serializePlurals(item, message, id, isGeneratedId, options);
168
+ });
169
+ return po.toString();
170
+ }
171
+ };
172
+ }
173
+
174
+ export { formatter };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@lingui/format-po-gettext",
3
+ "version": "4.0.0-next.2",
4
+ "description": "Gettext PO format with gettext-style plurals for Lingui Catalogs",
5
+ "main": "./dist/po-gettext.cjs",
6
+ "module": "./dist/po-gettext.mjs",
7
+ "types": "./dist/po-gettext.d.ts",
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "i18n",
11
+ "lingui-formatter",
12
+ "lingui-format",
13
+ "gettext",
14
+ "po",
15
+ "pot",
16
+ "internationalization",
17
+ "i10n",
18
+ "localization",
19
+ "i9n",
20
+ "translation"
21
+ ],
22
+ "scripts": {
23
+ "build": "rimraf ./dist && unbuild",
24
+ "stub": "unbuild --stub"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/lingui/js-lingui.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/lingui/js-lingui/issues"
32
+ },
33
+ "engines": {
34
+ "node": ">=16.0.0"
35
+ },
36
+ "files": [
37
+ "LICENSE",
38
+ "README.md",
39
+ "dist/"
40
+ ],
41
+ "dependencies": {
42
+ "@lingui/cli": "4.0.0-next.2",
43
+ "@lingui/conf": "4.0.0-next.2",
44
+ "@lingui/format-po": "4.0.0-next.2",
45
+ "@messageformat/parser": "^5.0.0",
46
+ "node-gettext": "^3.0.0",
47
+ "plurals-cldr": "^2.0.1",
48
+ "pofile": "^1.1.4"
49
+ },
50
+ "devDependencies": {
51
+ "@lingui/jest-mocks": "workspace:^",
52
+ "unbuild": "^1.1.2"
53
+ }
54
+ }