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