locize-cli 11.0.0 → 12.0.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +1 -1
  3. package/README.md +1 -0
  4. package/dist/cjs/add.js +90 -0
  5. package/{bin/locize → dist/cjs/cli.js} +390 -670
  6. package/dist/cjs/combineSubkeyPreprocessor.js +155 -0
  7. package/dist/cjs/convertToDesiredFormat.js +205 -0
  8. package/dist/cjs/convertToFlatFormat.js +231 -0
  9. package/dist/cjs/copyVersion.js +60 -0
  10. package/dist/cjs/createBranch.js +59 -0
  11. package/dist/cjs/deleteBranch.js +89 -0
  12. package/dist/cjs/deleteNamespace.js +37 -0
  13. package/dist/cjs/download.js +376 -0
  14. package/dist/cjs/filterNamespaces.js +13 -0
  15. package/dist/cjs/format.js +156 -0
  16. package/dist/cjs/formats.js +33 -0
  17. package/dist/cjs/get.js +66 -0
  18. package/dist/cjs/getBranches.js +37 -0
  19. package/dist/cjs/getJob.js +37 -0
  20. package/dist/cjs/getProjectStats.js +37 -0
  21. package/dist/cjs/getRemoteLanguages.js +38 -0
  22. package/dist/cjs/getRemoteNamespace.js +125 -0
  23. package/dist/cjs/index.js +37 -0
  24. package/dist/cjs/isValidUuid.js +6 -0
  25. package/dist/cjs/lngs.js +215 -0
  26. package/dist/cjs/mapLimit.js +22 -0
  27. package/dist/cjs/mergeBranch.js +80 -0
  28. package/dist/cjs/migrate.js +239 -0
  29. package/dist/cjs/missing.js +162 -0
  30. package/dist/cjs/package.json +5 -0
  31. package/{parseLocalLanguage.js → dist/cjs/parseLocalLanguage.js} +135 -142
  32. package/dist/cjs/parseLocalLanguages.js +18 -0
  33. package/dist/cjs/parseLocalReference.js +11 -0
  34. package/dist/cjs/publishVersion.js +42 -0
  35. package/dist/cjs/removeUndefinedFromArrays.js +19 -0
  36. package/dist/cjs/removeVersion.js +42 -0
  37. package/dist/cjs/request.js +66 -0
  38. package/dist/cjs/shouldUnflatten.js +21 -0
  39. package/dist/cjs/sortFlatResources.js +13 -0
  40. package/dist/cjs/sync.js +772 -0
  41. package/dist/cjs/unflatten.js +81 -0
  42. package/dist/esm/add.js +88 -0
  43. package/dist/esm/cli.js +1020 -0
  44. package/{combineSubkeyPreprocessor.js → dist/esm/combineSubkeyPreprocessor.js} +70 -73
  45. package/dist/esm/convertToDesiredFormat.js +203 -0
  46. package/dist/esm/convertToFlatFormat.js +229 -0
  47. package/dist/esm/copyVersion.js +58 -0
  48. package/dist/esm/createBranch.js +57 -0
  49. package/dist/esm/deleteBranch.js +87 -0
  50. package/dist/esm/deleteNamespace.js +35 -0
  51. package/dist/esm/download.js +374 -0
  52. package/{filterNamespaces.js → dist/esm/filterNamespaces.js} +4 -4
  53. package/dist/esm/format.js +154 -0
  54. package/{formats.js → dist/esm/formats.js} +7 -11
  55. package/dist/esm/get.js +64 -0
  56. package/dist/esm/getBranches.js +35 -0
  57. package/dist/esm/getJob.js +35 -0
  58. package/dist/esm/getProjectStats.js +35 -0
  59. package/dist/esm/getRemoteLanguages.js +36 -0
  60. package/dist/esm/getRemoteNamespace.js +123 -0
  61. package/dist/esm/index.js +16 -0
  62. package/dist/esm/isValidUuid.js +4 -0
  63. package/dist/esm/lngs.js +213 -0
  64. package/dist/esm/mapLimit.js +20 -0
  65. package/dist/esm/mergeBranch.js +78 -0
  66. package/dist/esm/migrate.js +237 -0
  67. package/dist/esm/missing.js +160 -0
  68. package/dist/esm/parseLocalLanguage.js +194 -0
  69. package/dist/esm/parseLocalLanguages.js +16 -0
  70. package/dist/esm/parseLocalReference.js +9 -0
  71. package/dist/esm/publishVersion.js +40 -0
  72. package/{removeUndefinedFromArrays.js → dist/esm/removeUndefinedFromArrays.js} +5 -5
  73. package/dist/esm/removeVersion.js +40 -0
  74. package/dist/esm/request.js +64 -0
  75. package/{shouldUnflatten.js → dist/esm/shouldUnflatten.js} +7 -7
  76. package/dist/esm/sortFlatResources.js +11 -0
  77. package/dist/esm/sync.js +770 -0
  78. package/{unflatten.js → dist/esm/unflatten.js} +36 -34
  79. package/package.json +39 -18
  80. package/rollup.config.js +57 -0
  81. package/add.js +0 -105
  82. package/convertToDesiredFormat.js +0 -268
  83. package/convertToFlatFormat.js +0 -322
  84. package/copyVersion.js +0 -69
  85. package/createBranch.js +0 -61
  86. package/deleteBranch.js +0 -97
  87. package/deleteNamespace.js +0 -39
  88. package/download.js +0 -516
  89. package/format.js +0 -206
  90. package/get.js +0 -81
  91. package/getBranches.js +0 -40
  92. package/getJob.js +0 -40
  93. package/getProjectStats.js +0 -40
  94. package/getRemoteLanguages.js +0 -40
  95. package/getRemoteNamespace.js +0 -122
  96. package/index.js +0 -9
  97. package/isValidUuid.js +0 -2
  98. package/lngs.json +0 -211
  99. package/mergeBranch.js +0 -102
  100. package/migrate.js +0 -314
  101. package/missing.js +0 -169
  102. package/parseLocalLanguages.js +0 -22
  103. package/parseLocalReference.js +0 -10
  104. package/publishVersion.js +0 -64
  105. package/removeVersion.js +0 -64
  106. package/request.js +0 -64
  107. package/sortFlatResources.js +0 -9
  108. package/sync.js +0 -786
