locize-cli 7.8.0 → 7.9.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,21 @@ 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.9.0](https://github.com/locize/locize-cli/compare/v7.8.2...v7.9.0) - 2022-03-01
9
+
10
+ - xliff: detect i18n format and merge pluralforms if necessary
11
+
12
+
13
+ ## [7.8.2](https://github.com/locize/locize-cli/compare/v7.8.1...v7.8.2) - 2022-02-28
14
+
15
+ - update dependencies
16
+
17
+
18
+ ## [7.8.1](https://github.com/locize/locize-cli/compare/v7.8.0...v7.8.1) - 2022-02-10
19
+
20
+ - sync: optimize handling if pathmask does not include namespace
21
+
22
+
8
23
  ## [7.8.0](https://github.com/locize/locize-cli/compare/v7.7.2...v7.8.0) - 2022-01-13
9
24
 
10
25
  - sync: introduce --delete-remote-namespace option
package/README.md CHANGED
@@ -91,7 +91,7 @@ locize download --project-id my-project-id-93e1-442a-ab35-24331fa294ba --ver lat
91
91
  By using the sync command, you can keep your existing code setup and synchronize the translations with locize.
92
92
  An example on how this could look like can be seen in [this tutorial](https://github.com/locize/react-tutorial#step-1---keep-existing-code-setup-but-synchronize-with-locize).
93
93
 
94
- **⚠️ Since the remote source are the published translations, make sure the desired version is set to standard publish mode, or alternatively [publish](#publish-version) that version before you execute the cli command. ⚠️**
94
+ **⚠️ Since the remote source are the published translations, make sure the desired version is set to standard/auto publish mode, or alternatively [publish](#publish-version) that version before you execute the cli command. ⚠️**
95
95
 
96
96
  ### Step 1: Go near to your translation files
97
97
 
@@ -0,0 +1,136 @@
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, i) => {
32
+ str = str.replace(new RegExp(`^(?:${sk}: )+`, 'm'), `{${sk}}: `);
33
+ });
34
+ return str;
35
+ };
36
+
37
+ const transformKeys = (segments, baseKeys, toMerge, deli) => {
38
+ baseKeys.forEach((bk) => {
39
+ const asObj = toMerge[bk].reduce((mem, k) => {
40
+ const subKey = k.substring((bk + deli).length);
41
+ mem[subKey] = segments[k];
42
+ return mem;
43
+ }, {});
44
+ if (Object.keys(asObj).length > 0) {
45
+ const value = stringify(asObj);
46
+ segments[`${bk}__#locize.com/combinedSubkey`] = value;
47
+ toMerge[bk].forEach((k) => {
48
+ delete segments[k];
49
+ });
50
+ }
51
+ });
52
+ return segments;
53
+ };
54
+
55
+ // CLDR
56
+ const pluralForms = [
57
+ 'zero',
58
+ 'one',
59
+ 'two',
60
+ 'few',
61
+ 'many',
62
+ 'other'
63
+ ];
64
+
65
+ const endsWithPluralForm = (k) => !!pluralForms.find((f) => k.endsWith(`.${f}`)) || !!pluralForms.find((f) => k.endsWith(`_${f}`)) || /_\d+$/.test(k) || k.endsWith('_plural');
66
+
67
+ const prepareExport = (refRes, trgRes) => {
68
+ const refLngKeys = Object.keys(refRes);
69
+ const trgLngKeys = Object.keys(trgRes);
70
+
71
+ const nonMatchInRef = refLngKeys.filter((k) => trgLngKeys.indexOf(k) < 0 && endsWithPluralForm(k));
72
+ const nonMatchInTrg = trgLngKeys.filter((k) => refLngKeys.indexOf(k) < 0 && endsWithPluralForm(k));
73
+
74
+ const allMatches = nonMatchInRef.concat(nonMatchInTrg);
75
+
76
+ const format = detectFormat(allMatches);
77
+ if (!format) return { ref: refRes, trg: trgRes };
78
+
79
+ const nonMatchBaseKeysInRef = nonMatchInRef.map(getBaseKey(delimiter[format])).filter(uniq);
80
+ const nonMatchBaseKeysInTrg = nonMatchInTrg.map(getBaseKey(delimiter[format])).filter(uniq);
81
+ const nonMatchBaseKeys = nonMatchBaseKeysInRef.concat(nonMatchBaseKeysInTrg).filter(uniq);
82
+
83
+ const toMergeInRef = nonMatchBaseKeys.reduce((mem, bk) => {
84
+ mem[bk] = refLngKeys.filter((k) => k.indexOf(bk + delimiter[format]) === 0);
85
+ return mem;
86
+ }, {});
87
+ const toMergeInTrg = nonMatchBaseKeys.reduce((mem, bk) => {
88
+ mem[bk] = trgLngKeys.filter((k) => k.indexOf(bk + delimiter[format]) === 0);
89
+ return mem;
90
+ }, {});
91
+
92
+ let falseFlags = nonMatchBaseKeysInRef.filter((k) => toMergeInRef[k].length < 2 && (!toMergeInTrg[k] || toMergeInTrg[k].length < 2));
93
+ falseFlags = falseFlags.concat(nonMatchBaseKeysInTrg.filter((k) => toMergeInTrg[k].length < 2 && (!toMergeInRef[k] || toMergeInRef[k].length < 2)));
94
+ falseFlags.forEach((k) => {
95
+ delete toMergeInRef[k];
96
+ delete toMergeInTrg[k];
97
+ nonMatchBaseKeys.splice(nonMatchBaseKeys.indexOf(k), 1);
98
+ });
99
+
100
+ const transformedRef = transformKeys(refRes, nonMatchBaseKeys, toMergeInRef, delimiter[format]);
101
+ const transformedTrg = transformKeys(trgRes, nonMatchBaseKeys, toMergeInTrg, delimiter[format]);
102
+ return { ref: transformedRef, trg: transformedTrg };
103
+ };
104
+
105
+ const skRegex = new RegExp('^(?:{(.+)})+', 'gm');
106
+ const parse = (s) => {
107
+ let matchArray;
108
+ while ((matchArray = skRegex.exec(s)) !== null) {
109
+ const [match, sk] = matchArray;
110
+ s = s.replace(new RegExp(`^(?:${match}: )+`, 'm'), `${sk}: `);
111
+ }
112
+ return jsyaml.load(s);
113
+ };
114
+
115
+ const prepareImport = (resources) => {
116
+ const keys = Object.keys(resources);
117
+ keys.forEach((k) => {
118
+ if (k.indexOf('__#locize.com/combinedSubkey') > -1) {
119
+ const baseKey = k.substring(0, k.indexOf('__#locize.com/combinedSubkey'));
120
+ if (resources[k]) {
121
+ const parsed = parse(resources[k]);
122
+ Object.keys(parsed).map((sk) => {
123
+ const skVal = parsed[sk];
124
+ resources[`${baseKey}_${sk}`] = skVal;
125
+ });
126
+ delete resources[k];
127
+ }
128
+ }
129
+ });
130
+ return resources;
131
+ };
132
+
133
+ module.exports = {
134
+ prepareExport,
135
+ prepareImport
136
+ };
@@ -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,
@@ -172,7 +173,8 @@ const convertToDesiredFormat = (
172
173
  opt.getNamespace(opt, opt.referenceLanguage, namespace, (err, refNs) => {
173
174
  if (err) return cb(err);
174
175
 
175
- fn(opt.referenceLanguage, lng, refNs, flatten(data), namespace, cb);
176
+ const prepared = prepareCombinedExport(refNs, flatten(data));
177
+ fn(opt.referenceLanguage, lng, prepared.ref, prepared.trg, namespace, cb);
176
178
  });
177
179
  return;
178
180
  }
@@ -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) {
@@ -171,34 +172,34 @@ const convertToFlatFormat = (opt, data, lng, cb) => {
171
172
  res.resources = res.resources || {};
172
173
  const ns = Object.keys(res.resources)[0];
173
174
  const orgRes = res.resources[ns] || res.resources;
174
- function checkForContext(nsRes) {
175
+ function checkForPostProcessing(nsRes) {
175
176
  Object.keys(nsRes).forEach((k) => {
176
177
  if (orgRes[k].note && (typeof nsRes[k] === 'string' || !nsRes[k])) {
177
178
  nsRes[k] = {
178
179
  value: nsRes[k],
179
180
  context: {
180
181
  text: orgRes[k].note,
181
- },
182
+ }
182
183
  };
183
184
  }
184
185
  });
185
- return nsRes;
186
+ return prepareCombinedImport(nsRes);
186
187
  }
187
188
  if (!res.targetLanguage) {
188
189
  sourceOfjs(res, (err, ret) => {
189
190
  if (err) return cb(err);
190
- cb(null, checkForContext(ret));
191
+ cb(null, checkForPostProcessing(ret));
191
192
  });
192
193
  } else {
193
194
  let ret = targetOfjs(res);
194
- if (lng !== opt.referenceLanguage) return cb(null, checkForContext(ret));
195
+ if (lng !== opt.referenceLanguage) return cb(null, checkForPostProcessing(ret));
195
196
  ret = ret || {};
196
197
  const keys = Object.keys(ret);
197
- if (keys.length === 0) return cb(null, checkForContext(ret));
198
+ if (keys.length === 0) return cb(null, checkForPostProcessing(ret));
198
199
  const allEmpty = keys.filter((k) => ret[k] !== '').length === 0;
199
- if (!allEmpty) return cb(null, checkForContext(ret));
200
+ if (!allEmpty) return cb(null, checkForPostProcessing(ret));
200
201
  ret = sourceOfjs(res);
201
- cb(null, checkForContext(ret));
202
+ cb(null, checkForPostProcessing(ret));
202
203
  }
203
204
  });
