locize-cli 8.6.1 → 8.7.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,14 @@ 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
+ ## [8.7.0](https://github.com/locize/locize-cli/compare/v8.6.2...v8.7.0) - 2025-03-20
9
+
10
+ - intruduce xcstrings format [106](https://github.com/locize/locize-cli/issues/106)
11
+
12
+ ## [8.6.2](https://github.com/locize/locize-cli/compare/v8.6.1...v8.6.2) - 2025-03-18
13
+
14
+ - fix migrate command to handle paged update
15
+
8
16
  ## [8.6.1](https://github.com/locize/locize-cli/compare/v8.6.0...v8.6.1) - 2025-03-11
9
17
 
10
18
  - fix .yml yaml format variats to address [104](https://github.com/locize/locize-cli/issues/104)
package/README.md CHANGED
@@ -84,7 +84,7 @@ or
84
84
  locize download
85
85
  ```
86
86
 
87
- or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties)
87
+ or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
88
88
 
89
89
  ```sh
90
90
  locize download --project-id my-project-id-93e1-442a-ab35-24331fa294ba --ver latest --language en --namespace namespace1 --path ./backup --format android
@@ -133,7 +133,7 @@ Add your api-key and your project-id and let's go...
133
133
  locize sync --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba
134
134
  ```
135
135
 
136
- or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties)
136
+ or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
137
137
 
138
138
  ```sh
139
139
  locize sync --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba --format android
@@ -192,7 +192,7 @@ Add your api-key and your project-id and let's go...
192
192
  locize save-missing --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba
193
193
  ```
194
194
 
195
- or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties)
195
+ or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
196
196
 
197
197
  ```sh
198
198
  locize save-missing --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba --format android
package/bin/locize CHANGED
@@ -232,7 +232,7 @@ program
232
232
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
233
233
  .option('-g, --get-path <url>', `Specify the get-path url that should be used (default: ${getPathUrl})`)
234
234
  .option('-k, --api-key <apiKey>', 'The api-key that should be used')
235
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties])', 'json')
235
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
236
236
  .option('-s, --skip-empty <true|false>', 'Skips to download empty files (default: true)', 'true')
237
237
  .option('-P, --language-folder-prefix <prefix>', 'This will be added as a local folder name prefix in front of the language.', '')
238
238
  .option('-m, --path-mask <mask>', 'This will define the folder and file structure; do not add a file extension (default: {{language}}/{{namespace}})', `{{language}}${path.sep}{{namespace}}`)
@@ -361,7 +361,7 @@ program
361
361
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
362
362
  .option('-B, --backup-deleted-path <path>', 'Saves the segments that will be deleted in this path')
363
363
  .option('-A, --auto-create-path <true|false>', 'This will automatically make sure the --path is created. (default: true)', 'true')
364
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties])', 'json')
364
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
365
365
  .option('-s, --skip-empty <true|false>', 'Skips to download empty files (default: false)', 'false')
366
366
  .option('-c, --clean <true|false>', 'Removes all local files by removing the whole folder (default: false)', 'false')
367
367
  .option('-cf, --clean-local-files <true|false>', 'Removes all local files without removing any folder (default: false)', 'false')
@@ -472,7 +472,7 @@ program
472
472
  .option('-i, --project-id <projectId>', 'The project-id that should be used')
473
473
  .option('-v, --ver <version>', 'Found namespaces will be matched to this version (default: latest)')
474
474
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
475
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties])', 'json')
475
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
476
476
  .option('-m, --path-mask <mask>', 'This will define the folder and file structure; do not add a file extension (default: {{language}}/{{namespace}})', `{{language}}${path.sep}{{namespace}}`)
477
477
  .option('-P, --language-folder-prefix <prefix>', 'This will be added as a local folder name prefix in front of the language.', '')
478
478
  .option('-d, --dry <true|false>', 'Dry run (default: false)', 'false')
@@ -695,7 +695,7 @@ program
695
695
  .command('format [fileOrDirectory]')
696
696
  .alias('ft')
697
697
  .description('format local files')
698
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties])', 'json')
698
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
699
699
  .option('-l, --reference-language <lng>', 'Some format conversions need to know the reference language.', 'en')
700
700
  .option('-d, --dry <true|false>', 'Dry run (default: false)', 'false')
701
701
  .option('-C, --config-path <configPath>', `Specify the path to the optional locize config file (default: ${configInWorkingDirectory} or ${configInHome})`)
@@ -13,6 +13,7 @@ const ftl2js = require('fluent_conv/cjs/ftl2js');
13
13
  const tmx2js = require('tmexchange/cjs/tmx2js');
14
14
  const laravel2js = require('laravelphp/cjs/laravel2js');
15
15
  const javaProperties = require('@js.properties/properties');
16
+ const xcstrings2locize = require('locize-xcstrings/cjs/xcstrings2locize');
16
17
  const flatten = require('flat');
17
18
  const prepareCombinedImport = require('./combineSubkeyPreprocessor').prepareImport;
18
19
 
@@ -293,6 +294,10 @@ const convertToFlatFormat = (opt, data, lng, cb) => {
293
294
  cb(null, javaProperties.parseToProperties(data.toString()));
294
295
  return;
295
296
  }
