@tolgee/cli 2.12.1 → 2.14.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.
@@ -1,5 +1,7 @@
1
+ import { getUnresolvedConflictsMessage } from '../utils/printFailedKeys.js';
1
2
  export const addErrorDetails = (loadable, showBeError = true) => {
2
- var _a, _b, _c;
3
+ var _a, _b, _c, _d, _e;
4
+ let additionalInfo = '';
3
5
  const items = [];
4
6
  items.push(`status: ${loadable.response.status}`);
5
7
  if (showBeError && ((_a = loadable.error) === null || _a === void 0 ? void 0 : _a.code)) {
@@ -8,7 +10,11 @@ export const addErrorDetails = (loadable, showBeError = true) => {
8
10
  if (loadable.response.status === 403 && ((_c = (_b = loadable.error) === null || _b === void 0 ? void 0 : _b.params) === null || _c === void 0 ? void 0 : _c[0])) {
9
11
  items.push(`missing scope: ${loadable.error.params[0]}`);
10
12
  }
11
- return `[${items.join(', ')}]`;
13
+ if (((_d = loadable.error) === null || _d === void 0 ? void 0 : _d.code) === 'conflict_is_not_resolved' &&
14
+ typeof ((_e = loadable.error.params) === null || _e === void 0 ? void 0 : _e[0]) === 'object') {
15
+ additionalInfo += getUnresolvedConflictsMessage(loadable.error.params, true);
16
+ }
17
+ return `[${items.join(', ')}]${additionalInfo ? '\n' + additionalInfo : ''}`;
12
18
  };
13
19
  export const errorFromLoadable = (loadable) => {
14
20
  switch (loadable.response.status) {
@@ -30,6 +30,7 @@ function fetchZipBlob(opts) {
30
30
  filterTagIn: opts.tags,
31
31
  filterTagNotIn: opts.excludeTags,
32
32
  fileStructureTemplate: opts.fileStructureTemplate,
33
+ escapeHtml: false,
33
34
  });
34
35
  handleLoadableError(loadable);
35
36
  return loadable.data;
@@ -18,6 +18,7 @@ import { mapImportFormat } from '../utils/mapImportFormat.js';
18
18
  import { handleLoadableError } from '../client/TolgeeClient.js';
19
19
  import { findFilesByTemplate } from '../utils/filesTemplate.js';
20
20
  import { valueToArray } from '../utils/valueToArray.js';
21
+ import { printUnresolvedConflicts } from '../utils/printFailedKeys.js';
21
22
  function allInPattern(pattern) {
22
23
  return __awaiter(this, void 0, void 0, function* () {
23
24
  const files = [];
@@ -100,7 +101,7 @@ function handleMappingError(fileMappings) {
100
101
  }
101
102
  const pushHandler = (config) => function () {
102
103
  return __awaiter(this, void 0, void 0, function* () {
103
- var _a, _b, _c, _d;
104
+ var _a, _b, _c, _d, _e, _f, _g;
104
105
  const opts = this.optsWithGlobals();
105
106
  let allMatchers = [];
106
107
  const filesTemplate = opts.filesTemplate;
@@ -130,12 +131,25 @@ const pushHandler = (config) => function () {
130
131
  error('Nothing to import.');
131
132
  return;
132
133
  }
134
+ let errorOnUnresolvedConflict;
135
+ switch (opts.errorOnUnresolvedConflict) {
136
+ case 'auto':
137
+ errorOnUnresolvedConflict = undefined;
138
+ break;
139
+ case 'yes':
140
+ errorOnUnresolvedConflict = true;
141
+ break;
142
+ case 'no':
143
+ errorOnUnresolvedConflict = false;
144
+ break;
145
+ }
133
146
  const params = {
134
147
  createNewKeys: true,
135
148
  forceMode: opts.forceMode,
136
149
  overrideKeyDescriptions: opts.overrideKeyDescriptions,
137
150
  convertPlaceholdersToIcu: opts.convertPlaceholdersToIcu,
138
151
  tagNewKeys: (_d = opts.tagNewKeys) !== null && _d !== void 0 ? _d : [],
152
+ overrideMode: (_e = opts.overrideMode) !== null && _e !== void 0 ? _e : 'RECOMMENDED',
139
153
  fileMappings: files.map((f) => {
140
154
  var _a;
141
155
  const format = mapImportFormat(opts.format, extname(f.name));
@@ -153,30 +167,34 @@ const pushHandler = (config) => function () {
153
167
  };
154
168
  }),
155
169
  removeOtherKeys: opts.removeOtherKeys,
170
+ errorOnUnresolvedConflict: errorOnUnresolvedConflict,
156
171
  };
157
- const attempt1 = yield loading('Importing...', importData(opts.client, {
172
+ let attempt = yield loading('Importing...', importData(opts.client, {
158
173
  files,
159
174
  params,
160
175
  }));
161
- if (attempt1.error) {
162
- if (attempt1.error.code === 'existing_language_not_selected') {
176
+ if (attempt.error) {
177
+ if (attempt.error.code === 'existing_language_not_selected') {
163
178
  handleMappingError(params.fileMappings);
164
179
  }
165
- if (attempt1.error.code !== 'conflict_is_not_resolved') {
166
- handleLoadableError(attempt1);
180
+ if (attempt.error.code !== 'conflict_is_not_resolved') {
181
+ handleLoadableError(attempt);
167
182
  }
168
183
  const forceMode = yield promptConflicts(opts);
169
- const attempt2 = yield loading('Overriding...', importData(opts.client, {
184
+ attempt = yield loading('Overriding...', importData(opts.client, {
170
185
  files,
171
186
  params: Object.assign(Object.assign({}, params), { forceMode }),
172
187
  }));
173
- handleLoadableError(attempt2);
188
+ handleLoadableError(attempt);
189
+ }
190
+ if ((_g = (_f = attempt.data) === null || _f === void 0 ? void 0 : _f.unresolvedConflicts) === null || _g === void 0 ? void 0 : _g.length) {
191
+ printUnresolvedConflicts(attempt.data.unresolvedConflicts, false);
174
192
  }
175
193
  success('Done!');
176
194
  });
177
195
  };
178
196
  export default (config) => {
179
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
197
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
180
198
  return new Command()
181
199
  .name('push')
182
200
  .description('Pushes translations to Tolgee')
@@ -191,5 +209,11 @@ export default (config) => {
191
209
  .addOption(new Option('-n, --namespaces <namespaces...>', 'Specifies which namespaces should be pushed (see push.files in config).').default((_h = config.push) === null || _h === void 0 ? void 0 : _h.namespaces))
192
210
  .addOption(new Option('--tag-new-keys <tags...>', 'Specify tags that will be added to newly created keys.').default((_j = config.push) === null || _j === void 0 ? void 0 : _j.tagNewKeys))
193
211
  .addOption(new Option('--remove-other-keys', 'Remove keys which are not present in the import (within imported namespaces).').default((_k = config.push) === null || _k === void 0 ? void 0 : _k.removeOtherKeys))
212
+ .addOption(new Option('--override-mode <mode>', 'Specifies what is considered non-overridable translation.')
213
+ .choices(['RECOMMENDED', 'ALL'])
214
+ .default((_l = config.push) === null || _l === void 0 ? void 0 : _l.overrideMode))
215
+ .addOption(new Option('--error-on-unresolved-conflict <choice>', 'Fail the whole import if there are unresolved conflicts.')
216
+ .choices(['yes', 'no', 'auto'])
217
+ .default((_o = (_m = config.push) === null || _m === void 0 ? void 0 : _m.errorOnUnresolvedConflict) !== null && _o !== void 0 ? _o : 'auto'))
194
218
  .action(pushHandler(config));
195
219
  };
@@ -24,6 +24,7 @@ function backup(client, dest) {
24
24
  supportArrays: false,
25
25
  filterState: ['UNTRANSLATED', 'TRANSLATED', 'REVIEWED'],
26
26
  structureDelimiter: '',
27
+ escapeHtml: false,
27
28
  });
28
29
  handleLoadableError(loadable);
29
30
  const blob = loadable.data;
@@ -46,7 +47,7 @@ function askForConfirmation(keys, operation) {
46
47
  }
47
48
  const syncHandler = (config) => function () {
48
49
  return __awaiter(this, void 0, void 0, function* () {
49
- var _a, _b, _c;
50
+ var _a, _b, _c, _d, _e, _f;
50
51
  const opts = this.optsWithGlobals();
51
52
  const rawKeys = yield loading('Analyzing code...', extractKeysOfFiles(opts));
52
53
  const warnCount = dumpWarnings(rawKeys);
@@ -55,11 +56,24 @@ const syncHandler = (config) => function () {
55
56
  process.exit(1);
56
57
  }
57
58
  const localKeys = filterExtractionResult(rawKeys);
59
+ if ((_a = opts.namespaces) === null || _a === void 0 ? void 0 : _a.length) {
60
+ for (const namespace of Object.keys(localKeys)) {
61
+ if (!((_b = opts.namespaces) === null || _b === void 0 ? void 0 : _b.includes(namespace))) {
62
+ localKeys[namespace].clear();
63
+ }
64
+ }
65
+ }
58
66
  const allKeysLoadable = yield opts.client.GET('/v2/projects/{projectId}/all-keys', {
59
67
  params: { path: { projectId: opts.client.getProjectId() } },
60
68
  });
61
69
  handleLoadableError(allKeysLoadable);
62
- const remoteKeys = (_c = (_b = (_a = allKeysLoadable.data) === null || _a === void 0 ? void 0 : _a._embedded) === null || _b === void 0 ? void 0 : _b.keys) !== null && _c !== void 0 ? _c : [];
70
+ let remoteKeys = (_e = (_d = (_c = allKeysLoadable.data) === null || _c === void 0 ? void 0 : _c._embedded) === null || _d === void 0 ? void 0 : _d.keys) !== null && _e !== void 0 ? _e : [];
71
+ if ((_f = opts.namespaces) === null || _f === void 0 ? void 0 : _f.length) {
72
+ remoteKeys = remoteKeys.filter((key) => {
73
+ var _a, _b;
74
+ return (_a = opts.namespaces) === null || _a === void 0 ? void 0 : _a.includes((_b = key.namespace) !== null && _b !== void 0 ? _b : '');
75
+ });
76
+ }
63
77
  const diff = compareKeys(localKeys, remoteKeys);
64
78
  if (!diff.added.length && !diff.removed.length) {
65
79
  console.log(ansi.green('Your code project is in sync with the associated Tolgee project!'));
@@ -127,14 +141,15 @@ const syncHandler = (config) => function () {
127
141
  });
128
142
  };
129
143
  export default (config) => {
130
- var _a, _b, _c, _d, _e, _f;
144
+ var _a, _b, _c, _d, _e, _f, _g;
131
145
  return new Command()
132
146
  .name('sync')
133
147
  .description('Synchronizes the keys in your code project and in the Tolgee project, by creating missing keys and optionally deleting unused ones. For a dry-run, use `tolgee compare`.')
134
148
  .addOption(new Option('-B, --backup <path>', 'Store translation files backup (only translation files, not states, comments, tags, etc.). If something goes wrong, the backup can be used to restore the project to its previous state.').default((_b = (_a = config.sync) === null || _a === void 0 ? void 0 : _a.backup) !== null && _b !== void 0 ? _b : false))
135
149
  .addOption(new Option('--continue-on-warning', 'Set this flag to continue the sync if warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync.').default((_d = (_c = config.sync) === null || _c === void 0 ? void 0 : _c.continueOnWarning) !== null && _d !== void 0 ? _d : false))
150
+ .addOption(new Option('-n, --namespaces <namespaces...>', 'Specifies which namespaces should be synchronized.').default((_e = config.sync) === null || _e === void 0 ? void 0 : _e.namespaces))
136
151
  .addOption(new Option('-Y, --yes', 'Skip prompts and automatically say yes to them. You will not be asked for confirmation before creating/deleting keys.').default(false))
137
- .addOption(new Option('--remove-unused', 'Delete unused keys from the Tolgee project.').default((_f = (_e = config.sync) === null || _e === void 0 ? void 0 : _e.removeUnused) !== null && _f !== void 0 ? _f : false))
152
+ .addOption(new Option('--remove-unused', 'Delete unused keys from the Tolgee project (within selected namespaces if specified).').default((_g = (_f = config.sync) === null || _f === void 0 ? void 0 : _f.removeUnused) !== null && _g !== void 0 ? _g : false))
138
153
  .option('--tag-new-keys <tags...>', 'Specify tags that will be added to newly created keys.')
139
154
  .action(syncHandler(config));
140
155
  };
@@ -1,4 +1,3 @@
1
- import { NullNamespace } from '../../extractor/runner.js';
2
1
  import ansi from 'ansi-colors';
3
2
  /**
4
3
  * Prints information about a key, with coloring and formatting.
@@ -6,15 +5,20 @@ import ansi from 'ansi-colors';
6
5
  * @param key The key to print.
7
6
  * @param deletion True if the key is about to be deleted.
8
7
  */
9
- export function printKey(key, deletion) {
8
+ export function printKey(key, deletion, color, note) {
9
+ const colorFunc = color !== null && color !== void 0 ? color : (deletion ? ansi.red : ansi.green);
10
10
  const namespace = key.namespace
11
11
  ? ` ${ansi.italic(`(namespace: ${key.namespace})`)}`
12
12
  : '';
13
- if (deletion) {
14
- console.log(`${ansi.red(`- ${key.keyName}`)}${namespace}`);
13
+ const renderedNote = note ? ` ${note}` : '';
14
+ if (deletion === undefined) {
15
+ console.log(`${colorFunc(`${key.keyName}`)}${namespace}${renderedNote}`);
16
+ }
17
+ else if (deletion) {
18
+ console.log(`${colorFunc(`- ${key.keyName}`)}${namespace}${renderedNote}`);
15
19
  }
16
20
  else {
17
- console.log(`${ansi.green(`+ ${key.keyName}`)}${namespace}`);
21
+ console.log(`${colorFunc(`+ ${key.keyName}`)}${namespace}${renderedNote}`);
18
22
  }
19
23
  }
20
24
  /**
@@ -30,18 +34,18 @@ export function compareKeys(local, remote) {
30
34
  const result = { added: [], removed: [] };
31
35
  // Deleted keys
32
36
  for (const remoteKey of remote) {
33
- const namespace = remoteKey.namespace || NullNamespace;
37
+ const namespace = remoteKey.namespace || '';
34
38
  const keyExists = (_a = local[namespace]) === null || _a === void 0 ? void 0 : _a.delete(remoteKey.name);
35
39
  if (!keyExists) {
36
40
  result.removed.push({
37
41
  id: remoteKey.id,
38
42
  keyName: remoteKey.name,
39
- namespace: remoteKey.namespace || undefined,
43
+ namespace: remoteKey.namespace || '',
40
44
  });
41
45
  }
42
46
  }
43
47
  // Added keys
44
- const namespaces = [NullNamespace, ...Object.keys(local).sort()];
48
+ const namespaces = [...Object.keys(local).sort()];
45
49
  for (const namespace of namespaces) {
46
50
  if (namespace in local && local[namespace].size) {
47
51
  const keys = local[namespace];
@@ -49,7 +53,7 @@ export function compareKeys(local, remote) {
49
53
  for (const keyName of keyNames) {
50
54
  result.added.push({
51
55
  keyName: keyName,
52
- namespace: namespace === NullNamespace ? undefined : namespace,
56
+ namespace: namespace || '',
53
57
  defaultValue: keys.get(keyName) || undefined,
54
58
  });
55
59
  }
@@ -11,7 +11,6 @@ import { glob } from 'tinyglobby';
11
11
  import { extname } from 'path';
12
12
  import { callWorker } from './worker.js';
13
13
  import { exitWithError } from '../utils/logger.js';
14
- export const NullNamespace = Symbol('namespace.null');
15
14
  function parseVerbose(v) {
16
15
  return Array.isArray(v) ? v : v ? [] : undefined;
17
16
  }
@@ -101,7 +100,7 @@ export function filterExtractionResult(data) {
101
100
  const result = Object.create(null);
102
101
  for (const { keys } of data.values()) {
103
102
  for (const key of keys) {
104
- const namespace = key.namespace || NullNamespace;
103
+ const namespace = key.namespace || '';
105
104
  if (!(namespace in result)) {
106
105
  result[namespace] = new Map();
107
106
  }
@@ -1,7 +1,9 @@
1
1
  export const getStackTrace = () => {
2
- const obj = {};
3
- Error.captureStackTrace(obj, getStackTrace);
4
- const stack = obj.stack;
5
- const parts = stack.split('\n');
6
- return parts.slice(2).join('\n');
2
+ var _a, _b;
3
+ if (typeof Error.captureStackTrace === 'function') {
4
+ const obj = {};
5
+ Error.captureStackTrace(obj, getStackTrace);
6
+ return (_a = obj.stack) !== null && _a !== void 0 ? _a : '';
7
+ }
8
+ return (_b = new Error().stack) !== null && _b !== void 0 ? _b : '';
7
9
  };
@@ -0,0 +1,26 @@
1
+ import ansi from 'ansi-colors';
2
+ export function renderKey(key, note) {
3
+ const colorFunc = ansi.yellow;
4
+ const namespace = key.namespace
5
+ ? ` ${ansi.italic(`(namespace: ${key.namespace})`)}`
6
+ : '';
7
+ const renderedNote = note ? ` ${note}` : '';
8
+ return `${colorFunc(`${key.keyName}`)}${namespace}${renderedNote}`;
9
+ }
10
+ export function getUnresolvedConflictsMessage(translations, isError) {
11
+ const someOverridable = Boolean(translations.find((c) => c.isOverridable));
12
+ const result = [''];
13
+ result.push(`🟡 Some translations cannot be updated:`);
14
+ translations.forEach((c) => {
15
+ result.push(renderKey({ keyName: c.keyName, namespace: c.keyNamespace }, `${c.language}` + (c.isOverridable ? ' (overridable)' : '')));
16
+ });
17
+ result.push('');
18
+ if (someOverridable) {
19
+ result.push('HINT: Overridable translations can be updated with the `--override-mode ALL`');
20
+ result.push('');
21
+ }
22
+ return result.join('\n');
23
+ }
24
+ export function printUnresolvedConflicts(translations, isError) {
25
+ console.log(getUnresolvedConflictsMessage(translations, isError));
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolgee/cli",
3
- "version": "2.12.1",
3
+ "version": "2.14.0",
4
4
  "type": "module",
5
5
  "description": "A tool to interact with the Tolgee Platform through CLI",
6
6
  "repository": {
package/schema.json CHANGED
@@ -90,6 +90,12 @@
90
90
  "removeOtherKeys": {
91
91
  "description": "Remove keys which are not present in the import (within imported namespaces).",
92
92
  "type": "boolean"
93
+ },
94
+ "errorOnUnresolvedConflict": {
95
+ "$ref": "#/$defs/errorOnUnresolvedConflict"
96
+ },
97
+ "overrideMode": {
98
+ "$ref": "#/$defs/overrideMode"
93
99
  }
94
100
  }
95
101
  },
@@ -165,8 +171,15 @@
165
171
  "type": "boolean"
166
172
  },
167
173
  "removeUnused": {
168
- "description": "Delete unused keys from the Tolgee project",
174
+ "description": "Delete unused keys from the Tolgee project (within selected namespaces if specified).",
169
175
  "type": "boolean"
176
+ },
177
+ "namespaces": {
178
+ "description": "Specifies which namespaces should be synchronized.",
179
+ "type": "array",
180
+ "items": {
181
+ "type": "string"
182
+ }
170
183
  }
171
184
  }
172
185
  },
@@ -244,6 +257,16 @@
244
257
  "type": "string",
245
258
  "enum": ["OVERRIDE", "KEEP", "NO_FORCE"]
246
259
  },
260
+ "overrideMode": {
261
+ "description": "Specifies what is considered non-overridable translation: \n - RECOMMENDED - protected reviewed translations are considered as non-overridable\n - ALL - translations that user has permissions for",
262
+ "type": "string",
263
+ "enum": ["ALL", "RECOMMENDED"]
264
+ },
265
+ "errorOnUnresolvedConflict": {
266
+ "description": "Fail the whole import if there are unresolved conflicts in import: \n - yes - fail if any unresolved conflict is present\n no - don't fail and just print unresolved conflicts\n auto - fails when when forceMode=KEEP, otherwise doesn't fail",
267
+ "type": "string",
268
+ "enum": ["yes", "no", "auto"]
269
+ },
247
270
  "path": {
248
271
  "description": "File glob specifying which files to include.",
249
272
  "type": "string"