@@ -0,0 +1,772 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs');
4
+ var path = require('node:path');
5
+ var mkdirp = require('mkdirp');
6
+ var rimraf = require('rimraf');
7
+ var colors = require('colors');
8
+ var request = require('./request.js');
9
+ var flatten = require('flat');
10
+ var cloneDeep = require('lodash.clonedeep');
11
+ var getRemoteNamespace = require('./getRemoteNamespace.js');
12
+ var getRemoteLanguages = require('./getRemoteLanguages.js');
13
+ var convertToDesiredFormat = require('./convertToDesiredFormat.js');
14
+ var parseLocalLanguages = require('./parseLocalLanguages.js');
15
+ var parseLocalReference = require('./parseLocalReference.js');
16
+ var formats = require('./formats.js');
17
+ var deleteNamespace = require('./deleteNamespace.js');
18
+ var getProjectStats = require('./getProjectStats.js');
19
+ var locize2xcstrings = require('locize-xcstrings/cjs/locize2xcstrings');
20
+ var getBranches = require('./getBranches.js');
21
+ var isValidUuid = require('./isValidUuid.js');
22
+ var os = require('node:os');
23
+ var lngs = require('./lngs.js');
24
+
25
+ const reversedFileExtensionsMap = formats.reversedFileExtensionsMap;
26
+
27
+ // concurrency-limited map: returns array of results
28
+ async function pMapLimit (items, limit, iterator) {
29
+ if (!Array.isArray(items)) items = Array.from(items || []);
30
+ const results = new Array(items.length);
31
+ let i = 0;
32
+ const workers = new Array(Math.min(limit || 1, items.length)).fill(0).map(async () => {
33
+ while (true) {
34
+ const idx = i++;
35
+ if (idx >= items.length) break
36
+ results[idx] = await iterator(items[idx], idx);
37
+ }
38
+ });
39
+ await Promise.all(workers);
40
+ return results
41
+ }
42
+
43
+ // concurrency-limited each (ignore results)
44
+ async function pEachLimit (items, limit, iterator) {
45
+ await pMapLimit(items, limit, async (item, idx) => {
46
+ await iterator(item, idx);
47
+ return null
48
+ });
49
+ }
50
+
51
+ // run array of functions returning Promises in series
52
+ async function pSeries (tasks) {
53
+ const results = [];
54
+ for (const t of tasks) {
55
+ // t may be a function that accepts no args and returns a Promise
56
+ results.push(await t());
57
+ }
58
+ return results
59
+ }
60
+
61
+ const getDirectories = (srcpath) => {
62
+ return fs.readdirSync(srcpath).filter((file) => {
63
+ return fs.statSync(path.join(srcpath, file)).isDirectory()
64
+ })
65
+ };
66
+
67
+ function getInfosInUrl (download) {
68
+ const splitted = download.key.split('/');
69
+ const version = splitted[download.isPrivate ? 2 : 1];
70
+ const language = splitted[download.isPrivate ? 3 : 2];
71
+ const namespace = splitted[download.isPrivate ? 4 : 3];
72
+ return { version, language, namespace }
73
+ }
74
+
75
+ const compareNamespace = (local, remote, lastModifiedLocal, lastModifiedRemote) => {
76
+ const wasLastChangedRemote = lastModifiedLocal && lastModifiedRemote && lastModifiedLocal.getTime() < lastModifiedRemote.getTime();
77
+ const diff = {
78
+ toAdd: [],
79
+ toAddLocally: [],
80
+ toUpdate: [],
81
+ toUpdateLocally: [],
82
+ toRemove: [],
83
+ toRemoveLocally: []
84
+ };
85
+ local = local || {};
86
+ remote = remote || {};
87
+ Object.keys(local).forEach((k) => {
88
+ if (remote[k] === '' && local[k] === '') return
89
+ if (!remote[k]) {
90
+ if (wasLastChangedRemote) {
91
+ diff.toRemoveLocally.push(k); // will download later
92
+ } else {
93
+ diff.toAdd.push(k);
94
+ }
95
+ }
96
+ if (
97
+ remote[k] && (
98
+ (typeof local[k] === 'object' && local[k] && local[k].value && remote[k] !== local[k].value) ||
99
+ (typeof local[k] !== 'object' && remote[k] !== local[k])
100
+ )
101
+ ) {
102
+ if (wasLastChangedRemote) {
103
+ diff.toUpdateLocally.push(k); // will download later
104
+ } else {
105
+ diff.toUpdate.push(k);
106
+ }
107
+ }
108
+ });
109
+ Object.keys(remote).forEach((k) => {
110
+ if (local[k] === '' && remote[k] === '') return
111
+ if (!local[k]) {
112
+ if (wasLastChangedRemote) {
113
+ diff.toAddLocally.push(k); // will download later
114
+ } else {
115
+ diff.toRemove.push(k);
116
+ }
117
+ }
118
+ });
119
+ return diff
120
+ };
121
+
122
+ const doesDirectoryExist = (p) => {
123
+ let directoryExists = false;
124
+ try {
125
+ directoryExists = fs.statSync(p).isDirectory();
126
+ } catch (e) {}
127
+ return directoryExists
128
+ };
129
+
130
+ const getNamespaceNamesAvailableInReference = (opt, downloads) => {
131
+ const nsNames = [];
132
+ downloads.forEach((d) => {
133
+ const splitted = d.key.split('/');
134
+ const lng = splitted[2];
135
+ const ns = splitted[3];
136
+ if (lng === opt.referenceLanguage) {
137
+ nsNames.push(ns);
138
+ }
139
+ });
140
+ return nsNames
141
+ };
142
+
143
+ const ensureAllNamespacesInLanguages = (opt, remoteLanguages, downloads) => {
144
+ const namespaces = getNamespaceNamesAvailableInReference(opt, downloads);
145
+
146
+ remoteLanguages.forEach((lng) => {
147
+ namespaces.forEach((n) => {
148
+ const found = downloads.find((d) => d.key === `${opt.projectId}/${opt.version}/${lng}/${n}`);
149
+ if (!found) {
150
+ downloads.push({
151
+ key: `${opt.projectId}/${opt.version}/${lng}/${n}`,
152
+ lastModified: '1960-01-01T00:00:00.000Z',
153
+ size: 0,
154
+ url: `${opt.apiEndpoint}/${opt.projectId}/${opt.version}/${lng}/${n}`
155
+ });
156
+ }
157
+ });
158
+ });
159
+ };
160
+
161
+ const cleanupLanguages = (opt, remoteLanguages) => {
162
+ if (opt.pathMask.lastIndexOf(path.sep) < 0) return
163
+ const dirs = getDirectories(opt.path).filter((dir) => dir.indexOf('.') !== 0);
164
+ if (!opt.language && (!opt.languages || opt.languages.length === 0) && !opt.namespace && !opt.namespaces) {
165
+ dirs
166
+ .filter((lng) => {
167
+ const lMask = `${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`;
168
+ const startLIndex = opt.pathMask.indexOf(lMask);
169
+ const restLMask = lng.substring((startLIndex || 0) + lMask.length);
170
+ lng = lng.substring(startLIndex || 0, lng.lastIndexOf(restLMask));
171
+
172
+ return lng !== opt.referenceLanguage &&
173
+ !!lngs.find((c) => lng === c || lng.indexOf(c + '-') === 0)
174
+ })
175
+ .forEach((lng) => {
176
+ const filledLngMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng);
177
+ let lngPath;
178
+ if (filledLngMask.lastIndexOf(path.sep) > 0) {
179
+ lngPath = filledLngMask.substring(0, filledLngMask.lastIndexOf(path.sep));
180
+ }
181
+ if (doesDirectoryExist(path.join(opt.path, lngPath, 'CVS'))) return // special hack for CVS
182
+ rimraf.rimraf.sync(path.join(opt.path, lngPath));
183
+ });
184
+ }
185
+ remoteLanguages.forEach((lng) => {
186
+ if (opt.language && opt.language !== lng) return
187
+ if (opt.languages && opt.languages.length > 0 && opt.languages.indexOf(lng) < 0) return
188
+ const filledLngMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng);
189
+ let lngPath;
190
+ if (filledLngMask.lastIndexOf(path.sep) > 0) {
191
+ lngPath = filledLngMask.substring(0, filledLngMask.lastIndexOf(path.sep));
192
+ }
193
+ if (lngPath && lngPath.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) < 0) mkdirp.mkdirp.sync(path.join(opt.path, lngPath));
194
+ });
195
+ };
196
+
197
+ const backupDeleted = (opt, ns, now) => {
198
+ if (opt.dry || ns.diff.toRemove.length === 0) return
199
+ let m = now.getMonth() + 1;
200
+ if (m < 10) m = `0${m}`;
201
+ let d = now.getDate();
202
+ if (d < 10) d = `0${d}`;
203
+ let h = now.getHours();
204
+ if (h < 10) h = `0${h}`;
205
+ let mi = now.getMinutes();
206
+ if (mi < 10) mi = `0${mi}`;
207
+ let s = now.getSeconds();
208
+ if (s < 10) s = `0${s}`;
209
+ const currentBackupPath = path.join(opt.backupDeletedPath, `${now.getFullYear()}${m}${d}-${h}${mi}${s}`);
210
+ mkdirp.mkdirp.sync(currentBackupPath);
211
+ const removingRemote = ns.diff.toRemove.reduce((prev, k) => {
212
+ prev[k] = ns.remoteContent[k];
213
+ return prev
214
+ }, {});
215
+ mkdirp.mkdirp.sync(path.join(currentBackupPath, ns.language));
216
+ const content = JSON.stringify(removingRemote, null, 2);
217
+ const fileContent = (opt.format !== 'xlsx' && !content.endsWith('\n')) ? (content + '\n') : content;
218
+ fs.writeFileSync(path.join(currentBackupPath, ns.language, `${ns.namespace}.json`), fileContent);
219
+ };
220
+
221
+ async function getDownloads (opt) {
222
+ // replicates earlier behavior but returns a Promise that resolves to downloads array
223
+ if (!opt.unpublished) {
224
+ const url = opt.apiEndpoint + '/download/' + opt.projectId + '/' + opt.version;
225
+ const headers = opt.apiKey ? { Authorization: opt.apiKey } : undefined;
226
+ let { res, obj } = await request(url, { method: 'get', headers });
227
+ if (res.status >= 300) {
228
+ if (obj && (obj.errorMessage || obj.message)) {
229
+ if (res.statusText && res.status) {
230
+ throw new Error(res.statusText + ' (' + res.status + ') | ' + (obj.errorMessage || obj.message))
231
+ }
232
+ throw new Error((obj.errorMessage || obj.message))
233
+ }
234
+ throw new Error(res.statusText + ' (' + res.status + ')')
235
+ }
236
+ if (obj.length > 0) {
237
+ if (opt.skipEmpty) obj = obj.filter((d) => d.size > 2);
238
+ return obj
239
+ }
240
+
241
+ const resStats = await getProjectStats(opt);
242
+ const stats = resStats;
243
+ if (!stats) throw new Error('Nothing found!')
244
+ if (!stats[opt.version]) throw new Error(`Version "${opt.version}" not found!`)
245
+ return obj
246
+ } else {
247
+ const stats = await getProjectStats(opt);
248
+ if (!stats) throw new Error('Nothing found!')
249
+ if (!stats[opt.version]) throw new Error(`Version "${opt.version}" not found!`)
250
+
251
+ const toDownload = [];
252
+ const lngsToCheck = opt.language ? [opt.language] : (opt.languages && opt.languages.length > 0) ? opt.languages : Object.keys(stats[opt.version]);
253
+ lngsToCheck.forEach((l) => {
254
+ if (opt.namespaces) {
255
+ opt.namespaces.forEach((n) => {
256
+ if (!stats[opt.version][l][n]) return
257
+ if (opt.skipEmpty && stats[opt.version][l][n].segmentsTranslated === 0) return
258
+ toDownload.push({
259
+ url: `${opt.apiEndpoint}/${opt.projectId}/${opt.version}/${l}/${n}`,
260
+ key: `${opt.projectId}/${opt.version}/${l}/${n}`,
261
+ lastModified: '1960-01-01T00:00:00.000Z',
262
+ size: 0
263
+ });
264
+ });
265
+ } else if (opt.namespace) {
266
+ if (!stats[opt.version][l][opt.namespace]) return
267
+ if (opt.skipEmpty && stats[opt.version][l][opt.namespace].segmentsTranslated === 0) return
268
+ toDownload.push({
269
+ url: `${opt.apiEndpoint}/${opt.projectId}/${opt.version}/${l}/${opt.namespace}`,
270
+ key: `${opt.projectId}/${opt.version}/${l}/${opt.namespace}`,
271
+ lastModified: '1960-01-01T00:00:00.000Z',
272
+ size: 0
273
+ });
274
+ } else {
275
+ Object.keys(stats[opt.version][l]).forEach((n) => {
276
+ if (opt.skipEmpty && stats[opt.version][l][n].segmentsTranslated === 0) return
277
+ toDownload.push({
278
+ url: `${opt.apiEndpoint}/${opt.projectId}/${opt.version}/${l}/${n}`,
279
+ key: `${opt.projectId}/${opt.version}/${l}/${n}`,
280
+ lastModified: '1960-01-01T00:00:00.000Z',
281
+ size: 0
282
+ });
283
+ });
284
+ }
285
+ });
286
+ return toDownload
287
+ }
288
+ }
289
+
290
+ async function compareNamespaces (opt, localNamespaces) {
291
+ // previously used async.mapLimit -> pMapLimit
292
+ const limit = 20;
293
+ return pMapLimit(localNamespaces, limit, async (ns) => {
294
+ const { result: remoteNamespace, lastModified } = await getRemoteNamespace(opt, ns.language, ns.namespace);
295
+ const diff = compareNamespace(ns.content, remoteNamespace, opt.compareModificationTime ? ns.mtime : undefined, opt.compareModificationTime ? lastModified : undefined);
296
+ ns.diff = diff;
297
+ ns.remoteContent = remoteNamespace;
298
+ return ns
299
+ })
300
+ }
301
+
302
+ async function downloadAll (opt, remoteLanguages, omitRef = false, manipulate) {
303
+ if (!opt.dry && opt.format !== 'xcstrings') cleanupLanguages(opt, remoteLanguages);
304
+
305
+ let downloads = await getDownloads(opt);
306
+
307
+ ensureAllNamespacesInLanguages(opt, remoteLanguages, downloads);
308
+
309
+ if (omitRef) {
310
+ downloads = downloads.filter((d) => {
311
+ const splitted = d.key.split('/');
312
+ const lng = splitted[d.isPrivate ? 3 : 2];
313
+ return lng !== opt.referenceLanguage
314
+ });
315
+ }
316
+
317
+ if (opt.format === 'xcstrings') { // 1 file per namespace including all languages
318
+ const downloadsByNamespace = {};
319
+ downloads.forEach((download) => {
320
+ const { namespace } = getInfosInUrl(download);
321
+ downloadsByNamespace[namespace] = downloadsByNamespace[namespace] || [];
322
+ downloadsByNamespace[namespace].push(download);
323
+ });
324
+
325
+ const namespaceKeys = Object.keys(downloadsByNamespace);
326
+ const concurrency = opt.unpublished ? 5 : 20;
327
+
328
+ await pEachLimit(namespaceKeys, concurrency, async (namespace) => {
329
+ const locizeData = {
330
+ sourceLng: opt.referenceLanguage,
331
+ resources: {}
332
+ };
333
+
334
+ const entries = downloadsByNamespace[namespace];
335
+ await pEachLimit(entries, opt.unpublished ? 5 : 20, async (download) => {
336
+ const { language } = getInfosInUrl(download);
337
+ opt.isPrivate = download.isPrivate;
338
+
339
+ if (opt.language && opt.language !== language && language !== opt.referenceLanguage) return
340
+ if (opt.languages && opt.languages.length > 0 && opt.languages.indexOf(language) < 0 && language !== opt.referenceLanguage) return
341
+ if (opt.namespace && opt.namespace !== namespace) return
342
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return
343
+
344
+ if (opt.unpublished) opt.raw = true;
345
+ const { result: ns } = await getRemoteNamespace(opt, language, namespace);
346
+
347
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
348
+ return
349
+ }
350
+
351
+ if (manipulate && typeof manipulate === 'function') manipulate(language, namespace, ns);
352
+
353
+ locizeData.resources[language] = ns;
354
+ });
355
+
356
+ try {
357
+ const converted = locize2xcstrings(locizeData);
358
+
359
+ const filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, '').replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
360
+ if (opt.dry) return
361
+ if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) && filledMask.lastIndexOf(path.sep) > 0) {
362
+ mkdirp.mkdirp.sync(path.join(opt.path, filledMask.substring(0, filledMask.lastIndexOf(path.sep))));
363
+ }
364
+ const parentDir = path.dirname(path.join(opt.path, filledMask));
365
+ mkdirp.mkdirp.sync(parentDir);
366
+ const fileContent = (opt.format !== 'xlsx' && !converted.endsWith('\n')) ? (converted + '\n') : converted;
367
+ await fs.promises.writeFile(path.join(opt.path, filledMask), fileContent);
368
+ } catch (e) {
369
+ e.message = 'Invalid content for "' + opt.format + '" format!\n' + (e.message || '');
370
+ throw e
371
+ }
372
+ });
373
+ } else { // 1 file per namespace/lng
374
+ const concurrency = opt.unpublished ? 5 : 20;
375
+ await pEachLimit(downloads, concurrency, async (download) => {
376
+ const { language, namespace } = getInfosInUrl(download);
377
+ opt.isPrivate = download.isPrivate;
378
+
379
+ if (opt.language && opt.language !== language && language !== opt.referenceLanguage) return
380
+ if (opt.languages && opt.languages.length > 0 && opt.languages.indexOf(language) < 0 && language !== opt.referenceLanguage) return
381
+ if (opt.namespace && opt.namespace !== namespace) return
382
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return
383
+
384
+ const { result: ns, lastModified } = await getRemoteNamespace(opt, language, namespace);
385
+
386
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
387
+ return
388
+ }
389
+
390
+ if (manipulate && typeof manipulate === 'function') manipulate(language, namespace, ns);
391
+
392
+ const converted = await convertToDesiredFormat(opt, namespace, language, ns, lastModified);
393
+ // convertToDesiredFormatP either resolves converted or throws
394
+ let convertedText = converted;
395
+ if (Array.isArray(converted) && converted.length === 1) convertedText = converted[0];
396
+
397
+ const filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, language).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
398
+ if (opt.dry) return
399
+ if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) && filledMask.lastIndexOf(path.sep) > 0) {
400
+ mkdirp.mkdirp.sync(path.join(opt.path, filledMask.substring(0, filledMask.lastIndexOf(path.sep))));
401
+ }
402
+ const parentDir = path.dirname(path.join(opt.path, filledMask));
403
+ mkdirp.mkdirp.sync(parentDir);
404
+ const fileContent = (opt.format !== 'xlsx' && !convertedText.endsWith('\n')) ? (convertedText + '\n') : convertedText;
405
+ await fs.promises.writeFile(path.join(opt.path, filledMask), fileContent);
406
+ });
407
+ }
408
+ }
409
+
410
+ async function update (opt, lng, ns, shouldOmit = false) {
411
+ const data = {};
412
+ if (!opt.skipDelete) {
413
+ ns.diff.toRemove.forEach((k) => { data[k] = null; });
414
+ }
415
+ ns.diff.toAdd.forEach((k) => { data[k] = ns.content[k]; });
416
+ if (opt.updateValues) {
417
+ ns.diff.toUpdate.forEach((k) => { data[k] = ns.content[k]; });
418
+ }
419
+
420
+ const keysToSend = Object.keys(data).length;
421
+ if (keysToSend === 0) return
422
+
423
+ if (opt.dry) return
424
+
425
+ const payloadKeysLimit = 1000;
426
+
427
+ async function send (d, so) {
428
+ const queryParams = new URLSearchParams();
429
+ if (opt.autoTranslate && lng === opt.referenceLanguage) {
430
+ /** @See https://www.locize.com/docs/api#optional-autotranslate */
431
+ queryParams.append('autotranslate', 'true');
432
+ }
433
+ if (so) {
434
+ queryParams.append('omitstatsgeneration', 'true');
435
+ }
436
+
437
+ const queryString = queryParams.size > 0 ? '?' + queryParams.toString() : '';
438
+
439
+ // retry once on 504
440
+ let isRetrying = false;
441
+ while (true) {
442
+ const { res, obj } = await request(opt.apiEndpoint + '/update/' + opt.projectId + '/' + opt.version + '/' + lng + '/' + ns.namespace + queryString, {
443
+ method: 'post',
444
+ body: d,
445
+ headers: {
446
+ Authorization: opt.apiKey
447
+ }
448
+ });
449
+ const cliInfo = res.headers.get('x-cli-info');
450
+ if (cliInfo && cliInfo !== opt.lastShownCliInfo) {
451
+ console.log(colors.yellow(cliInfo));
452
+ opt.lastShownCliInfo = cliInfo;
453
+ }
454
+ if (res.status === 504 && !isRetrying) {
455
+ isRetrying = true;
456
+ await new Promise((resolve) => setTimeout(resolve, 3000));
457
+ continue
458
+ }
459
+ if (res.status >= 300 && res.status !== 412) {
460
+ if (obj && (obj.errorMessage || obj.message)) {
461
+ throw new Error((obj.errorMessage || obj.message))
462
+ }
463
+ throw new Error(res.statusText + ' (' + res.status + ')')
464
+ }
465
+ await new Promise((resolve) => setTimeout(resolve, 1000));
466
+ return
467
+ }
468
+ }
469
+
470
+ if (keysToSend > payloadKeysLimit) {
471
+ const tasks = [];
472
+ const keysInObj = Object.keys(data);
473
+
474
+ while (keysInObj.length > payloadKeysLimit) {
475
+ const pagedData = {};
476
+ keysInObj.splice(0, payloadKeysLimit).forEach((k) => { pagedData[k] = data[k]; });
477
+ const hasMoreKeys = keysInObj.length > 0;
478
+ tasks.push(async () => send(pagedData, hasMoreKeys ? true : shouldOmit));
479
+ }
480
+
481
+ if (keysInObj.length === 0) return
482
+
483
+ const finalPagedData = {};
484
+ keysInObj.splice(0, keysInObj.length).forEach((k) => { finalPagedData[k] = data[k]; });
485
+ tasks.push(async () => send(finalPagedData, shouldOmit));
486
+
487
+ // run tasks in series (as original async.series)
488
+ await pSeries(tasks);
489
+ return
490
+ }
491
+
492
+ await send(data, shouldOmit);
493
+ }
494
+
495
+ async function handleSync (opt, remoteLanguages, localNamespaces) {
496
+ if (!localNamespaces || localNamespaces.length === 0) {
497
+ await downloadAll(opt, remoteLanguages, false);
498
+ return
499
+ }
500
+
501
+ const downloads = await getDownloads(opt);
502
+ opt.isPrivate = downloads.length > 0 && downloads[0].isPrivate;
503
+
504
+ const localMissingNamespaces = checkForMissingLocalNamespaces(downloads, localNamespaces, opt);
505
+
506
+ const compared = await compareNamespaces(opt, localNamespaces);
507
+
508
+ const onlyToUpdate = compared.filter((ns) => ns.diff.toAdd.concat(opt.skipDelete ? [] : ns.diff.toRemove).concat(ns.diff.toUpdate).length > 0);
509
+
510
+ const lngsInReqs = [];
511
+ const nsInReqs = [];
512
+ onlyToUpdate.forEach((n) => {
513
+ if (lngsInReqs.indexOf(n.language) < 0) {
514
+ lngsInReqs.push(n.language);
515
+ }
516
+ if (nsInReqs.indexOf(n.namespace) < 0) {
517
+ nsInReqs.push(n.namespace);
518
+ }
519
+ });
520
+ const shouldOmit = lngsInReqs.length > 5 || nsInReqs.length > 5;
521
+
522
+ let wasThereSomethingToUpdate = opt.autoTranslate || false;
523
+
524
+ async function updateComparedNamespaces () {
525
+ const now = new Date();
526
+ const concurrency = Math.max(1, Math.round(os.cpus().length / 2));
527
+ await pEachLimit(compared, concurrency, async (ns) => {
528
+ if (ns.diff.toRemove.length > 0) {
529
+ if (opt.skipDelete) {
530
+ console.log(colors.bgRed(`skipping the removal of ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}...`));
531
+ if (opt.dry) console.log(colors.bgRed(`skipped to remove ${ns.diff.toRemove.join(', ')} in ${ns.language}/${ns.namespace}...`));
532
+ } else {
533
+ console.log(colors.red(`removing ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}...`));
534
+ if (opt.dry) console.log(colors.red(`would remove ${ns.diff.toRemove.join(', ')} in ${ns.language}/${ns.namespace}...`));
535
+ if (!opt.dry && opt.backupDeletedPath) backupDeleted(opt, ns, now);
536
+ }
537
+ }
538
+ if (ns.diff.toRemoveLocally.length > 0) {
539
+ console.log(colors.red(`removing ${ns.diff.toRemoveLocally.length} keys in ${ns.language}/${ns.namespace} locally...`));
540
+ if (opt.dry) console.log(colors.red(`would remove ${ns.diff.toRemoveLocally.join(', ')} in ${ns.language}/${ns.namespace} locally...`));
541
+ }
542
+ if (ns.diff.toAdd.length > 0) {
543
+ console.log(colors.green(`adding ${ns.diff.toAdd.length} keys in ${ns.language}/${ns.namespace}...`));
544
+ if (opt.dry) console.log(colors.green(`would add ${ns.diff.toAdd.join(', ')} in ${ns.language}/${ns.namespace}...`));
545
+ }
546
+ if (ns.diff.toAddLocally.length > 0) {
547
+ if (opt.skipDelete) {
548
+ console.log(colors.bgGreen(`skipping the addition of ${ns.diff.toAddLocally.length} keys in ${ns.language}/${ns.namespace} locally...`));
549
+ if (opt.dry) console.log(colors.bgGreen(`skipped the addition of ${ns.diff.toAddLocally.join(', ')} in ${ns.language}/${ns.namespace} locally...`));
550
+ } else {
551
+ console.log(colors.green(`adding ${ns.diff.toAddLocally.length} keys in ${ns.language}/${ns.namespace} locally...`));
552
+ if (opt.dry) console.log(colors.green(`would add ${ns.diff.toAddLocally.join(', ')} in ${ns.language}/${ns.namespace} locally...`));
553
+ }
554
+ }
555
+ if (opt.updateValues) {
556
+ if (ns.diff.toUpdate.length > 0) {
557
+ console.log(colors.yellow(`updating ${ns.diff.toUpdate.length} keys in ${ns.language}/${ns.namespace}${opt.autoTranslate ? ' with automatic translation' : ''}...`));
558
+ if (opt.dry) console.log(colors.yellow(`would update ${ns.diff.toUpdate.join(', ')} in ${ns.language}/${ns.namespace}...`));
559
+ }
560
+ if (ns.diff.toUpdateLocally.length > 0) {
561
+ console.log(colors.yellow(`updating ${ns.diff.toUpdateLocally.length} keys in ${ns.language}/${ns.namespace} locally...`));
562
+ if (opt.dry) console.log(colors.yellow(`would update ${ns.diff.toUpdateLocally.join(', ')} in ${ns.language}/${ns.namespace} locally...`));
563
+ }
564
+ }
565
+ const somethingToUpdate = ns.diff.toAdd.concat(opt.skipDelete ? [] : ns.diff.toRemove)/* .concat(ns.diff.toUpdate) */.length > 0;
566
+ if (!somethingToUpdate) console.log(colors.grey(`nothing to update for ${ns.language}/${ns.namespace}`));
567
+ if (!wasThereSomethingToUpdate && somethingToUpdate) wasThereSomethingToUpdate = true;
568
+
569
+ await update(opt, ns.language, ns, shouldOmit);
570
+ if (ns.diff.toRemove.length === 0 || ns.language !== opt.referenceLanguage) return
571
+ const nsOnlyRemove = cloneDeep(ns);
572
+ nsOnlyRemove.diff.toAdd = [];
573
+ nsOnlyRemove.diff.toUpdate = [];
574
+ await pEachLimit(remoteLanguages, Math.max(1, Math.round(os.cpus().length / 2)), async (lng) => {
575
+ await update(opt, lng, nsOnlyRemove, shouldOmit);
576
+ });
577
+ });
578
+
579
+ console.log(colors.grey('syncing...'));
580
+
581
+ async function down () {
582
+ await new Promise((resolve) => setTimeout(resolve, wasThereSomethingToUpdate && !opt.dry ? (opt.autoTranslate ? 10000 : 5000) : 0));
583
+ await downloadAll(opt, remoteLanguages, false,
584
+ opt.skipDelete
585
+ ? (lng, namespace, ns) => {
586
+ const found = compared.find((n) => n.namespace === namespace && n.language === lng);
587
+ if (found && found.diff) {
588
+ if (found.diff.toAddLocally && found.diff.toAddLocally.length > 0) {
589
+ found.diff.toAddLocally.forEach((k) => {
590
+ delete ns[k];
591
+ });
592
+ }
593
+ if (found.diff.toRemove && found.diff.toRemove.length > 0) {
594
+ found.diff.toRemove.forEach((k) => {
595
+ delete ns[k];
596
+ });
597
+ }
598
+ }
599
+ }
600
+ : undefined
601
+ );
602
+ }
603
+
604
+ if (!shouldOmit) return down()
605
+ if (opt.dry) return down()
606
+
607
+ // optimize stats generation...
608
+ const url = opt.apiEndpoint + '/stats/project/regenerate/' + opt.projectId + '/' + opt.version + (lngsInReqs.length === 1 ? `/${lngsInReqs[0]}` : '') + (nsInReqs.length === 1 ? `?namespace=${nsInReqs[0]}` : '');
609
+ const { res, obj } = await request(url, {
610
+ method: 'post',
611
+ body: {},
612
+ headers: {
613
+ Authorization: opt.apiKey
614
+ }
615
+ });
616
+ if (res.status >= 300 && res.status !== 412) {
617
+ if (obj && (obj.errorMessage || obj.message)) {
618
+ throw new Error((obj.errorMessage || obj.message))
619
+ }
620
+ throw new Error(res.statusText + ' (' + res.status + ')')
621
+ }
622
+ return down()
623
+ }
624
+
625
+ if (opt.deleteRemoteNamespace && localMissingNamespaces.length > 0) {
626
+ wasThereSomethingToUpdate = true;
627
+ await pEachLimit(localMissingNamespaces, 20, async (n) => {
628
+ if (opt.dry) {
629
+ console.log(colors.red(`would delete complete namespace ${n.namespace}...`));
630
+ return
631
+ }
632
+ console.log(colors.red(`deleting complete namespace ${n.namespace}...`));
633
+ await deleteNamespace({
634
+ apiEndpoint: opt.apiEndpoint,
635
+ apiKey: opt.apiKey,
636
+ projectId: opt.projectId,
637
+ version: opt.version,
638
+ namespace: n.namespace
639
+ });
640
+ });
641
+ return updateComparedNamespaces()
642
+ }
643
+ return updateComparedNamespaces()
644
+ }
645
+
646
+ const checkForMissingLocalNamespaces = (downloads, localNamespaces, opt) => {
647
+ const localMissingNamespaces = [];
648
+ downloads.forEach((d) => {
649
+ const splitted = d.url.split('/');
650
+ const namespace = splitted.pop();
651
+ const language = splitted.pop();
652
+ if (language === opt.referenceLanguage) {
653
+ const foundLocalNamespace = localNamespaces.find((n) => n.namespace === namespace && n.language === language);
654
+ if (!foundLocalNamespace) {
655
+ localMissingNamespaces.push({
656
+ language,
657
+ namespace
658
+ });
659
+ }
660
+ }
661
+ });
662
+ return localMissingNamespaces
663
+ };
664
+
665
+ async function continueToSync (opt) {
666
+ console.log(colors.grey('checking remote (locize)...'));
667
+ const remoteLanguages = await getRemoteLanguages(opt);
668
+
669
+ if (opt.referenceLanguageOnly && opt.language && opt.referenceLanguage !== opt.language) {
670
+ opt.referenceLanguage = opt.language;
671
+ }
672
+ if (opt.referenceLanguageOnly && !opt.language && opt.languages && opt.languages.length > 0 && opt.languages.indexOf(opt.referenceLanguage) < 0) {
673
+ opt.referenceLanguage = opt.languages[0];
674
+ }
675
+
676
+ if (opt.referenceLanguageOnly) {
677
+ console.log(colors.grey(`checking local${opt.path !== process.cwd() ? ` (${opt.path})` : ''} only reference language...`));
678
+ let localNamespaces = await parseLocalReference(opt);
679
+ if (!opt.dry && opt.cleanLocalFiles) {
680
+ localNamespaces.forEach((ln) => fs.unlinkSync(ln.path));
681
+ localNamespaces = [];
682
+ }
683
+
684
+ console.log(colors.grey('calculate diffs...'));
685
+ await handleSync(opt, remoteLanguages, localNamespaces);
686
+ return
687
+ }
688
+
689
+ console.log(colors.grey(`checking local${opt.path !== process.cwd() ? ` (${opt.path})` : ''}...`));
690
+ let localNamespaces = await parseLocalLanguages(opt, remoteLanguages);
691
+ if (!opt.dry && opt.cleanLocalFiles) {
692
+ localNamespaces.forEach((ln) => fs.unlinkSync(ln.path));
693
+ localNamespaces = [];
694
+ }
695
+
696
+ console.log(colors.grey('calculate diffs...'));
697
+ await handleSync(opt, remoteLanguages, localNamespaces);
698
+ }
699
+
700
+ async function syncInternal (opt) {
701
+ opt.format = opt.format || 'json';
702
+ if (!reversedFileExtensionsMap[opt.format]) {
703
+ throw new Error(`${opt.format} is not a valid format!`)
704
+ }
705
+
706
+ if (opt.autoTranslate && !opt.referenceLanguageOnly) {
707
+ console.log(colors.yellow('Using the "--auto-translate true" option together with the "--reference-language-only false" option might result in inconsistent target language translations (automatic translation vs. what is sent direcly to locize).'));
708
+ }
709
+
710
+ opt.version = opt.version || 'latest';
711
+ opt.apiEndpoint = opt.apiEndpoint || 'https://api.locize.app';
712
+
713
+ if (!opt.dry && opt.clean) rimraf.rimraf.sync(path.join(opt.path, '*'));
714
+
715
+ if (opt.autoCreatePath === false) {
716
+ if (!doesDirectoryExist(opt.path)) {
717
+ throw new Error(`${opt.path} does not exist!`)
718
+ }
719
+ }
720
+ if (!opt.dry) mkdirp.mkdirp.sync(opt.path);
721
+
722
+ if (opt.namespace && opt.namespace.indexOf(',') > 0 && opt.namespace.indexOf(' ') < 0) {
723
+ opt.namespaces = opt.namespace.split(',');
724
+ delete opt.namespace;
725
+ }
726
+
727
+ opt.pathMaskInterpolationPrefix = opt.pathMaskInterpolationPrefix || '{{';
728
+ opt.pathMaskInterpolationSuffix = opt.pathMaskInterpolationSuffix || '}}';
729
+ opt.pathMask = opt.pathMask || `${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}${path.sep}${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`;
730
+ opt.languageFolderPrefix = opt.languageFolderPrefix || '';
731
+ opt.pathMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, `${opt.languageFolderPrefix}${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`);
732
+ if (opt.overriddenOnly) {
733
+ opt.unpublished = true;
734
+ }
735
+ if (opt.unpublished && !opt.apiKey) {
736
+ throw new Error('Please provide also an api-key!')
737
+ }
738
+
739
+ if (opt.branch === '') {
740
+ throw new Error('The branch name seems invalid!')
741
+ }
742
+
743
+ if (opt.branch) {
744
+ const branches = await getBranches(opt);
745
+ let b;
746
+ if (isValidUuid(opt.branch)) b = branches.find((br) => br.id === opt.branch);
747
+ if (!b) b = branches.find((br) => br.name === opt.branch);
748
+ if (!b) {
749
+ throw new Error(`Branch ${opt.branch} not found!`)
750
+ }
751
+ opt.projectId = b.id;
752
+ opt.version = b.version;
753
+
754
+ return continueToSync(opt)
755
+ }
756
+
757
+ return continueToSync(opt)
758
+ }
759
+
760
+ async function sync (opt) {
761
+ opt = opt || {};
762
+
763
+ try {
764
+ await syncInternal(opt);
765
+ console.log(colors.green('FINISHED'));
766
+ } catch (err) {
767
+ console.error(colors.red(err.stack));
768
+ process.exit(1);
769
+ }
770
+ }
771
+
772
+ module.exports = sync;