297
+ if (opt.format === 'xcstrings') {
298
+ cb(null, xcstrings2locize(data.toString()));
299
+ return;
300
+ }
296
301
  cb(new Error(`${opt.format} is not a valid format!`));
297
302
  } catch (err) {
298
303
  cb(err);
package/download.js CHANGED
@@ -12,6 +12,15 @@ const convertToDesiredFormat = require('./convertToDesiredFormat');
12
12
  const formats = require('./formats');
13
13
  const getProjectStats = require('./getProjectStats');
14
14
  const reversedFileExtensionsMap = formats.reversedFileExtensionsMap;
15
+ const locize2xcstrings = require('locize-xcstrings/cjs/locize2xcstrings');
16
+
17
+ function getInfosInUrl(download) {
18
+ const splitted = download.key.split('/');
19
+ const version = splitted[download.isPrivate ? 2 : 1];
20
+ const language = splitted[download.isPrivate ? 3 : 2];
21
+ const namespace = splitted[download.isPrivate ? 4 : 3];
22
+ return { version, language, namespace };
23
+ }
15
24
 
16
25
  function handleDownload(opt, url, err, res, downloads, cb) {
17
26
  if (err || (downloads && (downloads.errorMessage || downloads.message))) {
@@ -34,113 +43,263 @@ function handleDownload(opt, url, err, res, downloads, cb) {
34
43
  return;
35
44
  }
36
45
 
37
- async.eachLimit(downloads, 20, (download, clb) => {
38
- const splitted = download.key.split('/');
39
- const version = splitted[download.isPrivate ? 2 : 1];
40
- const lng = splitted[download.isPrivate ? 3 : 2];
41
- const namespace = splitted[download.isPrivate ? 4 : 3];
42
- opt.isPrivate = download.isPrivate;
43
-
44
- if (opt.namespace && opt.namespace !== namespace) return clb(null);
45
- if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
46
+ if (opt.format === 'xcstrings') { // 1 file per namespace including all languages
47
+ const downloadsByNamespace = {};
48
+ downloads.forEach((download) => {
49
+ const { version, namespace } = getInfosInUrl(download);
50
+ opt.isPrivate = download.isPrivate;
46
51
 
47
- getRemoteNamespace(opt, lng, namespace, (err, ns, lastModified) => {
48
- if (err) return clb(err);
52
+ downloadsByNamespace[version] = downloadsByNamespace[version] || {};
53
+ downloadsByNamespace[version][namespace] = downloadsByNamespace[version][namespace] || [];
54
+ downloadsByNamespace[version][namespace].push(download);
55
+ });
49
56
 
50
- if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
51
- return clb(null);
57
+ async.eachSeries(Object.keys(downloadsByNamespace), (version, clb) => {
58
+ async.eachLimit(Object.keys(downloadsByNamespace[version]), 20, (ns, clb) => {
59
+ if (opt.namespace && opt.namespace !== ns) return clb(null);
60
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(ns) < 0) return clb(null);
61
+
62
+ const locizeData = {
63
+ sourceLng: opt.referenceLanguage,
64
+ resources: {}
65
+ };
66
+ async.eachLimit(downloadsByNamespace[version][ns], 20, (download, clb2) => {
67
+ const { language } = getInfosInUrl(download);
68
+ getRemoteNamespace(opt, language, ns, (err, ns, lastModified) => {
69
+ if (err) return clb2(err);
70
+
71
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
72
+ return clb2(null);
73
+ }
74
+
75
+ locizeData.resources[language] = ns;
76
+ clb2();
77
+ });
78
+ }, (err) => {
79
+ if (err) return clb(err);
80
+
81
+ try {
82
+ const result = locize2xcstrings(locizeData);
83
+ const converted = JSON.stringify(result, null, 2);
84
+
85
+ var filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, '').replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, ns) + reversedFileExtensionsMap[opt.format];
86
+ var mkdirPath;
87
+ if (filledMask.lastIndexOf(path.sep) > 0) {
88
+ mkdirPath = filledMask.substring(0, filledMask.lastIndexOf(path.sep));
89
+ }
90
+
91
+ function logAndClb(err) {
92
+ if (err) return clb(err);
93
+ if (!cb) console.log(colors.green(`downloaded ${version}/${ns} to ${opt.path}...`));
94
+ if (clb) clb(null);
95
+ }
96
+
97
+ if (!opt.version) {
98
+ if (mkdirPath) mkdirp.sync(path.join(opt.path, version, mkdirPath));
99
+ fs.writeFile(path.join(opt.path, version, filledMask), converted, logAndClb);
100
+ return;
101
+ }
102
+
103
+ if (mkdirPath) mkdirp.sync(path.join(opt.path, mkdirPath));
104
+ fs.writeFile(path.join(opt.path, filledMask), converted, logAndClb);
105
+ } catch (e) {
106
+ err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
107
+ return clb(err);
108
+ }
109
+ });
110
+ }, clb);
111
+ }, (err) => {
112
+ if (err) {
113
+ if (!cb) {
114
+ console.error(colors.red(err.message));
115
+ process.exit(1);
116
+ }
117
+ if (cb) cb(err);
118
+ return;
52
119
  }
53
120
 
54
- convertToDesiredFormat(opt, namespace, lng, ns, lastModified, (err, converted) => {
55
- if (err) {
56
- err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
57
- return clb(err);
58
- }
59
- var filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
60
- var mkdirPath;
61
- if (filledMask.lastIndexOf(path.sep) > 0) {
62
- mkdirPath = filledMask.substring(0, filledMask.lastIndexOf(path.sep));
63
- }
64
- if (!opt.version) {
65
- if (mkdirPath) mkdirp.sync(path.join(opt.path, version, mkdirPath));
66
- fs.writeFile(path.join(opt.path, version, filledMask), converted, clb);
67
- return;
68
- }
69
- if (!opt.language) {
70
- if (mkdirPath) mkdirp.sync(path.join(opt.path, mkdirPath));
71
- fs.writeFile(path.join(opt.path, filledMask), converted, clb);
72
- return;
121
+ if (cb) cb(null);
122
+ });
123
+ } else { // 1 file per namespace/lng
124
+ async.eachLimit(downloads, 20, (download, clb) => {
125
+ const { version, language, namespace } = getInfosInUrl(download);
126
+ opt.isPrivate = download.isPrivate;
127
+
128
+ if (opt.namespace && opt.namespace !== namespace) return clb(null);
129
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
130
+
131
+ getRemoteNamespace(opt, language, namespace, (err, ns, lastModified) => {
132
+ if (err) return clb(err);
133
+
134
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
135
+ return clb(null);
73
136
  }
74
137
 
75
- if (filledMask.indexOf(path.sep) > 0) filledMask = filledMask.replace(opt.languageFolderPrefix + lng, '');
76
- const parentDir = path.dirname(path.join(opt.path, filledMask));
77
- mkdirp.sync(parentDir);
78
- fs.writeFile(path.join(opt.path, filledMask), converted, clb);
138
+ convertToDesiredFormat(opt, namespace, language, ns, lastModified, (err, converted) => {
139
+ if (err) {
140
+ err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
141
+ return clb(err);
142
+ }
143
+ var filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, language).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
144
+ var mkdirPath;
145
+ if (filledMask.lastIndexOf(path.sep) > 0) {
146
+ mkdirPath = filledMask.substring(0, filledMask.lastIndexOf(path.sep));
147
+ }
148
+ if (!opt.version) {
149
+ if (mkdirPath) mkdirp.sync(path.join(opt.path, version, mkdirPath));
150
+ fs.writeFile(path.join(opt.path, version, filledMask), converted, clb);
151
+ return;
152
+ }
153
+ if (!opt.language) {
154
+ if (mkdirPath) mkdirp.sync(path.join(opt.path, mkdirPath));
155
+ fs.writeFile(path.join(opt.path, filledMask), converted, clb);
156
+ return;
157
+ }
158
+
159
+ if (filledMask.indexOf(path.sep) > 0) filledMask = filledMask.replace(opt.languageFolderPrefix + language, '');
160
+ const parentDir = path.dirname(path.join(opt.path, filledMask));
161
+ mkdirp.sync(parentDir);
162
+ fs.writeFile(path.join(opt.path, filledMask), converted, clb);
163
+ });
79
164
  });
80
- });
81
- }, (err) => {
82
- if (err) {
83
- if (!cb) {
84
- console.error(colors.red(err.message));
85
- process.exit(1);
165
+ }, (err) => {
166
+ if (err) {
167
+ if (!cb) {
168
+ console.error(colors.red(err.message));
169
+ process.exit(1);
170
+ }
171
+ if (cb) cb(err);
172
+ return;
86
173
  }
87
- if (cb) cb(err);
88
- return;
89
- }
90
174
 
91
- if (!cb) console.log(colors.green(`downloaded ${url} to ${opt.path}...`));
92
- if (cb) cb(null);
93
- });
175
+ if (!cb) console.log(colors.green(`downloaded ${url} to ${opt.path}...`));
176
+ if (cb) cb(null);
177
+ });
178
+ }
94
179
  }
