locize-cli 7.8.2 → 7.10.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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  Project versioning adheres to [Semantic Versioning](http://semver.org/).
6
6
  Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
7
7
 
8
+ ## [7.10.0](https://github.com/locize/locize-cli/compare/v7.9.1...v7.10.0) - 2022-03-15
9
+
10
+ - gettext_i18next: try to detect v4 format
11
+
12
+ ## [7.9.1](https://github.com/locize/locize-cli/compare/v7.9.0...v7.9.1) - 2022-03-02
13
+
14
+ - xliff: fix combined plural keys
15
+
16
+
17
+ ## [7.9.0](https://github.com/locize/locize-cli/compare/v7.8.2...v7.9.0) - 2022-03-01
18
+
19
+ - xliff: detect i18n format and merge pluralforms if necessary
20
+
21
+
8
22
  ## [7.8.2](https://github.com/locize/locize-cli/compare/v7.8.1...v7.8.2) - 2022-02-28
9
23
 
10
24
  - update dependencies
@@ -0,0 +1,154 @@
1
+ const jsyaml = require('js-yaml');
2
+
3
+ const delimiter = {
4
+ i18next: '_',
5
+ i18njs: '.'
6
+ };
7
+
8
+ const detectFormat = (keys) => {
9
+ const i18nextMatches = keys.filter((k) => k.indexOf(delimiter.i18next) > 0).length;
10
+ const i18njsMatches = keys.filter((k) => k.indexOf(delimiter.i18njs) > 0).length;
11
+ if (i18nextMatches > i18njsMatches) {
12
+ return 'i18next';
13
+ }
14
+ if (i18nextMatches < i18njsMatches) {
15
+ return 'i18njs';
16
+ }
17
+ };
18
+
19
+ const getBaseKey = (delimiter) => (k) => {
20
+ const parts = k.split(delimiter);
21
+ parts.pop();
22
+ const baseKey = parts.join(delimiter);
23
+ return baseKey;
24
+ };
25
+
26
+ const uniq = (value, index, self) => self.indexOf(value) === index;
27
+
28
+ const stringify = (o) => {
29
+ let str = jsyaml.dump(o);
30
+ const subKeys = Object.keys(o);
31
+ subKeys.forEach((sk) => {
32
+ if (isNaN(sk)) {
33
+ str = str.replace(new RegExp(`^(?:${sk}: )+`, 'm'), `{${sk}}: `);
34
+ } else {
35
+ str = str.replace(new RegExp(`^(?:'${sk}': )+`, 'm'), `{${sk}}: `);
36
+ }
37
+ });
38
+ return str;
39
+ };
40
+
41
+ const transformKeys = (segments, baseKeys, toMerge, deli) => {
42
+ baseKeys.forEach((bk) => {
43
+ const asObj = toMerge[bk].reduce((mem, k) => {
44
+ const subKey = k.substring((bk + deli).length);
45
+ // special handling for i18next v3
46
+ if (deli === delimiter.i18next && subKey === 'plural' && segments[bk]) {
47
+ mem['__'] = segments[bk];
48
+ delete segments[bk];
49
+ }
50
+ mem[subKey] = segments[k];
51
+ return mem;
52
+ }, {});
53
+ if (Object.keys(asObj).length > 0) {
54
+ const value = stringify(asObj);
55
+ segments[`${bk}__#locize.com/combinedSubkey`] = value;
56
+ toMerge[bk].forEach((k) => {
57
+ delete segments[k];
58
+ });
59
+ }
60
+ });
61
+ return segments;
62
+ };
63
+
64
+ // CLDR
65
+ const pluralForms = [
66
+ 'zero',
67
+ 'one',
68
+ 'two',
69
+ 'few',
70
+ 'many',
71
+ 'other'
72
+ ];
73
+
74
+ const endsWithPluralForm = (k) => !!pluralForms.find((f) => k.endsWith(`.${f}`)) || !!pluralForms.find((f) => k.endsWith(`_${f}`)) || /_\d+$/.test(k) || k.endsWith('_plural');
75
+
76
+ const prepareExport = (refRes, trgRes) => {
77
+ const refLngKeys = Object.keys(refRes);
78
+ const trgLngKeys = Object.keys(trgRes);
79
+
80
+ const nonMatchInRef = refLngKeys.filter((k) => trgLngKeys.indexOf(k) < 0 && endsWithPluralForm(k));
81
+ const nonMatchInTrg = trgLngKeys.filter((k) => refLngKeys.indexOf(k) < 0 && endsWithPluralForm(k));
82
+
83
+ const allMatches = nonMatchInRef.concat(nonMatchInTrg);
84
+
85
+ const format = detectFormat(allMatches);
86
+ if (!format) return { ref: refRes, trg: trgRes };
87
+
88
+ const nonMatchBaseKeysInRef = nonMatchInRef.map(getBaseKey(delimiter[format])).filter(uniq);
89
+ const nonMatchBaseKeysInTrg = nonMatchInTrg.map(getBaseKey(delimiter[format])).filter(uniq);
90
+ const nonMatchBaseKeys = nonMatchBaseKeysInRef.concat(nonMatchBaseKeysInTrg).filter(uniq);
91
+
92
+ const toMergeInRef = nonMatchBaseKeys.reduce((mem, bk) => {
93
+ mem[bk] = refLngKeys.filter((k) => k.indexOf(bk + delimiter[format]) === 0);
94
+ return mem;
95
+ }, {});
96
+ const toMergeInTrg = nonMatchBaseKeys.reduce((mem, bk) => {
97
+ mem[bk] = trgLngKeys.filter((k) => k.indexOf(bk + delimiter[format]) === 0);
98
+ return mem;
99
+ }, {});
100
+
101
+ let falseFlags = nonMatchBaseKeysInRef.filter((k) => toMergeInRef[k].length < 2 && (!toMergeInTrg[k] || toMergeInTrg[k].length < 2));
102
+ falseFlags = falseFlags.concat(nonMatchBaseKeysInTrg.filter((k) => toMergeInTrg[k].length < 2 && (!toMergeInRef[k] || toMergeInRef[k].length < 2)));
103
+ falseFlags.forEach((k) => {
104
+ delete toMergeInRef[k];
105
+ delete toMergeInTrg[k];
106
+ nonMatchBaseKeys.splice(nonMatchBaseKeys.indexOf(k), 1);
107
+ });
108
+
109
+ const transformedRef = transformKeys(refRes, nonMatchBaseKeys, toMergeInRef, delimiter[format]);
110
+ const transformedTrg = transformKeys(trgRes, nonMatchBaseKeys, toMergeInTrg, delimiter[format]);
111
+ return { ref: transformedRef, trg: transformedTrg };
112
+ };
113
+
114
+ const skRegex = new RegExp('^(?:{(.+)})+', 'gm');
115
+ const parse = (s) => {
116
+ let matchArray;
117
+ while ((matchArray = skRegex.exec(s)) !== null) {
118
+ const [match, sk] = matchArray;
119
+ if (isNaN(sk)) {
120
+ s = s.replace(new RegExp(`^(?:${match}: )+`, 'm'), `${sk}: `);
121
+ } else {
122
+ const escapedMatch = match.replace('{', '\\{').replace('}', '\\}');
123
+ s = s.replace(new RegExp(`^(?:${escapedMatch}: )+`, 'm'), `${sk}: `);
124
+ }
125
+ }
126
+ return jsyaml.load(s);
127
+ };
128
+
129
+ const prepareImport = (resources) => {
130
+ const keys = Object.keys(resources);
131
+ keys.forEach((k) => {
132
+ if (k.indexOf('__#locize.com/combinedSubkey') > -1) {
133
+ const baseKey = k.substring(0, k.indexOf('__#locize.com/combinedSubkey'));
134
+ if (resources[k]) {
135
+ const parsed = parse(resources[k]);
136
+ Object.keys(parsed).map((sk) => {
137
+ const skVal = parsed[sk];
138
+ resources[`${baseKey}_${sk}`] = skVal;
139
+ if (sk === '__') {
140
+ resources[baseKey] = resources[`${baseKey}_${sk}`];
141
+ delete resources[`${baseKey}_${sk}`];
142
+ }
143
+ });
144
+ delete resources[k];
145
+ }
146
+ }
147
+ });
148
+ return resources;
149
+ };
150
+
151
+ module.exports = {
152
+ prepareExport,
153
+ prepareImport
154
+ };
@@ -16,6 +16,7 @@ const unflatten = require('./unflatten');
16
16
  const getRemoteNamespace = require('./getRemoteNamespace');
17
17
  const removeUndefinedFromArrays = require('./removeUndefinedFromArrays');
18
18
  const shouldUnflatten = require('./shouldUnflatten');
19
+ const prepareCombinedExport = require('./combineSubkeyPreprocessor').prepareExport;
19
20
 
20
21
  const convertToDesiredFormat = (
21
22
  opt,
@@ -55,11 +56,13 @@ const convertToDesiredFormat = (
55
56
  }
56
57
  if (opt.format === 'po_i18next' || opt.format === 'gettext_i18next') {
57
58
  const flatData = flatten(data);
59
+ const compatibilityJSON = !!Object.keys(flatData).find((k) => /_(zero|one|two|few|many|other)/.test(k)) && 'v4';
58
60
  const gettextOpt = {
59
61
  project: 'locize',
60
62
  language: lng,
61
63
  potCreationDate: lastModified,
62
- poRevisionDate: lastModified
64
+ poRevisionDate: lastModified,
65
+ compatibilityJSON
63
66
  };
64
67
  cb(null, i18next2po(lng, flatData, gettextOpt));
65
68
  return;
@@ -172,7 +175,8 @@ const convertToDesiredFormat = (
172
175
  opt.getNamespace(opt, opt.referenceLanguage, namespace, (err, refNs) => {
173
176
  if (err) return cb(err);
174
177
 
175
- fn(opt.referenceLanguage, lng, refNs, flatten(data), namespace, cb);
178
+ const prepared = prepareCombinedExport(refNs, flatten(data));
179
+ fn(opt.referenceLanguage, lng, prepared.ref, prepared.trg, namespace, cb);
176
180
  });
177
181
  return;
178
182
  }
@@ -14,6 +14,7 @@ const tmx2js = require('tmexchange/cjs/tmx2js');
14
14
  const laravel2js = require('laravelphp/cjs/laravel2js');
15
15
  const javaProperties = require('@js.properties/properties');
16
16
  const flatten = require('flat');
17
+ const prepareCombinedImport = require('./combineSubkeyPreprocessor').prepareImport;
17
18
 
18
19
  const convertToFlatFormat = (opt, data, lng, cb) => {
19
20
  if (!cb) {
@@ -39,7 +40,9 @@ const convertToFlatFormat = (opt, data, lng, cb) => {
39
40
  }
40
41
  if (opt.format === 'po_i18next' || opt.format === 'gettext_i18next') {
41
42
  try {
42
- const ret = po2i18next(data.toString());
43
+ const potxt = data.toString();
44
+ const compatibilityJSON = /msgctxt "(zero|one|two|few|many|other)"/.test(potxt) && 'v4';
45
+ const ret = po2i18next(potxt, { compatibilityJSON });
43
46
  cb(null, flatten(ret));
44
47
  } catch (err) {
45
48
  cb(err);
@@ -171,34 +174,34 @@ const convertToFlatFormat = (opt, data, lng, cb) => {
171
174
  res.resources = res.resources || {};
172
175
  const ns = Object.keys(res.resources)[0];
173
176
  const orgRes = res.resources[ns] || res.resources;
174
- function checkForContext(nsRes) {
177
+ function checkForPostProcessing(nsRes) {
175
178
  Object.keys(nsRes).forEach((k) => {
176
179
  if (orgRes[k].note && (typeof nsRes[k] === 'string' || !nsRes[k])) {
177
180
  nsRes[k] = {
178
181
  value: nsRes[k],
179
182
  context: {
180
183
  text: orgRes[k].note,
181
- },
184
+ }
182
185
  };
183
186
  }
184
187
  });
185
- return nsRes;
188
+ return prepareCombinedImport(nsRes);
186
189
  }
187
190
  if (!res.targetLanguage) {
188
191
  sourceOfjs(res, (err, ret) => {
189
192
  if (err) return cb(err);
190
- cb(null, checkForContext(ret));
193
+ cb(null, checkForPostProcessing(ret));
191
194
  });
192
195
  } else {
193
196
  let ret = targetOfjs(res);
194
- if (lng !== opt.referenceLanguage) return cb(null, checkForContext(ret));
197
+ if (lng !== opt.referenceLanguage) return cb(null, checkForPostProcessing(ret));
195
198
  ret = ret || {};
196
199
  const keys = Object.keys(ret);
197
- if (keys.length === 0) return cb(null, checkForContext(ret));
200
+ if (keys.length === 0) return cb(null, checkForPostProcessing(ret));
198
201
  const allEmpty = keys.filter((k) => ret[k] !== '').length === 0;
199
- if (!allEmpty) return cb(null, checkForContext(ret));
202
+ if (!allEmpty) return cb(null, checkForPostProcessing(ret));
200
203
  ret = sourceOfjs(res);
201
- cb(null, checkForContext(ret));
204
+ cb(null, checkForPostProcessing(ret));
202
205
  }
203
206
  });
204
207
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locize-cli",
3
- "version": "7.8.2",
3
+ "version": "7.10.0",
4
4
  "description": "locize cli to import locales",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "diff": "5.0.0",
17
17
  "flat": "5.0.2",
18
18
  "fluent_conv": "3.1.0",
19
- "gettext-converter": "1.2.0",
19
+ "gettext-converter": "1.2.2",
20
20
  "https-proxy-agent": "5.0.0",
21
21
  "ini": "2.0.0",
22
22
  "js-yaml": "4.1.0",
@@ -29,11 +29,11 @@
29
29
  "strings-file": "0.0.5",
30
30
  "tmexchange": "2.0.4",
31
31
  "xliff": "6.0.3",
32
- "xlsx": "0.18.2"
32
+ "xlsx": "0.18.3"
33
33
  },
34
34
  "devDependencies": {
35
35
  "eslint": "7.32.0",
36
- "gh-release": "6.0.1",
36
+ "gh-release": "6.0.2",
37
37
  "pkg": "5.5.2"
38
38
  },
39
39
  "scripts": {