204
205
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locize-cli",
3
- "version": "7.8.0",
3
+ "version": "7.9.0",
4
4
  "description": "locize cli to import locales",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,9 +9,9 @@
9
9
  "dependencies": {
10
10
  "@js.properties/properties": "0.5.4",
11
11
  "android-string-resource": "2.3.4",
12
- "async": "3.2.2",
12
+ "async": "3.2.3",
13
13
  "colors": "1.4.0",
14
- "commander": "7.2.0",
14
+ "commander": "9.0.0",
15
15
  "csvjson": "5.1.0",
16
16
  "diff": "5.0.0",
17
17
  "flat": "5.0.2",
@@ -23,18 +23,18 @@
23
23
  "laravelphp": "2.0.3",
24
24
  "lodash.clonedeep": "4.5.0",
25
25
  "mkdirp": "1.0.4",
26
- "node-fetch": "2.6.6",
26
+ "node-fetch": "2.6.7",
27
27
  "resx": "2.0.3",
28
28
  "rimraf": "3.0.2",
29
29
  "strings-file": "0.0.5",
30
30
  "tmexchange": "2.0.4",
31
- "xliff": "5.7.2",
32
- "xlsx": "0.17.4"
31
+ "xliff": "6.0.3",
32
+ "xlsx": "0.18.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "eslint": "7.32.0",
36
36
  "gh-release": "6.0.1",
37
- "pkg": "5.5.1"
37
+ "pkg": "5.5.2"
38
38
  },
39
39
  "scripts": {
40
40
  "lint": "eslint .",
@@ -20,6 +20,7 @@ const getDirectories = (srcpath) => {
20
20
  };
21
21
 
22
22
  const parseLocalLanguage = (opt, lng, cb) => {
23
+ const hasNamespaceInPath = opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) > -1;
23
24
  const filledLngMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng);
24
25
  var firstPartLngMask, lastPartLngMask;
25
26
  if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`)) {
@@ -114,6 +115,8 @@ const parseLocalLanguage = (opt, lng, cb) => {
114
115
  } else {
115
116
  namespace = dirPath.substring(filledNsMask.indexOf(nsMask));
116
117
  }
118
+ } else if (!hasNamespaceInPath && startNsIndex < 0) {
119
+ namespace = opt.namespace;
117
120
  }
118
121
  var fPath = path.join(opt.path, lngPath || '', file);
119
122
  if (dirPath && lngPath.indexOf(nsMask) > -1) {