95
180
 
96
181
  function handlePull(opt, toDownload, cb) {
97
182
  const url = opt.apiPath + '/pull/' + opt.projectId + '/' + opt.version;
98
- async.eachLimit(toDownload, 5, (download, clb) => {
99
- const lng = download.language;
100
- const namespace = download.namespace;
101
183
 
102
- getRemoteNamespace(opt, lng, namespace, (err, ns, lastModified) => {
103
- if (err) return clb(err);
184
+ if (opt.format === 'xcstrings') { // 1 file per namespace including all languages
185
+ const downloadsByNamespace = {};
186
+ toDownload.forEach((download) => {
187
+ const { namespace } = download;
188
+ downloadsByNamespace[namespace] = downloadsByNamespace[namespace] || [];
189
+ downloadsByNamespace[namespace].push(download);
190
+ });
104
191
 
105
- if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
106
- return clb(null);
107
- }
192
+ async.eachLimit(Object.keys(downloadsByNamespace), 5, (namespace, clb) => {
193
+ if (opt.namespace && opt.namespace !== namespace) return clb(null);
194
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
108
195
 
109
- convertToDesiredFormat(opt, namespace, lng, ns, lastModified, (err, converted) => {
110
- if (err) {
196
+ const locizeData = {
197
+ sourceLng: opt.referenceLanguage,
198
+ resources: {}
199
+ };
200
+
201
+ async.eachLimit(downloadsByNamespace[namespace], 5, (download, clb2) => {
202
+ const { language } = download;
203
+ opt.raw = true;
204
+ getRemoteNamespace(opt, language, namespace, (err, ns, lastModified) => {
205
+ if (err) return clb2(err);
206
+
207
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
208
+ return clb2(null);
209
+ }
210
+
211
+ locizeData.resources[language] = ns;
212
+ clb2();
213
+ });
214
+ }, (err) => {
215
+ if (err) return clb(err);
216
+
217
+ try {
218
+ const result = locize2xcstrings(locizeData);
219
+ const converted = JSON.stringify(result, null, 2);
220
+
221
+ var filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, '').replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
222
+ var mkdirPath;
223
+ if (filledMask.lastIndexOf(path.sep) > 0) {
224
+ mkdirPath = filledMask.substring(0, filledMask.lastIndexOf(path.sep));
225
+ }
226
+
227
+ function logAndClb(err) {
228
+ if (err) return clb(err);
229
+ if (!cb) console.log(colors.green(`downloaded ${opt.version}/${namespace} to ${opt.path}...`));
230
+ if (clb) clb(null);
231
+ }
232
+
233
+ if (mkdirPath) mkdirp.sync(path.join(opt.path, mkdirPath));
234
+ fs.writeFile(path.join(opt.path, filledMask), converted, logAndClb);
235
+ } catch (e) {
111
236
  err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
112
237
  return clb(err);
113
238
  }
114
- var filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
115
- var mkdirPath;
116
- if (filledMask.lastIndexOf(path.sep) > 0) {
117
- mkdirPath = filledMask.substring(0, filledMask.lastIndexOf(path.sep));
239
+ });
240
+ }, (err) => {
241
+ if (err) {
242
+ if (!cb) {
243
+ console.error(colors.red(err.message));
244
+ process.exit(1);
118
245
  }
119
- if (!opt.language) {
120
- if (mkdirPath) mkdirp.sync(path.join(opt.path, mkdirPath));
121
- fs.writeFile(path.join(opt.path, filledMask), converted, clb);
122
- return;
246
+ if (cb) cb(err);
247
+ return;
248
+ }
249
+
250
+ if (cb) cb(null);
251
+ });
252
+ } else { // 1 file per namespace/lng
253
+ async.eachLimit(toDownload, 5, (download, clb) => {
254
+ const lng = download.language;
255
+ const namespace = download.namespace;
256
+
257
+ if (opt.namespace && opt.namespace !== namespace) return clb(null);
258
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
259
+
260
+ getRemoteNamespace(opt, lng, namespace, (err, ns, lastModified) => {
261
+ if (err) return clb(err);
262
+
263
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
264
+ return clb(null);
123
265
  }
124
266
 
125
- if (filledMask.indexOf(path.sep) > 0) filledMask = filledMask.replace(opt.languageFolderPrefix + lng, '');
126
- const parentDir = path.dirname(path.join(opt.path, filledMask));
127
- mkdirp.sync(parentDir);
128
- fs.writeFile(path.join(opt.path, filledMask), converted, clb);
267
+ convertToDesiredFormat(opt, namespace, lng, ns, lastModified, (err, converted) => {
268
+ if (err) {
269
+ err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
270
+ return clb(err);
271
+ }
272
+ var filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
273
+ var mkdirPath;
274
+ if (filledMask.lastIndexOf(path.sep) > 0) {
275
+ mkdirPath = filledMask.substring(0, filledMask.lastIndexOf(path.sep));
276
+ }
277
+ if (!opt.language) {
278
+ if (mkdirPath) mkdirp.sync(path.join(opt.path, mkdirPath));
279
+ fs.writeFile(path.join(opt.path, filledMask), converted, clb);
280
+ return;
281
+ }
282
+
283
+ if (filledMask.indexOf(path.sep) > 0) filledMask = filledMask.replace(opt.languageFolderPrefix + lng, '');
284
+ const parentDir = path.dirname(path.join(opt.path, filledMask));
285
+ mkdirp.sync(parentDir);
286
+ fs.writeFile(path.join(opt.path, filledMask), converted, clb);
287
+ });
129
288
  });
130
- });
131
- }, (err) => {
132
- if (err) {
133
- if (!cb) {
134
- console.error(colors.red(err.message));
135
- process.exit(1);
289
+ }, (err) => {
290
+ if (err) {
291
+ if (!cb) {
292
+ console.error(colors.red(err.message));
293
+ process.exit(1);
294
+ }
295
+ if (cb) cb(err);
296
+ return;
136
297
  }
137
- if (cb) cb(err);
138
- return;
139
- }
140
298
 
141
- if (!cb) console.log(colors.green(`downloaded ${url} to ${opt.path}...`));
142
- if (cb) cb(null);
143
- });
299
+ if (!cb) console.log(colors.green(`downloaded ${url} to ${opt.path}...`));
300
+ if (cb) cb(null);
301
+ });
302
+ }
144
303
  }
145
304
 
146
305
  const handleError = (err, cb) => {
package/formats.js CHANGED
@@ -13,7 +13,8 @@ const fileExtensionsMap = {
13
13
  '.ftl': ['fluent'],
14
14
  '.tmx': ['tmx'],
15
15
  '.php': ['laravel'],
16
- '.properties': ['properties']
16
+ '.properties': ['properties'],
17
+ '.xcstrings': ['xcstrings']
17
18
  };
18
19
 
19
20
  const acceptedFileExtensions = Object.keys(fileExtensionsMap);
@@ -4,9 +4,26 @@ const sortFlatResources = require('./sortFlatResources');
4
4
 
5
5
  const getRandomDelay = (delayFrom, delayTo) => Math.floor(Math.random() * delayTo) + delayFrom;
6
6
 
7
+ function onlyKeysFlat(resources, prefix, ret) {
8
+ if (!resources) resources;
9
+ ret = ret || {};
10
+ Object.keys(resources).forEach((k) => {
11
+ if (typeof resources[k] === 'string' || !resources[k] || typeof resources[k].value === 'string') {
12
+ if (prefix) {
13
+ ret[prefix + '.' + k] = resources[k];
14
+ } else {
15
+ ret[k] = resources[k];
16
+ }
17
+ } else {
18
+ onlyKeysFlat(resources[k], prefix ? prefix + '.' + k : k, ret);
19
+ }
20
+ });
21
+ return ret;
22
+ }
23
+
7
24
  const pullNamespacePaged = (opt, lng, ns, cb, next, retry) => {
8
25
  next = next || '';
9
- request(opt.apiPath + '/pull/' + opt.projectId + '/' + opt.version + '/' + lng + '/' + ns + '?' + 'next=' + next + '&ts=' + Date.now(), {
26
+ request(opt.apiPath + '/pull/' + opt.projectId + '/' + opt.version + '/' + lng + '/' + ns + '?' + 'next=' + next + (opt.raw ? '&raw=true' : '') + '&ts=' + Date.now(), {
10
27
  method: 'get',
11
28
  headers: {
12
29
  'Authorization': opt.apiKey
@@ -31,7 +48,7 @@ const pullNamespacePaged = (opt, lng, ns, cb, next, retry) => {
31
48
  }
32
49
 
33
50
  cb(null, {
34
- result: sortFlatResources(flatten(obj)),
51
+ result: opt.raw ? sortFlatResources(onlyKeysFlat(obj)) : sortFlatResources(flatten(obj)),
35
52
  next: res.headers.get('x-next-page'),
36
53
  lastModified: res.headers.get('last-modified') ? new Date(res.headers.get('last-modified')) : undefined
37
54
  });
package/lngs.json CHANGED
@@ -111,6 +111,7 @@
111
111
  "kg",
112
112
  "ko",
113
113
  "ku",
114
+ "ckb",
114
115
  "kj",
115
116
  "la",
116
117
  "lb",
package/migrate.js CHANGED
@@ -69,28 +69,79 @@ const transfer = (opt, ns, cb) => {
69
69
 
70
70
  if (!opt.replace) url = url.replace('/update/', '/missing/');
71
71
 
72
- request(url + `?replace=${!!opt.replace}`, {
73
- method: 'post',
74
- body: ns.value,
75
- headers: {
76
- 'Authorization': opt.apiKey
72
+ var data = ns.value;
73
+ var keysToSend = Object.keys(data).length;
74
+ if (keysToSend === 0) return cb(null);
75
+
76
+ var payloadKeysLimit = 1000;
77
+
78
+ function send(d, so, isFirst, clb, isRetrying) {
79
+ const queryParams = new URLSearchParams();
80
+ if (so) {
81
+ queryParams.append('omitstatsgeneration', 'true');
77
82
  }
78
- }, (err, res, obj) => {
79
- if (err || (obj && (obj.errorMessage || obj.message))) {
80
- if (url.indexOf('/missing/') > -1 && res.status === 412) {
81
- console.log(colors.green(`transfered ${opt.version}/${ns.language}/${ns.namespace} (but all keys already existed)...`));
82
- cb(null);
83
- return;
83
+ if (isFirst && opt.replace) {
84
+ queryParams.append('replace', 'true');
85
+ }
86
+
87
+ const queryString = queryParams.size > 0 ? '?' + queryParams.toString() : '';
88
+
89
+ request(url + queryString, {
90
+ method: 'post',
91
+ body: d,
92
+ headers: {
93
+ 'Authorization': opt.apiKey
84
94
  }
85
- console.log(colors.red(`transfer failed for ${opt.version}/${ns.language}/${ns.namespace}...`));
95
+ }, (err, res, obj) => {
96
+ if (err || (obj && (obj.errorMessage || obj.message))) {
97
+ if (url.indexOf('/missing/') > -1 && res.status === 412) {
98
+ console.log(colors.green(`transfered ${Object.keys(d).length} keys ${opt.version}/${ns.language}/${ns.namespace} (but all keys already existed)...`));
99
+ clb(null);
100
+ return;
101
+ }
102
+ if (res.status === 504 && !isRetrying) {
103
+ return setTimeout(() => send(d, so, isFirst, clb, true), 3000);
104
+ }
105
+ console.log(colors.red(`transfer failed for ${Object.keys(d).length} keys ${opt.version}/${ns.language}/${ns.namespace}...`));
86
106
 
87
- if (err) return cb(err);
88
- if (obj && (obj.errorMessage || obj.message)) return cb(new Error((obj.errorMessage || obj.message)));
107
+ if (err) return clb(err);
108
+ if (obj && (obj.errorMessage || obj.message)) return clb(new Error((obj.errorMessage || obj.message)));
109
+ }
110
+ if (res.status >= 300 && res.status !== 412) {
111
+ if (obj && (obj.errorMessage || obj.message)) {
112
+ return clb(new Error((obj.errorMessage || obj.message)));
113
+ }
114
+ return clb(new Error(res.statusText + ' (' + res.status + ')'));
115
+ }
116
+ console.log(colors.green(`transfered ${Object.keys(d).length} keys ${opt.version}/${ns.language}/${ns.namespace}...`));
117
+ clb(null);
118
+ });
119
+ }
120
+
121
+ if (keysToSend > payloadKeysLimit) {
122
+ var tasks = [];
123
+ var keysInObj = Object.keys(data);
124
+
125
+ while (keysInObj.length > payloadKeysLimit) {
126
+ (function() {
127
+ var pagedData = {};
128
+ keysInObj.splice(0, payloadKeysLimit).forEach((k) => pagedData[k] = data[k]);
129
+ var hasMoreKeys = keysInObj.length > 0;
130
+ tasks.push((c) => send(pagedData, hasMoreKeys, false, c));
131
+ })();
89
132
  }
90
- if (res.status >= 300 && res.status !== 412) return cb(new Error(res.statusText + ' (' + res.status + ')'));
91
- console.log(colors.green(`transfered ${opt.version}/${ns.language}/${ns.namespace}...`));
92
- cb(null);
93
- });
133
+
134
+ if (keysInObj.length === 0) return cb(null);
135
+
136
+ var finalPagedData = {};
137
+ keysInObj.splice(0, keysInObj.length).forEach((k) => finalPagedData[k] = data[k]);
138
+ tasks.push((c) => send(finalPagedData, false, false, c));
139
+
140
+ async.series(tasks, cb);
141
+ return;
142
+ }
143
+
144
+ send(data, false, true, cb);
94
145
  };
95
146
 
96
147
  const upload = (opt, nss, cb) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locize-cli",
3
- "version": "8.6.1",
3
+ "version": "8.7.0",
4
4
  "description": "locize cli to import locales",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "ini": "4.1.3",
24
24
  "js-yaml": "4.1.0",
25
25
  "laravelphp": "2.0.4",
26
+ "locize-xcstrings": "1.0.0",
26
27
  "lodash.clonedeep": "4.5.0",
27
28
  "mkdirp": "3.0.1",
28
29
  "node-fetch": "2.7.0",
@@ -34,9 +35,9 @@
34
35
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
35
36
  },
36
37
  "devDependencies": {
38
+ "@yao-pkg/pkg": "6.3.1",
37
39
  "eslint": "8.56.0",
38
- "gh-release": "7.0.2",
39
- "@yao-pkg/pkg": "6.3.1"
40
+ "gh-release": "7.0.2"
40
41
  },
41
42
  "scripts": {
42
43
  "lint": "eslint .",
@@ -6,6 +6,7 @@ const convertToFlatFormat = require('./convertToFlatFormat');
6
6
  const formats = require('./formats');
7
7
  const fileExtensionsMap = formats.fileExtensionsMap;
8
8
  const acceptedFileExtensions = formats.acceptedFileExtensions;
9
+ const xcstrings2locize = require('locize-xcstrings/cjs/xcstrings2locize');
9
10
 
10
11
  const getFiles = (srcpath) => {
11
12
  return fs.readdirSync(srcpath).filter((file) => {
@@ -21,7 +22,7 @@ const getDirectories = (srcpath) => {
21
22
 
22
23
  const parseLocalLanguage = (opt, lng, cb) => {
23
24
  const hasNamespaceInPath = opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) > -1;
24
- const filledLngMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng);
25
+ const filledLngMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, opt.format === 'xcstrings' ? '' : lng);
25
26
  var firstPartLngMask, lastPartLngMask;
26
27
  if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`)) {
27
28
  const secondPartMask = opt.pathMask.substring(opt.pathMask.lastIndexOf(path.sep) + 1);
@@ -138,28 +139,63 @@ const parseLocalLanguage = (opt, lng, cb) => {
138
139
  }
139
140
  }
140
141
 
141
- convertToFlatFormat(opt, data, lng, (err, content) => {
142
- if (err) {
142
+ if (opt.format === 'xcstrings') { // 1 file per namespace including all languages
143
+ try {
144
+ const content = xcstrings2locize(JSON.parse(data));
145
+
146
+ fs.stat(fPath, (err, stat) => {
147
+ if (err) return clb(err);
148
+
149
+ clb(null, Object.keys(content.resources).map((l) => ({
150
+ namespace: namespace,
151
+ path: fPath,
152
+ extension: fExt,
153
+ content: content.resources[l],
154
+ language: l,
155
+ mtime: stat.mtime
156
+ })));
157
+ });
158
+ } catch (e) {
143
159
  err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
144
160
  err.message += '\n' + fPath;
145
161
  return clb(err);
146
162
  }
163
+ } else { // 1 file per namespace/lng
164
+ convertToFlatFormat(opt, data, lng, (err, content) => {
165
+ if (err) {
166
+ err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
167
+ err.message += '\n' + fPath;
168
+ return clb(err);
169
+ }
147
170
 
148
- fs.stat(fPath, (err, stat) => {
149
- if (err) return clb(err);
171
+ fs.stat(fPath, (err, stat) => {
172
+ if (err) return clb(err);
150
173
 
151
- clb(null, {
152
- namespace: namespace,
153
- path: fPath,
154
- extension: fExt,
155
- content: content,
156
- language: lng,
157
- mtime: stat.mtime
174
+ clb(null, {
175
+ namespace: namespace,
176
+ path: fPath,
177
+ extension: fExt,
178
+ content: content,
179
+ language: lng,
180
+ mtime: stat.mtime
181
+ });
158
182
  });
159
183
  });
160
- });
184
+ }
161
185
  });
162
- }, cb);
186
+ }, (err, ret) => {
187
+ if (err) return cb(err);
188
+ // xcstrings, returns array in array
189
+ const r = ret.reduce((prev, cur) => {
190
+ if (Array.isArray(cur)) {
191
+ prev = prev.concat(cur);
192
+ } else {
193
+ prev.push(cur);
194
+ }
195
+ return prev;
196
+ }, []);
197
+ cb(null, r);
198
+ });
163
199
  };
164
200
 
165
201
  module.exports = parseLocalLanguage;
@@ -4,7 +4,7 @@ const filterNamespaces = require('./filterNamespaces');
4
4
  const parseLocalReference = (opt, cb) => parseLocalLanguage(opt, opt.referenceLanguage, (err, nss) => {
5
5
  if (err) return cb(err);
6
6
 
7
- cb(err, filterNamespaces(opt, nss));
7
+ cb(err, filterNamespaces(opt, nss).filter((n) => n.language === opt.referenceLanguage));
8
8
  });
9
9
 
10
10
  module.exports = parseLocalReference;
package/sync.js CHANGED
@@ -17,6 +17,7 @@ const lngCodes = require('./lngs.json');
17
17
  const deleteNamespace = require('./deleteNamespace');
18
18
  const getProjectStats = require('./getProjectStats');
19
19
  const reversedFileExtensionsMap = formats.reversedFileExtensionsMap;
20
+ const locize2xcstrings = require('locize-xcstrings/cjs/locize2xcstrings');
20
21
 
21
22
  const getDirectories = (srcpath) => {
22
23
  return fs.readdirSync(srcpath).filter((file) => {
@@ -24,6 +25,14 @@ const getDirectories = (srcpath) => {
24
25
  });
25
26
  };
26
27
 
28
+ function getInfosInUrl(download) {
29
+ const splitted = download.key.split('/');
30
+ const version = splitted[download.isPrivate ? 2 : 1];
31
+ const language = splitted[download.isPrivate ? 3 : 2];
32
+ const namespace = splitted[download.isPrivate ? 4 : 3];
33
+ return { version, language, namespace };
34
+ }
35
+
27
36
  const getDownloads = (opt, cb) => {
28
37
  if (!opt.unpublished) {
29
38
  request(opt.apiPath + '/download/' + opt.projectId + '/' + opt.version, {
@@ -194,7 +203,7 @@ const downloadAll = (opt, remoteLanguages, omitRef, manipulate, cb) => {
194
203
  }
195
204
  }
196
205
 
197
- if (!opt.dry) cleanupLanguages(opt, remoteLanguages);
206
+ if (!opt.dry && opt.format !== 'xcstrings') cleanupLanguages(opt, remoteLanguages);
198
207
 
199
208
  getDownloads(opt, (err, downloads) => {
200
209
  if (err) return cb(err);
@@ -208,43 +217,101 @@ const downloadAll = (opt, remoteLanguages, omitRef, manipulate, cb) => {
208
217
  return lng !== opt.referenceLanguage;
209
218
  });
210
219
  }
211
- async.eachLimit(downloads, opt.unpublished ? 5 : 20, (download, clb) => {
212
- const splitted = download.key.split('/');
213
- const lng = splitted[download.isPrivate ? 3 : 2];
214
- const namespace = splitted[download.isPrivate ? 4 : 3];
215
- opt.isPrivate = download.isPrivate;
216
-
217
- if (opt.language && opt.language !== lng && lng !== opt.referenceLanguage) return clb(null);
218
- if (opt.languages && opt.languages.length > 0 && opt.languages.indexOf(lng) < 0 && lng !== opt.referenceLanguage) return clb(null);
219
- if (opt.namespace && opt.namespace !== namespace) return clb(null);
220
- if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
221
-
222
- getRemoteNamespace(opt, lng, namespace, (err, ns, lastModified) => {
223
- if (err) return clb(err);
224
-
225
- if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
226
- return clb(null);
227
- }
228
220
 
229
- if (manipulate && typeof manipulate == 'function') manipulate(lng, namespace, ns);
221
+ if (opt.format === 'xcstrings') { // 1 file per namespace including all languages
222
+ const downloadsByNamespace = {};
223
+ downloads.forEach((download) => {
224
+ const { namespace } = getInfosInUrl(download);
225
+ downloadsByNamespace[namespace] = downloadsByNamespace[namespace] || [];
226
+ downloadsByNamespace[namespace].push(download);
227
+ });
228
+
229
+ async.eachLimit(Object.keys(downloadsByNamespace), opt.unpublished ? 5 : 20, (namespace, clb) => {
230
+ const locizeData = {
231
+ sourceLng: opt.referenceLanguage,
232
+ resources: {}
233
+ };
234
+
235
+ async.eachLimit(downloadsByNamespace[namespace], opt.unpublished ? 5 : 20, (download, clb) => {
236
+ const { language } = getInfosInUrl(download);
237
+ opt.isPrivate = download.isPrivate;
238
+
239
+ if (opt.language && opt.language !== language && language !== opt.referenceLanguage) return clb(null);
240
+ if (opt.languages && opt.languages.length > 0 && opt.languages.indexOf(language) < 0 && language !== opt.referenceLanguage) return clb(null);
241
+ if (opt.namespace && opt.namespace !== namespace) return clb(null);
242
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
243
+
244
+ if (opt.unpublished) opt.raw = true;
245
+ getRemoteNamespace(opt, language, namespace, (err, ns, lastModified) => {
246
+ if (err) return clb(err);
247
+
248
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
249
+ return clb(null);
250
+ }
251
+
252
+ if (manipulate && typeof manipulate == 'function') manipulate(language, namespace, ns);
253
+
254
+ locizeData.resources[language] = ns;
255
+ clb();
256
+ });
257
+ }, (err) => {
258
+ if (err) return clb(err);
259
+
260
+ try {
261
+ const result = locize2xcstrings(locizeData);
262
+ const converted = JSON.stringify(result, null, 2);
230
263
 
231
- convertToDesiredFormat(opt, namespace, lng, ns, lastModified, (err, converted) => {
232
- if (err) {
264
+ const filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, '').replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
265
+ if (opt.dry) return clb(null);
266
+ if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) && filledMask.lastIndexOf(path.sep) > 0) {
267
+ mkdirp.sync(path.join(opt.path, filledMask.substring(0, filledMask.lastIndexOf(path.sep))));
268
+ }
269
+ const parentDir = path.dirname(path.join(opt.path, filledMask));
270
+ mkdirp.sync(parentDir);
271
+ fs.writeFile(path.join(opt.path, filledMask), converted, clb);
272
+ } catch (e) {
233
273
  err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
234
274
  return clb(err);
235
275
  }
236
-
237
- const filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, lng).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
238
- if (opt.dry) return clb(null);
239
- if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) && filledMask.lastIndexOf(path.sep) > 0) {
240
- mkdirp.sync(path.join(opt.path, filledMask.substring(0, filledMask.lastIndexOf(path.sep))));
276
+ });
277
+ }, cb);
278
+ } else { // 1 file per namespace/lng
279
+ async.eachLimit(downloads, opt.unpublished ? 5 : 20, (download, clb) => {
280
+ const { language, namespace } = getInfosInUrl(download);
281
+ opt.isPrivate = download.isPrivate;
282
+
283
+ if (opt.language && opt.language !== language && language !== opt.referenceLanguage) return clb(null);
284
+ if (opt.languages && opt.languages.length > 0 && opt.languages.indexOf(language) < 0 && language !== opt.referenceLanguage) return clb(null);
285
+ if (opt.namespace && opt.namespace !== namespace) return clb(null);
286
+ if (opt.namespaces && opt.namespaces.length > 0 && opt.namespaces.indexOf(namespace) < 0) return clb(null);
287
+
288
+ getRemoteNamespace(opt, language, namespace, (err, ns, lastModified) => {
289
+ if (err) return clb(err);
290
+
291
+ if (opt.skipEmpty && Object.keys(flatten(ns)).length === 0) {
292
+ return clb(null);
241
293
  }
242
- const parentDir = path.dirname(path.join(opt.path, filledMask));
243
- mkdirp.sync(parentDir);
244
- fs.writeFile(path.join(opt.path, filledMask), converted, clb);
294
+
295
+ if (manipulate && typeof manipulate == 'function') manipulate(language, namespace, ns);
296
+
297
+ convertToDesiredFormat(opt, namespace, language, ns, lastModified, (err, converted) => {
298
+ if (err) {
299
+ err.message = 'Invalid content for "' + opt.format + '" format!\n' + (err.message || '');
300
+ return clb(err);
301
+ }
302
+
303
+ const filledMask = opt.pathMask.replace(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`, language).replace(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`, namespace) + reversedFileExtensionsMap[opt.format];
304
+ if (opt.dry) return clb(null);
305
+ if (opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}language${opt.pathMaskInterpolationSuffix}`) > opt.pathMask.indexOf(`${opt.pathMaskInterpolationPrefix}namespace${opt.pathMaskInterpolationSuffix}`) && filledMask.lastIndexOf(path.sep) > 0) {
306
+ mkdirp.sync(path.join(opt.path, filledMask.substring(0, filledMask.lastIndexOf(path.sep))));
307
+ }
308
+ const parentDir = path.dirname(path.join(opt.path, filledMask));
309
+ mkdirp.sync(parentDir);
310
+ fs.writeFile(path.join(opt.path, filledMask), converted, clb);
311
+ });
245
312
  });
246
- });
247
- }, cb);
313
+ }, cb);
314
+ }
248
315
  });
249
316
  };
250
317
 
@@ -272,7 +339,6 @@ const update = (opt, lng, ns, shouldOmit, cb) => {
272
339
  queryParams.append('autotranslate', 'true');
273
340
  }
274
341
  if (so) {
275
- // no API docs for this
276
342
  queryParams.append('omitstatsgeneration', 'true');
277
343
  }
278
344