locize-cli 12.3.1 → 12.5.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,35 @@ 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
+ ## [12.5.0](https://github.com/locize/locize-cli/compare/v12.4.0...v12.5.0) - 2026-07-15
9
+
10
+ - sync: removed keys are now listed by name in real runs too (previously the
11
+ names were only shown with `--dry`), capped at 50 keys per namespace, so a
12
+ sync always shows exactly what it deleted. When keys are removed without
13
+ `--backup-deleted-path`, a one-line tip now points to `-B` to keep a local
14
+ backup of the removed segments ([98](https://github.com/locize/locize-cli/issues/98)).
15
+ - remove: accepts multiple keys (`locize remove common title subtitle ...`),
16
+ batched into a single update request per language ([53](https://github.com/locize/locize-cli/issues/53)).
17
+ - docs: README notes for pnpm >= 11 (`blockExoticSubdeps` and why `xlsx` is
18
+ pinned to the SheetJS CDN tarball) and for the xcstrings format (file-level
19
+ conversion; i18next plural suffixes / `{{...}}` interpolation are not
20
+ rewritten to String Catalog plural variations / format specifiers).
21
+
22
+ ## [12.4.0](https://github.com/locize/locize-cli/compare/v12.3.1...v12.4.0) - 2026-07-06
23
+
24
+ - xliff: proper support for inline elements (e.g. Angular's
25
+ `<x id="INTERPOLATION" equiv-text="{{ count }}"/>` placeholders, or
26
+ `<ph>`/`<pc>`/`<g>`/`<bpt>` tags from other TMS exports). On import they are
27
+ flattened into stable placeholder tokens inside the translation value
28
+ (previously such values were silently corrupted into arrays); on export the
29
+ tokens are restored as real XLIFF inline elements, so Angular `.xlf` files
30
+ round-trip cleanly. Requires `xliff` >= 6.4.0 (the encoding is implemented
31
+ in the library and shared with the locize CAT editor).
32
+ - xliff: new formats `xliff21`, `xliff22`, `xlf21`, `xlf22` for XLIFF 2.1 and
33
+ 2.2 (import of 2.1/2.2 files already worked via `xliff2`; exports now emit
34
+ the matching `version` attribute). XLIFF 2.1 exports carry the
35
+ OASIS-correct `urn:oasis:names:tc:xliff:document:2.0` core namespace.
36
+
8
37
  ## [12.3.1](https://github.com/locize/locize-cli/compare/v12.3.0...v12.3.1) - 2026-06-18
9
38
 
10
39
  - request: retry transient network failures with exponential backoff and jitter
package/README.md CHANGED
@@ -12,6 +12,11 @@
12
12
  npm install -g locize-cli
13
13
  ```
14
14
 
15
+ > ℹ️ **pnpm >= 11:** locize-cli depends on `xlsx` pinned to the official SheetJS CDN tarball, because the npm registry version is outdated and has known vulnerabilities. pnpm 11 blocks such non-registry transitive dependencies by default (`ERR_PNPM_EXOTIC_SUBDEP`). Install with npm, or opt out in your `pnpm-workspace.yaml`:
16
+ > ```yaml
17
+ > blockExoticSubdeps: false
18
+ > ```
19
+
15
20
  ### with Shell: (downloads [released](https://github.com/locize/locize-cli/releases) (linux or macos) binaries)
16
21
 
17
22
  ```sh
@@ -67,6 +72,12 @@ or
67
72
  locize remove common title
68
73
  ```
69
74
 
75
+ or remove multiple keys at once (batched into one request per language):
76
+
77
+ ```sh
78
+ locize remove common title subtitle description
79
+ ```
80
+
70
81
 
71
82
  ## Get keys
72
83
  ### Step 1: execute
@@ -105,11 +116,12 @@ locize download --cdn-type standard
105
116
  locize download --cdn-type pro
106
117
  ```
107
118
 
108
- or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
119
+ or add a format like (json, nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
109
120
 
110
121
  *Special formats:*
111
122
  - use yaml-rails to have the language code in the resulting yaml as root object
112
123
  - use yaml-rails-ns to have the namespace in the resulting yaml as first key scope
124
+ - xcstrings (Apple String Catalog): conversion happens at the file level, not at the in-string syntax level — i18next-style plural suffixes (`key_one`/`key_other`) stay separate entries (they are not merged into `variations.plural`) and interpolation placeholders like `{{name}}` are not rewritten to `%@`/`%lld` format specifiers. For a clean String Catalog export, the content should already use xcstrings-native plural variations and format specifiers.
113
125
 
114
126
  ```sh
115
127
  locize download --project-id my-project-id-93e1-442a-ab35-24331fa294ba --ver latest --language en --namespace namespace1 --path ./backup --format android
@@ -160,11 +172,12 @@ locize sync --cdn-type standard --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672
160
172
  locize sync --cdn-type pro --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba
161
173
  ```
162
174
 
163
- or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
175
+ or add a format like (json, nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
164
176
 
165
177
  *Special formats:*
166
178
  - use yaml-rails to have the language code in the resulting yaml as root object
167
179
  - use yaml-rails-ns to have the namespace in the resulting yaml as first key scope
180
+ - xcstrings (Apple String Catalog): conversion happens at the file level, not at the in-string syntax level — i18next-style plural suffixes (`key_one`/`key_other`) stay separate entries (they are not merged into `variations.plural`) and interpolation placeholders like `{{name}}` are not rewritten to `%@`/`%lld` format specifiers. For a clean String Catalog export, the content should already use xcstrings-native plural variations and format specifiers.
168
181
 
169
182
  ```sh
170
183
  locize sync --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba --format android
@@ -227,7 +240,7 @@ locize save-missing --cdn-type standard --api-key my-api-key-d9de-4f55-9855-a9ef
227
240
  locize save-missing --cdn-type pro --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba
228
241
  ```
229
242
 
230
- or add a format like (json, nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
243
+ or add a format like (json, nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings)
231
244
 
232
245
  ```sh
233
246
  locize save-missing --api-key my-api-key-d9de-4f55-9855-a9ef0ed44672 --project-id my-project-id-93e1-442a-ab35-24331fa294ba --format android
package/dist/cjs/add.js CHANGED
@@ -5,6 +5,18 @@ var flatten = require('flat');
5
5
  var getRemoteLanguages = require('./getRemoteLanguages.js');
6
6
  var request = require('./request.js');
7
7
 
8
+ const payloadOf = (opt) => {
9
+ const data = flatten(opt.data || {});
10
+ if (!opt.data) {
11
+ data[opt.key] = opt.value || null; // null will remove the key
12
+ }
13
+ return data
14
+ };
15
+
16
+ const isRemoval = (data) => Object.keys(data).length > 0 && Object.values(data).every((v) => v === undefined || v === null);
17
+
18
+ const keyLabel = (opt, data) => opt.key || Object.keys(data).join(', ');
19
+
8
20
  const _add = async (opt) => {
9
21
  const url = `${opt.apiEndpoint}/update/{{projectId}}/{{version}}/{{lng}}/{{ns}}`
10
22
  .replace('{{projectId}}', opt.projectId)
@@ -15,15 +27,14 @@ const _add = async (opt) => {
15
27
  .replace('{{ns}}', opt.namespace)
16
28
  .replace('{{namespace}}', opt.namespace);
17
29
 
18
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
19
- console.log(colors.yellow(`removing ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
20
- } else {
21
- console.log(colors.yellow(`adding ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
22
- }
30
+ const data = payloadOf(opt);
31
+ const removing = isRemoval(data);
32
+ const label = keyLabel(opt, data);
23
33
 
24
- const data = flatten(opt.data || {});
25
- if (!opt.data) {
26
- data[opt.key] = opt.value || null; // null will remove the key
34
+ if (removing) {
35
+ console.log(colors.yellow(`removing ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
36
+ } else {
37
+ console.log(colors.yellow(`adding ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
27
38
  }
28
39
 
29
40
  try {
@@ -35,10 +46,10 @@ const _add = async (opt) => {
35
46
  body: data
36
47
  });
37
48
  if (res.status >= 300 && res.status !== 412) {
38
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
39
- console.log(colors.red(`remove failed for ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
49
+ if (removing) {
50
+ console.log(colors.red(`remove failed for ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
40
51
  } else {
41
- console.log(colors.red(`add failed for ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
52
+ console.log(colors.red(`add failed for ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
42
53
  }
43
54
  if (obj && (obj.errorMessage || obj.message)) {
44
55
  console.error(colors.red((obj.errorMessage || obj.message)));
@@ -49,16 +60,16 @@ const _add = async (opt) => {
49
60
  }
50
61
  return
51
62
  }
52
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
53
- console.log(colors.green(`removed ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
63
+ if (removing) {
64
+ console.log(colors.green(`removed ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
54
65
  } else {
55
- console.log(colors.green(`added ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
66
+ console.log(colors.green(`added ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
56
67
  }
57
68
  } catch (err) {
58
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
59
- console.log(colors.red(`remove failed for ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
69
+ if (removing) {
70
+ console.log(colors.red(`remove failed for ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
60
71
  } else {
61
- console.log(colors.red(`add failed for ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
72
+ console.log(colors.red(`add failed for ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
62
73
  }
63
74
  console.error(colors.red(err.message));
64
75
  process.exit(1);
@@ -80,10 +91,11 @@ const add = async (opt) => {
80
91
  opt.language = lng;
81
92
  await _add(opt);
82
93
  }
83
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
84
- console.log(colors.green(`removed ${opt.namespace}/${opt.key} (${opt.version}) from all languages...`));
94
+ const data = payloadOf(opt);
95
+ if (isRemoval(data)) {
96
+ console.log(colors.green(`removed ${opt.namespace}/${keyLabel(opt, data)} (${opt.version}) from all languages...`));
85
97
  } else {
86
- console.log(colors.green(`added ${opt.namespace}/${opt.key} (${opt.version}) in all languages...`));
98
+ console.log(colors.green(`added ${opt.namespace}/${keyLabel(opt, data)} (${opt.version}) in all languages...`));
87
99
  }
88
100
  };
89
101
 
package/dist/cjs/cli.js CHANGED
@@ -50,7 +50,7 @@ const program = new commander.Command();
50
50
 
51
51
  program
52
52
  .description('The official locize CLI.')
53
- .version('12.3.1'); // This string is replaced with the actual version at build time by rollup
53
+ .version('12.5.0'); // This string is replaced with the actual version at build time by rollup
54
54
  // .option('-a, --api-endpoint <url>', `Specify the api-endpoint url that should be used (default: ${defaultApiEndpoint})`)
55
55
  // .option('-C, --config-path <configPath>', `Specify the path to the optional locize config file (default: ${configInWorkingDirectory} or ${configInHome})`);
56
56
 
@@ -189,9 +189,9 @@ program
189
189
  });
190
190
 
191
191
  program
192
- .command('remove <namespace> <key>')
192
+ .command('remove <namespace> <key...>')
193
193
  .alias('rm')
194
- .description('remove a key')
194
+ .description('remove one or more keys')
195
195
  .option('-k, --api-key <apiKey>', 'The api-key that should be used')
196
196
  .option('-i, --project-id <projectId>', 'The project-id that should be used')
197
197
  .option('-l, --language <lng>', 'The language that should be targeted (omitting this attribute will result in removing the key from all languages)')
@@ -199,7 +199,7 @@ program
199
199
  .option('-a, --api-endpoint <url>', `Specify the api-endpoint url that should be used (default: ${defaultApiEndpoint})`)
200
200
  .option('-C, --config-path <configPath>', `Specify the path to the optional locize config file (default: ${configInWorkingDirectory} or ${configInHome})`)
201
201
  .option('--ct, --cdn-type <standard|pro>', `Specify the cdn endpoint that should be used (depends on which cdn type you've in your Locize project) (default: ${defaultCdnType})`)
202
- .action((namespace, key, options) => {
202
+ .action((namespace, keys, options) => {
203
203
  try {
204
204
  config = ini.parse(fs.readFileSync(options.configPath, 'utf-8')) || config;
205
205
  } catch (e) {}
@@ -240,13 +240,16 @@ program
240
240
  language,
241
241
  version,
242
242
  namespace,
243
- key
243
+ key: keys.length === 1 ? keys[0] : undefined,
244
+ // multiple keys are batched into one update request per language
245
+ data: keys.length > 1 ? keys.reduce((m, k) => { m[k] = null; return m }, {}) : undefined
244
246
  });
245
247
  })
246
248
  .on('--help', () => {
247
249
  console.log(' Examples:');
248
250
  console.log();
249
251
  console.log(' $ locize remove common title');
252
+ console.log(' $ locize remove common title subtitle description');
250
253
  console.log(' $ locize remove common title --language en');
251
254
  console.log(' $ locize remove common title --api-key <apiKey> --project-id <projectId> --language en');
252
255
  console.log();
@@ -263,7 +266,7 @@ program
263
266
  .option('-n, --namespace <ns>', 'The namespace that should be targeted')
264
267
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
265
268
  .option('-k, --api-key <apiKey>', 'The api-key that should be used')
266
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
269
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
267
270
  .option('-s, --skip-empty <true|false>', 'Skips to download empty files (default: true)', 'true')
268
271
  .option('-P, --language-folder-prefix <prefix>', 'This will be added as a local folder name prefix in front of the language.', '')
269
272
  .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}}`)
@@ -412,7 +415,7 @@ program
412
415
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
413
416
  .option('-B, --backup-deleted-path <path>', 'Saves the segments that will be deleted in this path')
414
417
  .option('-A, --auto-create-path <true|false>', 'This will automatically make sure the --path is created. (default: true)', 'true')
415
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
418
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
416
419
  .option('-s, --skip-empty <true|false>', 'Skips to download empty files (default: false)', 'false')
417
420
  .option('-c, --clean <true|false>', 'Removes all local files by removing the whole folder (default: false)', 'false')
418
421
  .option('--cf, --clean-local-files <true|false>', 'Removes all local files without removing any folder (default: false)', 'false')
@@ -540,7 +543,7 @@ program
540
543
  .option('-i, --project-id <projectId>', 'The project-id that should be used')
541
544
  .option('-v, --ver <version>', 'Found namespaces will be matched to this version (default: latest)')
542
545
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
543
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
546
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
544
547
  .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}}`)
545
548
  .option('-P, --language-folder-prefix <prefix>', 'This will be added as a local folder name prefix in front of the language.', '')
546
549
  .option('-d, --dry <true|false>', 'Dry run (default: false)', 'false')
@@ -835,7 +838,7 @@ program
835
838
  .command('format [fileOrDirectory]')
836
839
  .alias('ft')
837
840
  .description('format local files')
838
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
841
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
839
842
  .option('-l, --reference-language <lng>', 'Some format conversions need to know the reference language.', 'en')
840
843
  .option('-d, --dry <true|false>', 'Dry run (default: false)', 'false')
841
844
  .action((fileOrDirectory, options) => {
@@ -143,19 +143,17 @@ const convertToDesiredFormat = async (
143
143
  });
144
144
  return stringsFile.compile(data)
145
145
  }
146
- if (
147
- opt.format === 'xliff2' ||
148
- opt.format === 'xliff12' ||
149
- opt.format === 'xlf2' ||
150
- opt.format === 'xlf12'
151
- ) {
152
- const fn =
153
- opt.format === 'xliff12' || opt.format === 'xlf12'
154
- ? xliff.createxliff12
155
- : xliff.createxliff;
146
+ if (['xliff2', 'xliff21', 'xliff22', 'xliff12', 'xlf2', 'xlf21', 'xlf22', 'xlf12'].indexOf(opt.format) > -1) {
156
147
  const refNs = await opt.getNamespace(opt, opt.referenceLanguage, namespace);
157
148
  const prepared = combineSubkeyPreprocessor.prepareExport(refNs, flatten(data));
158
- return await fn(opt.referenceLanguage, lng, prepared.ref, prepared.trg, namespace)
149
+ const js = await xliff.createjs(opt.referenceLanguage, lng, prepared.ref, prepared.trg, namespace);
150
+ if (opt.format === 'xliff12' || opt.format === 'xlf12') {
151
+ return await xliff.jsToXliff12(js, { inlineAsString: true })
152
+ }
153
+ let targetXliffVersion; // default 2.0
154
+ if (opt.format === 'xliff21' || opt.format === 'xlf21') targetXliffVersion = '2.1';
155
+ if (opt.format === 'xliff22' || opt.format === 'xlf22') targetXliffVersion = '2.2';
156
+ return await xliff.js2xliff(js, { targetXliffVersion, inlineAsString: true })
159
157
  }
160
158
  if (opt.format === 'resx') {
161
159
  return await resx.js2resx(flatten(data))
@@ -116,17 +116,13 @@ const convertToFlatFormat = async (opt, data, lng) => {
116
116
  // CRLF => LF
117
117
  return stringsFile.parse(data.toString().replace(/\r\n/g, '\n'), false)
118
118
  }
119
- if (
120
- opt.format === 'xliff2' ||
121
- opt.format === 'xliff12' ||
122
- opt.format === 'xlf2' ||
123
- opt.format === 'xlf12'
124
- ) {
119
+ if (['xliff2', 'xliff21', 'xliff22', 'xliff12', 'xlf2', 'xlf21', 'xlf22', 'xlf12'].indexOf(opt.format) > -1) {
120
+ // xliff2js parses 2.0, 2.1 and 2.2
125
121
  const fn =
126
122
  opt.format === 'xliff12' || opt.format === 'xlf12'
127
123
  ? xliff.xliff12ToJs
128
124
  : xliff.xliff2js;
129
- const res = await fn(data.toString());
125
+ const res = await fn(data.toString(), { inlineAsString: true });
130
126
  res.resources = res.resources || {};
131
127
  const ns = Object.keys(res.resources)[0];
132
128
  const orgRes = res.resources[ns] || res.resources;
@@ -10,8 +10,8 @@ const fileExtensionsMap = {
10
10
  '.yaml': ['yaml', 'yaml-rails', 'yaml-rails-ns', 'yaml-nested'],
11
11
  '.yml': ['yml', 'yml-rails', 'yml-rails-ns', 'yml-nested'],
12
12
  '.xlsx': ['xlsx'],
13
- '.xliff': ['xliff2', 'xliff12'],
14
- '.xlf': ['xlf2', 'xlf12'],
13
+ '.xliff': ['xliff2', 'xliff21', 'xliff22', 'xliff12'],
14
+ '.xlf': ['xlf2', 'xlf21', 'xlf22', 'xlf12'],
15
15
  '.ftl': ['fluent'],
16
16
  '.tmx': ['tmx'],
17
17
  '.php': ['laravel'],
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "locize-cli",
3
- "version": "12.3.1",
3
+ "version": "12.5.0",
4
4
  "type": "commonjs"
5
5
  }
@@ -43,7 +43,7 @@ async function request (url, options) {
43
43
  }
44
44
 
45
45
  options.headers = options.headers || {};
46
- options.headers['User-Agent'] = `locize-cli/v12.3.1 (node/${process.version}; ${process.platform} ${process.arch})`; // This string is replaced with the actual version at build time by rollup
46
+ options.headers['User-Agent'] = `locize-cli/v12.5.0 (node/${process.version}; ${process.platform} ${process.arch})`; // This string is replaced with the actual version at build time by rollup
47
47
  options.headers['X-User-Agent'] = options.headers['User-Agent'];
48
48
  if (options.body || options.method !== 'get') options.headers['Content-Type'] = 'application/json';
49
49
  if (options.body) {
package/dist/cjs/sync.js CHANGED
@@ -195,6 +195,8 @@ const cleanupLanguages = (opt, remoteLanguages) => {
195
195
  });
196
196
  };
197
197
 
198
+ const listRemovedKeys = (keys) => keys.length <= 50 ? keys.join(', ') : `${keys.slice(0, 50).join(', ')} ... and ${keys.length - 50} more`;
199
+
198
200
  const backupDeleted = (opt, ns, now) => {
199
201
  if (opt.dry || ns.diff.toRemove.length === 0) return
200
202
  let m = now.getMonth() + 1;
@@ -529,6 +531,7 @@ async function handleSync (opt, remoteLanguages, localNamespaces) {
529
531
  const shouldOmit = lngsInReqs.length > 5 || nsInReqs.length > 5;
530
532
 
531
533
  let wasThereSomethingToUpdate = opt.autoTranslate || false;
534
+ let suggestBackupTip = false;
532
535
 
533
536
  async function updateComparedNamespaces () {
534
537
  const now = new Date();
@@ -539,14 +542,13 @@ async function handleSync (opt, remoteLanguages, localNamespaces) {
539
542
  console.log(colors.bgRed(`skipping the removal of ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}...`));
540
543
  if (opt.dry) console.log(colors.bgRed(`skipped to remove ${ns.diff.toRemove.join(', ')} in ${ns.language}/${ns.namespace}...`));
541
544
  } else {
542
- console.log(colors.red(`removing ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}...`));
543
- if (opt.dry) console.log(colors.red(`would remove ${ns.diff.toRemove.join(', ')} in ${ns.language}/${ns.namespace}...`));
545
+ console.log(colors.red(`${opt.dry ? 'would remove' : 'removing'} ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}: ${listRemovedKeys(ns.diff.toRemove)}`));
544
546
  if (!opt.dry && opt.backupDeletedPath) backupDeleted(opt, ns, now);
547
+ if (!opt.dry && !opt.backupDeletedPath) suggestBackupTip = true;
545
548
  }
546
549
  }
547
550
  if (ns.diff.toRemoveLocally.length > 0) {
548
- console.log(colors.red(`removing ${ns.diff.toRemoveLocally.length} keys in ${ns.language}/${ns.namespace} locally...`));
549
- if (opt.dry) console.log(colors.red(`would remove ${ns.diff.toRemoveLocally.join(', ')} in ${ns.language}/${ns.namespace} locally...`));
551
+ console.log(colors.red(`${opt.dry ? 'would remove' : 'removing'} ${ns.diff.toRemoveLocally.length} keys in ${ns.language}/${ns.namespace} locally: ${listRemovedKeys(ns.diff.toRemoveLocally)}`));
550
552
  }
551
553
  if (ns.diff.toAdd.length > 0) {
552
554
  console.log(colors.green(`adding ${ns.diff.toAdd.length} keys in ${ns.language}/${ns.namespace}...`));
@@ -585,6 +587,8 @@ async function handleSync (opt, remoteLanguages, localNamespaces) {
585
587
  });
586
588
  });
587
589
 
590
+ if (suggestBackupTip) console.log(colors.yellow('tip: use -B <path> (--backup-deleted-path) to keep a local backup of removed segments'));
591
+
588
592
  console.log(colors.grey('syncing...'));
589
593
 
590
594
  async function down () {
package/dist/esm/add.js CHANGED
@@ -3,6 +3,18 @@ import flatten from 'flat';
3
3
  import getRemoteLanguages from './getRemoteLanguages.js';
4
4
  import request from './request.js';
5
5
 
6
+ const payloadOf = (opt) => {
7
+ const data = flatten(opt.data || {});
8
+ if (!opt.data) {
9
+ data[opt.key] = opt.value || null; // null will remove the key
10
+ }
11
+ return data
12
+ };
13
+
14
+ const isRemoval = (data) => Object.keys(data).length > 0 && Object.values(data).every((v) => v === undefined || v === null);
15
+
16
+ const keyLabel = (opt, data) => opt.key || Object.keys(data).join(', ');
17
+
6
18
  const _add = async (opt) => {
7
19
  const url = `${opt.apiEndpoint}/update/{{projectId}}/{{version}}/{{lng}}/{{ns}}`
8
20
  .replace('{{projectId}}', opt.projectId)
@@ -13,15 +25,14 @@ const _add = async (opt) => {
13
25
  .replace('{{ns}}', opt.namespace)
14
26
  .replace('{{namespace}}', opt.namespace);
15
27
 
16
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
17
- console.log(colors.yellow(`removing ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
18
- } else {
19
- console.log(colors.yellow(`adding ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
20
- }
28
+ const data = payloadOf(opt);
29
+ const removing = isRemoval(data);
30
+ const label = keyLabel(opt, data);
21
31
 
22
- const data = flatten(opt.data || {});
23
- if (!opt.data) {
24
- data[opt.key] = opt.value || null; // null will remove the key
32
+ if (removing) {
33
+ console.log(colors.yellow(`removing ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
34
+ } else {
35
+ console.log(colors.yellow(`adding ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
25
36
  }
26
37
 
27
38
  try {
@@ -33,10 +44,10 @@ const _add = async (opt) => {
33
44
  body: data
34
45
  });
35
46
  if (res.status >= 300 && res.status !== 412) {
36
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
37
- console.log(colors.red(`remove failed for ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
47
+ if (removing) {
48
+ console.log(colors.red(`remove failed for ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
38
49
  } else {
39
- console.log(colors.red(`add failed for ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
50
+ console.log(colors.red(`add failed for ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
40
51
  }
41
52
  if (obj && (obj.errorMessage || obj.message)) {
42
53
  console.error(colors.red((obj.errorMessage || obj.message)));
@@ -47,16 +58,16 @@ const _add = async (opt) => {
47
58
  }
48
59
  return
49
60
  }
50
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
51
- console.log(colors.green(`removed ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
61
+ if (removing) {
62
+ console.log(colors.green(`removed ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
52
63
  } else {
53
- console.log(colors.green(`added ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
64
+ console.log(colors.green(`added ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
54
65
  }
55
66
  } catch (err) {
56
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
57
- console.log(colors.red(`remove failed for ${opt.key} from ${opt.version}/${opt.language}/${opt.namespace}...`));
67
+ if (removing) {
68
+ console.log(colors.red(`remove failed for ${label} from ${opt.version}/${opt.language}/${opt.namespace}...`));
58
69
  } else {
59
- console.log(colors.red(`add failed for ${opt.key} to ${opt.version}/${opt.language}/${opt.namespace}...`));
70
+ console.log(colors.red(`add failed for ${label} to ${opt.version}/${opt.language}/${opt.namespace}...`));
60
71
  }
61
72
  console.error(colors.red(err.message));
62
73
  process.exit(1);
@@ -78,10 +89,11 @@ const add = async (opt) => {
78
89
  opt.language = lng;
79
90
  await _add(opt);
80
91
  }
81
- if (!opt.data && (opt.value === undefined || opt.value === null)) {
82
- console.log(colors.green(`removed ${opt.namespace}/${opt.key} (${opt.version}) from all languages...`));
92
+ const data = payloadOf(opt);
93
+ if (isRemoval(data)) {
94
+ console.log(colors.green(`removed ${opt.namespace}/${keyLabel(opt, data)} (${opt.version}) from all languages...`));
83
95
  } else {
84
- console.log(colors.green(`added ${opt.namespace}/${opt.key} (${opt.version}) in all languages...`));
96
+ console.log(colors.green(`added ${opt.namespace}/${keyLabel(opt, data)} (${opt.version}) in all languages...`));
85
97
  }
86
98
  };
87
99
 
package/dist/esm/cli.js CHANGED
@@ -48,7 +48,7 @@ const program = new Command();
48
48
 
49
49
  program
50
50
  .description('The official locize CLI.')
51
- .version('12.3.1'); // This string is replaced with the actual version at build time by rollup
51
+ .version('12.5.0'); // This string is replaced with the actual version at build time by rollup
52
52
  // .option('-a, --api-endpoint <url>', `Specify the api-endpoint url that should be used (default: ${defaultApiEndpoint})`)
53
53
  // .option('-C, --config-path <configPath>', `Specify the path to the optional locize config file (default: ${configInWorkingDirectory} or ${configInHome})`);
54
54
 
@@ -187,9 +187,9 @@ program
187
187
  });
188
188
 
189
189
  program
190
- .command('remove <namespace> <key>')
190
+ .command('remove <namespace> <key...>')
191
191
  .alias('rm')
192
- .description('remove a key')
192
+ .description('remove one or more keys')
193
193
  .option('-k, --api-key <apiKey>', 'The api-key that should be used')
194
194
  .option('-i, --project-id <projectId>', 'The project-id that should be used')
195
195
  .option('-l, --language <lng>', 'The language that should be targeted (omitting this attribute will result in removing the key from all languages)')
@@ -197,7 +197,7 @@ program
197
197
  .option('-a, --api-endpoint <url>', `Specify the api-endpoint url that should be used (default: ${defaultApiEndpoint})`)
198
198
  .option('-C, --config-path <configPath>', `Specify the path to the optional locize config file (default: ${configInWorkingDirectory} or ${configInHome})`)
199
199
  .option('--ct, --cdn-type <standard|pro>', `Specify the cdn endpoint that should be used (depends on which cdn type you've in your Locize project) (default: ${defaultCdnType})`)
200
- .action((namespace, key, options) => {
200
+ .action((namespace, keys, options) => {
201
201
  try {
202
202
  config = ini.parse(fs.readFileSync(options.configPath, 'utf-8')) || config;
203
203
  } catch (e) {}
@@ -238,13 +238,16 @@ program
238
238
  language,
239
239
  version,
240
240
  namespace,
241
- key
241
+ key: keys.length === 1 ? keys[0] : undefined,
242
+ // multiple keys are batched into one update request per language
243
+ data: keys.length > 1 ? keys.reduce((m, k) => { m[k] = null; return m }, {}) : undefined
242
244
  });
243
245
  })
244
246
  .on('--help', () => {
245
247
  console.log(' Examples:');
246
248
  console.log();
247
249
  console.log(' $ locize remove common title');
250
+ console.log(' $ locize remove common title subtitle description');
248
251
  console.log(' $ locize remove common title --language en');
249
252
  console.log(' $ locize remove common title --api-key <apiKey> --project-id <projectId> --language en');
250
253
  console.log();
@@ -261,7 +264,7 @@ program
261
264
  .option('-n, --namespace <ns>', 'The namespace that should be targeted')
262
265
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
263
266
  .option('-k, --api-key <apiKey>', 'The api-key that should be used')
264
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
267
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
265
268
  .option('-s, --skip-empty <true|false>', 'Skips to download empty files (default: true)', 'true')
266
269
  .option('-P, --language-folder-prefix <prefix>', 'This will be added as a local folder name prefix in front of the language.', '')
267
270
  .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}}`)
@@ -410,7 +413,7 @@ program
410
413
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
411
414
  .option('-B, --backup-deleted-path <path>', 'Saves the segments that will be deleted in this path')
412
415
  .option('-A, --auto-create-path <true|false>', 'This will automatically make sure the --path is created. (default: true)', 'true')
413
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
416
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
414
417
  .option('-s, --skip-empty <true|false>', 'Skips to download empty files (default: false)', 'false')
415
418
  .option('-c, --clean <true|false>', 'Removes all local files by removing the whole folder (default: false)', 'false')
416
419
  .option('--cf, --clean-local-files <true|false>', 'Removes all local files without removing any folder (default: false)', 'false')
@@ -538,7 +541,7 @@ program
538
541
  .option('-i, --project-id <projectId>', 'The project-id that should be used')
539
542
  .option('-v, --ver <version>', 'Found namespaces will be matched to this version (default: latest)')
540
543
  .option('-p, --path <path>', `Specify the path that should be used (default: ${process.cwd()})`, process.cwd())
541
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
544
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
542
545
  .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}}`)
543
546
  .option('-P, --language-folder-prefix <prefix>', 'This will be added as a local folder name prefix in front of the language.', '')
544
547
  .option('-d, --dry <true|false>', 'Dry run (default: false)', 'false')
@@ -833,7 +836,7 @@ program
833
836
  .command('format [fileOrDirectory]')
834
837
  .alias('ft')
835
838
  .description('format local files')
836
- .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff12, xlf2, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
839
+ .option('-f, --format <json>', 'File format of namespaces (default: json; [nested, flat, xliff2, xliff21, xliff22, xliff12, xlf2, xlf21, xlf22, xlf12, android, yaml, yaml-rails, yaml-rails-ns, yaml-nested, yml, yml-rails, yml-nested, csv, xlsx, po, strings, resx, fluent, tmx, laravel, properties, xcstrings])', 'json')
837
840
  .option('-l, --reference-language <lng>', 'Some format conversions need to know the reference language.', 'en')
838
841
  .option('-d, --dry <true|false>', 'Dry run (default: false)', 'false')
839
842
  .action((fileOrDirectory, options) => {
@@ -141,19 +141,17 @@ const convertToDesiredFormat = async (
141
141
  });
142
142
  return stringsFile.compile(data)
143
143
  }
144
- if (
145
- opt.format === 'xliff2' ||
146
- opt.format === 'xliff12' ||
147
- opt.format === 'xlf2' ||
148
- opt.format === 'xlf12'
149
- ) {
150
- const fn =
151
- opt.format === 'xliff12' || opt.format === 'xlf12'
152
- ? xliff.createxliff12
153
- : xliff.createxliff;
144
+ if (['xliff2', 'xliff21', 'xliff22', 'xliff12', 'xlf2', 'xlf21', 'xlf22', 'xlf12'].indexOf(opt.format) > -1) {
154
145
  const refNs = await opt.getNamespace(opt, opt.referenceLanguage, namespace);
155
146
  const prepared = prepareExport(refNs, flatten(data));
156
- return await fn(opt.referenceLanguage, lng, prepared.ref, prepared.trg, namespace)
147
+ const js = await xliff.createjs(opt.referenceLanguage, lng, prepared.ref, prepared.trg, namespace);
148
+ if (opt.format === 'xliff12' || opt.format === 'xlf12') {
149
+ return await xliff.jsToXliff12(js, { inlineAsString: true })
150
+ }
151
+ let targetXliffVersion; // default 2.0
152
+ if (opt.format === 'xliff21' || opt.format === 'xlf21') targetXliffVersion = '2.1';
153
+ if (opt.format === 'xliff22' || opt.format === 'xlf22') targetXliffVersion = '2.2';
154
+ return await xliff.js2xliff(js, { targetXliffVersion, inlineAsString: true })
157
155
  }
158
156
  if (opt.format === 'resx') {
159
157
  return await resx.js2resx(flatten(data))
@@ -114,17 +114,13 @@ const convertToFlatFormat = async (opt, data, lng) => {
114
114
  // CRLF => LF
115
115
  return stringsFile.parse(data.toString().replace(/\r\n/g, '\n'), false)
116
116
  }
117
- if (
118
- opt.format === 'xliff2' ||
119
- opt.format === 'xliff12' ||
120
- opt.format === 'xlf2' ||
121
- opt.format === 'xlf12'
122
- ) {
117
+ if (['xliff2', 'xliff21', 'xliff22', 'xliff12', 'xlf2', 'xlf21', 'xlf22', 'xlf12'].indexOf(opt.format) > -1) {
118
+ // xliff2js parses 2.0, 2.1 and 2.2
123
119
  const fn =
124
120
  opt.format === 'xliff12' || opt.format === 'xlf12'
125
121
  ? xliff.xliff12ToJs
126
122
  : xliff.xliff2js;
127
- const res = await fn(data.toString());
123
+ const res = await fn(data.toString(), { inlineAsString: true });
128
124
  res.resources = res.resources || {};
129
125
  const ns = Object.keys(res.resources)[0];
130
126
  const orgRes = res.resources[ns] || res.resources;
@@ -8,8 +8,8 @@ const fileExtensionsMap = {
8
8
  '.yaml': ['yaml', 'yaml-rails', 'yaml-rails-ns', 'yaml-nested'],
9
9
  '.yml': ['yml', 'yml-rails', 'yml-rails-ns', 'yml-nested'],
10
10
  '.xlsx': ['xlsx'],
11
- '.xliff': ['xliff2', 'xliff12'],
12
- '.xlf': ['xlf2', 'xlf12'],
11
+ '.xliff': ['xliff2', 'xliff21', 'xliff22', 'xliff12'],
12
+ '.xlf': ['xlf2', 'xlf21', 'xlf22', 'xlf12'],
13
13
  '.ftl': ['fluent'],
14
14
  '.tmx': ['tmx'],
15
15
  '.php': ['laravel'],
@@ -41,7 +41,7 @@ async function request (url, options) {
41
41
  }
42
42
 
43
43
  options.headers = options.headers || {};
44
- options.headers['User-Agent'] = `locize-cli/v12.3.1 (node/${process.version}; ${process.platform} ${process.arch})`; // This string is replaced with the actual version at build time by rollup
44
+ options.headers['User-Agent'] = `locize-cli/v12.5.0 (node/${process.version}; ${process.platform} ${process.arch})`; // This string is replaced with the actual version at build time by rollup
45
45
  options.headers['X-User-Agent'] = options.headers['User-Agent'];
46
46
  if (options.body || options.method !== 'get') options.headers['Content-Type'] = 'application/json';
47
47
  if (options.body) {
package/dist/esm/sync.js CHANGED
@@ -193,6 +193,8 @@ const cleanupLanguages = (opt, remoteLanguages) => {
193
193
  });
194
194
  };
195
195
 
196
+ const listRemovedKeys = (keys) => keys.length <= 50 ? keys.join(', ') : `${keys.slice(0, 50).join(', ')} ... and ${keys.length - 50} more`;
197
+
196
198
  const backupDeleted = (opt, ns, now) => {
197
199
  if (opt.dry || ns.diff.toRemove.length === 0) return
198
200
  let m = now.getMonth() + 1;
@@ -527,6 +529,7 @@ async function handleSync (opt, remoteLanguages, localNamespaces) {
527
529
  const shouldOmit = lngsInReqs.length > 5 || nsInReqs.length > 5;
528
530
 
529
531
  let wasThereSomethingToUpdate = opt.autoTranslate || false;
532
+ let suggestBackupTip = false;
530
533
 
531
534
  async function updateComparedNamespaces () {
532
535
  const now = new Date();
@@ -537,14 +540,13 @@ async function handleSync (opt, remoteLanguages, localNamespaces) {
537
540
  console.log(colors.bgRed(`skipping the removal of ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}...`));
538
541
  if (opt.dry) console.log(colors.bgRed(`skipped to remove ${ns.diff.toRemove.join(', ')} in ${ns.language}/${ns.namespace}...`));
539
542
  } else {
540
- console.log(colors.red(`removing ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}...`));
541
- if (opt.dry) console.log(colors.red(`would remove ${ns.diff.toRemove.join(', ')} in ${ns.language}/${ns.namespace}...`));
543
+ console.log(colors.red(`${opt.dry ? 'would remove' : 'removing'} ${ns.diff.toRemove.length} keys in ${ns.language}/${ns.namespace}: ${listRemovedKeys(ns.diff.toRemove)}`));
542
544
  if (!opt.dry && opt.backupDeletedPath) backupDeleted(opt, ns, now);
545
+ if (!opt.dry && !opt.backupDeletedPath) suggestBackupTip = true;
543
546
  }
544
547
  }
545
548
  if (ns.diff.toRemoveLocally.length > 0) {
546
- console.log(colors.red(`removing ${ns.diff.toRemoveLocally.length} keys in ${ns.language}/${ns.namespace} locally...`));
547
- if (opt.dry) console.log(colors.red(`would remove ${ns.diff.toRemoveLocally.join(', ')} in ${ns.language}/${ns.namespace} locally...`));
549
+ console.log(colors.red(`${opt.dry ? 'would remove' : 'removing'} ${ns.diff.toRemoveLocally.length} keys in ${ns.language}/${ns.namespace} locally: ${listRemovedKeys(ns.diff.toRemoveLocally)}`));
548
550
  }
549
551
  if (ns.diff.toAdd.length > 0) {
550
552
  console.log(colors.green(`adding ${ns.diff.toAdd.length} keys in ${ns.language}/${ns.namespace}...`));
@@ -583,6 +585,8 @@ async function handleSync (opt, remoteLanguages, localNamespaces) {
583
585
  });
584
586
  });
585
587
 
588
+ if (suggestBackupTip) console.log(colors.yellow('tip: use -B <path> (--backup-deleted-path) to keep a local backup of removed segments'));
589
+
586
590
  console.log(colors.grey('syncing...'));
587
591
 
588
592
  async function down () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locize-cli",
3
- "version": "12.3.1",
3
+ "version": "12.5.0",
4
4
  "description": "locize cli to import locales",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -32,10 +32,10 @@
32
32
  "commander": "14.0.3",
33
33
  "diff": "8.0.4",
34
34
  "dotenv": "17.4.2",
35
- "fast-csv": "5.0.5",
35
+ "fast-csv": "5.0.7",
36
36
  "flat": "5.0.2",
37
37
  "fluent_conv": "3.3.0",
38
- "gettext-converter": "1.3.1",
38
+ "gettext-converter": "1.3.3",
39
39
  "https-proxy-agent": "7.0.6",
40
40
  "ini": "6.0.0",
41
41
  "laravelphp": "2.0.4",
@@ -46,20 +46,20 @@
46
46
  "rimraf": "6.1.3",
47
47
  "strings-file": "0.0.5",
48
48
  "tmexchange": "2.0.6",
49
- "xliff": "6.3.0",
49
+ "xliff": "6.4.0",
50
50
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
51
- "yaml": "2.8.3"
51
+ "yaml": "2.9.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@rollup/plugin-replace": "6.0.3",
55
- "@rollup/plugin-terser": "0.4.4",
56
- "@yao-pkg/pkg": "6.14.2",
55
+ "@rollup/plugin-terser": "1.0.0",
56
+ "@yao-pkg/pkg": "6.21.0",
57
57
  "eslint": "9.39.2",
58
58
  "eslint-plugin-import": "2.32.0",
59
- "gh-release": "7.0.2",
59
+ "gh-release": "8.1.0",
60
60
  "neostandard": "0.13.0",
61
- "rollup": "4.60.1",
62
- "vitest": "4.1.4"
61
+ "rollup": "4.62.2",
62
+ "vitest": "4.1.10"
63
63
  },
64
64
  "scripts": {
65
65
  "lint": "eslint .",
@@ -67,10 +67,10 @@
67
67
  "test:watch": "vitest",
68
68
  "test": "npm run lint && vitest run",
69
69
  "pkg": "mkdir -p ./bins && pkg ./dist/cjs/cli.js --output ./bins/locize --targets node24-linux-x64,node24-macos-x64,node24-win-x64,node24-alpine-x64",
70
- "release": "gh-release --assets ./bins/locize-linux,./bins/locize-macos,./bins/locize-win.exe,./bins/locize-alpine",
70
+ "release": "GH_RELEASE_GITHUB_API_TOKEN=$(gh auth token) gh-release --assets ./bins/locize-linux,./bins/locize-macos,./bins/locize-win.exe,./bins/locize-alpine",
71
71
  "preversion": "npm run test",
72
72
  "version": "npm run build && npm run pkg",
73
- "postversion": "git push && npm run release"
73
+ "postversion": "git push --follow-tags && npm run release"
74
74
  },
75
75
  "keywords": [
76
76
  "cli",