@theia/localization-manager 1.23.0-next.8 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +39 -2
  2. package/lib/common.d.ts +5 -0
  3. package/lib/common.d.ts.map +1 -0
  4. package/lib/common.js +27 -0
  5. package/lib/common.js.map +1 -0
  6. package/lib/deepl-api.d.ts +27 -0
  7. package/lib/deepl-api.d.ts.map +1 -0
  8. package/lib/deepl-api.js +94 -0
  9. package/lib/deepl-api.js.map +1 -0
  10. package/lib/index.d.ts +2 -15
  11. package/lib/index.d.ts.map +1 -1
  12. package/lib/index.js +18 -16
  13. package/lib/index.js.map +1 -1
  14. package/lib/localization-extractor.d.ts +5 -21
  15. package/lib/localization-extractor.d.ts.map +1 -1
  16. package/lib/localization-extractor.js +76 -39
  17. package/lib/localization-extractor.js.map +1 -1
  18. package/lib/localization-extractor.spec.d.ts +0 -15
  19. package/lib/localization-extractor.spec.d.ts.map +1 -1
  20. package/lib/localization-extractor.spec.js +27 -26
  21. package/lib/localization-extractor.spec.js.map +1 -1
  22. package/lib/localization-manager.d.ts +23 -0
  23. package/lib/localization-manager.d.ts.map +1 -0
  24. package/lib/localization-manager.js +141 -0
  25. package/lib/localization-manager.js.map +1 -0
  26. package/lib/localization-manager.spec.d.ts +2 -0
  27. package/lib/localization-manager.spec.d.ts.map +1 -0
  28. package/lib/localization-manager.spec.js +75 -0
  29. package/lib/localization-manager.spec.js.map +1 -0
  30. package/package.json +7 -4
  31. package/src/common.ts +27 -0
  32. package/src/deepl-api.ts +153 -0
  33. package/src/index.ts +17 -15
  34. package/src/localization-extractor.spec.ts +22 -21
  35. package/src/localization-extractor.ts +80 -44
  36. package/src/localization-manager.spec.ts +80 -0
  37. package/src/localization-manager.ts +151 -0
package/src/index.ts CHANGED
@@ -1,17 +1,19 @@
1
- /********************************************************************************
2
- * Copyright (C) 2021 TypeFox and others.
3
- *
4
- * This program and the accompanying materials are made available under the
5
- * terms of the Eclipse Public License v. 2.0 which is available at
6
- * http://www.eclipse.org/legal/epl-2.0.
7
- *
8
- * This Source Code may also be made available under the following Secondary
9
- * Licenses when the conditions for such availability set forth in the Eclipse
10
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- * with the GNU Classpath Exception which is available at
12
- * https://www.gnu.org/software/classpath/license.html.
13
- *
14
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
- ********************************************************************************/
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
16
 
17
+ export * from './common';
17
18
  export * from './localization-extractor';
19
+ export * from './localization-manager';
@@ -1,23 +1,24 @@
1
- /********************************************************************************
2
- * Copyright (C) 2021 TypeFox and others.
3
- *
4
- * This program and the accompanying materials are made available under the
5
- * terms of the Eclipse Public License v. 2.0 which is available at
6
- * http://www.eclipse.org/legal/epl-2.0.
7
- *
8
- * This Source Code may also be made available under the following Secondary
9
- * Licenses when the conditions for such availability set forth in the Eclipse
10
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- * with the GNU Classpath Exception which is available at
12
- * https://www.gnu.org/software/classpath/license.html.
13
- *
14
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
- ********************************************************************************/
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
16
 
17
17
  import * as assert from 'assert';
18
- import { extractFromFile } from './localization-extractor';
18
+ import { extractFromFile, ExtractionOptions } from './localization-extractor';
19
19
 
20
20
  const TEST_FILE = 'test.ts';
21
+ const quiet: ExtractionOptions = { quiet: true };
21
22
 
22
23
  describe('correctly extracts from file content', () => {
23
24
 
@@ -93,7 +94,7 @@ describe('correctly extracts from file content', () => {
93
94
  it('should return an error when resolving is not successful', async () => {
94
95
  const content = "nls.localize(a, 'value')";
95
96
  const errors: string[] = [];
96
- assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors), {});
97
+ assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors, quiet), {});
97
98
  assert.deepStrictEqual(errors, [
98
99
  "test.ts(1,14): Could not resolve reference to 'a'"
99
100
  ]);
@@ -102,7 +103,7 @@ describe('correctly extracts from file content', () => {
102
103
  it('should return an error when resolving from an expression', async () => {
103
104
  const content = "nls.localize(test.value, 'value');";
104
105
  const errors: string[] = [];
105
- assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors), {});
106
+ assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors, quiet), {});
106
107
  assert.deepStrictEqual(errors, [
107
108
  "test.ts(1,14): 'test.value' is not a string constant"
108
109
  ]);
@@ -114,7 +115,7 @@ describe('correctly extracts from file content', () => {
114
115
  nls.localize('key/nested', 'value');
115
116
  `.trim();
116
117
  const errors: string[] = [];
117
- assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors), {
118
+ assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors, quiet), {
118
119
  'key': 'value'
119
120
  });
120
121
  assert.deepStrictEqual(errors, [
@@ -128,7 +129,7 @@ describe('correctly extracts from file content', () => {
128
129
  nls.localize('key', 'value');
129
130
  `.trim();
130
131
  const errors: string[] = [];
131
- assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors), {
132
+ assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors, quiet), {
132
133
  'key': {
133
134
  'nested': 'value'
134
135
  }
@@ -141,7 +142,7 @@ describe('correctly extracts from file content', () => {
141
142
  it('should show error for template literals', async () => {
142
143
  const content = 'nls.localize("key", `template literal value`)';
143
144
  const errors: string[] = [];
144
- assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors), {});
145
+ assert.deepStrictEqual(await extractFromFile(TEST_FILE, content, errors, quiet), {});
145
146
  assert.deepStrictEqual(errors, [
146
147
  "test.ts(1,20): Template literals are not supported for localization. Please use the additional arguments of the 'nls.localize' function to format strings"
147
148
  ]);
@@ -1,18 +1,18 @@
1
- /********************************************************************************
2
- * Copyright (C) 2021 TypeFox and others.
3
- *
4
- * This program and the accompanying materials are made available under the
5
- * terms of the Eclipse Public License v. 2.0 which is available at
6
- * http://www.eclipse.org/legal/epl-2.0.
7
- *
8
- * This Source Code may also be made available under the following Secondary
9
- * Licenses when the conditions for such availability set forth in the Eclipse
10
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- * with the GNU Classpath Exception which is available at
12
- * https://www.gnu.org/software/classpath/license.html.
13
- *
14
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
- ********************************************************************************/
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
16
 
17
17
  import * as fs from 'fs-extra';
18
18
  import * as ts from 'typescript';
@@ -21,21 +21,19 @@ import * as path from 'path';
21
21
  import { glob } from 'glob';
22
22
  import { promisify } from 'util';
23
23
  import deepmerge = require('deepmerge');
24
+ import { Localization, sortLocalization } from './common';
24
25
 
25
26
  const globPromise = promisify(glob);
26
27
 
27
- export interface Localization {
28
- [key: string]: string | Localization
29
- }
30
-
31
28
  export interface ExtractionOptions {
32
- root: string
33
- output: string
29
+ root?: string
30
+ output?: string
34
31
  exclude?: string
35
32
  logs?: string
36
33
  /** List of globs matching the files to extract from. */
37
34
  files?: string[]
38
- merge: boolean
35
+ merge?: boolean
36
+ quiet?: boolean
39
37
  }
40
38
 
41
39
  class SingleFileServiceHost implements ts.LanguageServiceHost {
@@ -74,9 +72,9 @@ const tsOptions: ts.CompilerOptions = {
74
72
  };
75
73
 
76
74
  export async function extract(options: ExtractionOptions): Promise<void> {
77
- const cwd = path.resolve(process.cwd(), options.root);
75
+ const cwd = path.resolve(process.env.INIT_CWD || process.cwd(), options.root ?? '');
78
76
  const files: string[] = [];
79
- await Promise.all((options.files ?? ['**/src/**/*.ts']).map(
77
+ await Promise.all((options.files ?? ['**/src/**/*.{ts,tsx}']).map(
80
78
  async pattern => files.push(...await globPromise(pattern, { cwd }))
81
79
  ));
82
80
  let localization: Localization = {};
@@ -90,13 +88,15 @@ export async function extract(options: ExtractionOptions): Promise<void> {
90
88
  if (errors.length > 0 && options.logs) {
91
89
  await fs.writeFile(options.logs, errors.join(os.EOL));
92
90
  }
93
- const output = path.resolve(process.cwd(), options.output);
94
- if (options.merge && await fs.pathExists(output)) {
95
- const existing = await fs.readJson(output);
91
+ const out = path.resolve(process.env.INIT_CWD || process.cwd(), options.output ?? '');
92
+ if (options.merge && await fs.pathExists(out)) {
93
+ const existing = await fs.readJson(out);
96
94
  localization = deepmerge(existing, localization);
97
95
  }
98
- await fs.writeJson(options.output, localization, {
99
- spaces: 4
96
+ localization = sortLocalization(localization);
97
+ await fs.mkdirs(path.dirname(out));
98
+ await fs.writeJson(out, localization, {
99
+ spaces: 2
100
100
  });
101
101
  }
102
102
 
@@ -108,20 +108,22 @@ export async function extractFromFile(file: string, content: string, errors?: st
108
108
  const localizationCalls = collect(sourceFile, node => isLocalizeCall(node));
109
109
  for (const call of localizationCalls) {
110
110
  try {
111
- const extracted = extractFromLocalizeCall(call);
112
- if (!isExcluded(options, extracted[0])) {
111
+ const extracted = extractFromLocalizeCall(call, options);
112
+ if (extracted) {
113
113
  insert(localization, extracted);
114
114
  }
115
115
  } catch (err) {
116
116
  const tsError = err as Error;
117
117
  errors?.push(tsError.message);
118
- console.log(tsError.message);
118
+ if (!options?.quiet) {
119
+ console.log(tsError.message);
120
+ }
119
121
  }
120
122
  }
121
123
  const localizedCommands = collect(sourceFile, node => isCommandLocalizeUtility(node));
122
124
  for (const command of localizedCommands) {
123
125
  try {
124
- const extracted = extractFromLocalizedCommandCall(command);
126
+ const extracted = extractFromLocalizedCommandCall(command, errors, options);
125
127
  const label = extracted.label;
126
128
  const category = extracted.category;
127
129
  if (!isExcluded(options, label[0])) {
@@ -133,7 +135,9 @@ export async function extractFromFile(file: string, content: string, errors?: st
133
135
  } catch (err) {
134
136
  const tsError = err as Error;
135
137
  errors?.push(tsError.message);
136
- console.log(tsError.message);
138
+ if (!options?.quiet) {
139
+ console.log(tsError.message);
140
+ }
137
141
  }
138
142
  }
139
143
  return localization;
@@ -194,7 +198,7 @@ function isLocalizeCall(node: ts.Node): boolean {
194
198
  return node.expression.getText() === 'nls.localize';
195
199
  }
196
200
 
197
- function extractFromLocalizeCall(node: ts.Node): [string, string, ts.Node] {
201
+ function extractFromLocalizeCall(node: ts.Node, options?: ExtractionOptions): [string, string, ts.Node] | undefined {
198
202
  if (!ts.isCallExpression(node)) {
199
203
  throw new TypeScriptError('Invalid node type', node);
200
204
  }
@@ -206,10 +210,18 @@ function extractFromLocalizeCall(node: ts.Node): [string, string, ts.Node] {
206
210
 
207
211
  const key = extractString(args[0]);
208
212
  const value = extractString(args[1]);
213
+
214
+ if (isExcluded(options, key)) {
215
+ return undefined;
216
+ }
217
+
209
218
  return [key, value, args[1]];
210
219
  }
211
220
 
212
- function extractFromLocalizedCommandCall(node: ts.Node): { label: [string, string, ts.Node], category?: [string, string, ts.Node] } {
221
+ function extractFromLocalizedCommandCall(node: ts.Node, errors?: string[], options?: ExtractionOptions): {
222
+ label: [string, string, ts.Node],
223
+ category?: [string, string, ts.Node]
224
+ } {
213
225
  if (!ts.isCallExpression(node)) {
214
226
  throw new TypeScriptError('Invalid node type', node);
215
227
  }
@@ -248,8 +260,16 @@ function extractFromLocalizedCommandCall(node: ts.Node): { label: [string, strin
248
260
  labelNode = property.initializer;
249
261
  }
250
262
 
251
- const value = extractString(property.initializer);
252
- propertyMap.set(name, value);
263
+ try {
264
+ const value = extractString(property.initializer);
265
+ propertyMap.set(name, value);
266
+ } catch (err) {
267
+ const tsError = err as Error;
268
+ errors?.push(tsError.message);
269
+ if (!options?.quiet) {
270
+ console.log(tsError.message);
271
+ }
272
+ }
253
273
  }
254
274
 
255
275
  let labelKey = propertyMap.get('id');
@@ -258,17 +278,33 @@ function extractFromLocalizedCommandCall(node: ts.Node): { label: [string, strin
258
278
 
259
279
  // We have an explicit label translation key
260
280
  if (args.length > 1) {
261
- const labelOverrideKey = extractStringOrUndefined(args[1]);
262
- if (labelOverrideKey) {
263
- labelKey = labelOverrideKey;
264
- labelNode = args[1];
281
+ try {
282
+ const labelOverrideKey = extractStringOrUndefined(args[1]);
283
+ if (labelOverrideKey) {
284
+ labelKey = labelOverrideKey;
285
+ labelNode = args[1];
286
+ }
287
+ } catch (err) {
288
+ const tsError = err as Error;
289
+ errors?.push(tsError.message);
290
+ if (!options?.quiet) {
291
+ console.log(tsError.message);
292
+ }
265
293
  }
266
294
  }
267
295
 
268
296
  // We have an explicit category translation key
269
297
  if (args.length > 2) {
270
- categoryKey = extractStringOrUndefined(args[2]);
271
- categoryNode = args[2];
298
+ try {
299
+ categoryKey = extractStringOrUndefined(args[2]);
300
+ categoryNode = args[2];
301
+ } catch (err) {
302
+ const tsError = err as Error;
303
+ errors?.push(tsError.message);
304
+ if (!options?.quiet) {
305
+ console.log(tsError.message);
306
+ }
307
+ }
272
308
  }
273
309
 
274
310
  if (!labelKey) {
@@ -0,0 +1,80 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as assert from 'assert';
18
+ import { DeeplParameters, DeeplResponse } from './deepl-api';
19
+ import { LocalizationManager, LocalizationOptions } from './localization-manager';
20
+
21
+ describe('localization-manager#translateLanguage', () => {
22
+
23
+ async function mockLocalization(parameters: DeeplParameters): Promise<DeeplResponse> {
24
+ return {
25
+ translations: parameters.text.map(value => ({
26
+ detected_source_language: '',
27
+ text: `[${value}]`
28
+ }))
29
+ };
30
+ }
31
+
32
+ const manager = new LocalizationManager(mockLocalization);
33
+ const defaultOptions: LocalizationOptions = {
34
+ authKey: '',
35
+ freeApi: false,
36
+ sourceFile: '',
37
+ targetLanguages: ['EN']
38
+ };
39
+
40
+ it('should translate a single value', async () => {
41
+ const input = {
42
+ key: 'value'
43
+ };
44
+ const target = {};
45
+ await manager.translateLanguage(input, target, 'EN', defaultOptions);
46
+ assert.deepStrictEqual(target, {
47
+ key: '[value]'
48
+ });
49
+ });
50
+
51
+ it('should translate nested values', async () => {
52
+ const input = {
53
+ a: {
54
+ b: 'b'
55
+ },
56
+ c: 'c'
57
+ };
58
+ const target = {};
59
+ await manager.translateLanguage(input, target, 'EN', defaultOptions);
60
+ assert.deepStrictEqual(target, {
61
+ a: {
62
+ b: '[b]'
63
+ },
64
+ c: '[c]'
65
+ });
66
+ });
67
+
68
+ it('should not override existing targets', async () => {
69
+ const input = {
70
+ a: 'a'
71
+ };
72
+ const target = {
73
+ a: 'b'
74
+ };
75
+ await manager.translateLanguage(input, target, 'EN', defaultOptions);
76
+ assert.deepStrictEqual(target, {
77
+ a: 'b'
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,151 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as chalk from 'chalk';
18
+ import * as fs from 'fs-extra';
19
+ import * as path from 'path';
20
+ import { sortLocalization } from '.';
21
+ import { Localization } from './common';
22
+ import { deepl, DeeplLanguage, DeeplParameters, isSupportedLanguage, supportedLanguages } from './deepl-api';
23
+
24
+ export interface LocalizationOptions {
25
+ freeApi: Boolean
26
+ authKey: string
27
+ sourceFile: string
28
+ sourceLanguage?: string
29
+ targetLanguages: string[]
30
+ }
31
+
32
+ export type LocalizationFunction = (parameters: DeeplParameters) => Promise<string[]>;
33
+
34
+ export class LocalizationManager {
35
+
36
+ constructor(private localizationFn = deepl) { }
37
+
38
+ async localize(options: LocalizationOptions): Promise<void> {
39
+ let source: Localization = {};
40
+ const cwd = process.env.INIT_CWD || process.cwd();
41
+ const sourceFile = path.resolve(cwd, options.sourceFile);
42
+ try {
43
+ source = await fs.readJson(sourceFile);
44
+ } catch {
45
+ console.log(chalk.red(`Could not read file "${options.sourceFile}"`));
46
+ process.exit(1);
47
+ }
48
+ const languages: string[] = [];
49
+ for (const targetLanguage of options.targetLanguages) {
50
+ if (!isSupportedLanguage(targetLanguage)) {
51
+ console.log(chalk.yellow(`Language "${targetLanguage}" is not supported for automatic localization`));
52
+ } else {
53
+ languages.push(targetLanguage);
54
+ }
55
+ }
56
+ if (languages.length !== options.targetLanguages.length) {
57
+ console.log('Supported languages: ' + supportedLanguages.join(', '));
58
+ }
59
+ const existingTranslations: Map<string, Localization> = new Map();
60
+ for (const targetLanguage of languages) {
61
+ try {
62
+ const targetPath = this.translationFileName(sourceFile, targetLanguage);
63
+ existingTranslations.set(targetLanguage, await fs.readJson(targetPath));
64
+ } catch {
65
+ existingTranslations.set(targetLanguage, {});
66
+ }
67
+ }
68
+ await Promise.all(languages.map(language => this.translateLanguage(source, existingTranslations.get(language)!, language, options)));
69
+
70
+ for (const targetLanguage of languages) {
71
+ const targetPath = this.translationFileName(sourceFile, targetLanguage);
72
+ try {
73
+ const translation = existingTranslations.get(targetLanguage)!;
74
+ await fs.writeJson(targetPath, sortLocalization(translation), { spaces: 2 });
75
+ } catch {
76
+ console.error(chalk.red(`Error writing translated file to '${targetPath}'`));
77
+ }
78
+ }
79
+ }
80
+
81
+ protected translationFileName(original: string, language: string): string {
82
+ const directory = path.dirname(original);
83
+ const fileName = path.basename(original, '.json');
84
+ return path.join(directory, `${fileName}.${language.toLowerCase()}.json`);
85
+ }
86
+
87
+ async translateLanguage(source: Localization, target: Localization, targetLanguage: string, options: LocalizationOptions): Promise<void> {
88
+ const map = this.buildLocalizationMap(source, target);
89
+ if (map.text.length > 0) {
90
+ try {
91
+ const translationResponse = await this.localizationFn({
92
+ auth_key: options.authKey,
93
+ free_api: options.freeApi,
94
+ target_lang: targetLanguage.toUpperCase() as DeeplLanguage,
95
+ source_lang: options.sourceLanguage?.toUpperCase() as DeeplLanguage,
96
+ text: map.text
97
+ });
98
+ translationResponse.translations.forEach(({ text }, i) => {
99
+ map.localize(i, text);
100
+ });
101
+ console.log(chalk.green(`Successfully translated ${map.text.length} value${map.text.length > 1 ? 's' : ''} for language "${targetLanguage}"`));
102
+ } catch (e) {
103
+ console.log(chalk.red(`Could not translate into language "${targetLanguage}"`), e);
104
+ }
105
+ } else {
106
+ console.log(`No translation necessary for language "${targetLanguage}"`);
107
+ }
108
+ }
109
+
110
+ protected buildLocalizationMap(source: Localization, target: Localization): LocalizationMap {
111
+ const functionMap = new Map<number, (value: string) => void>();
112
+ const text: string[] = [];
113
+ const process = (s: Localization, t: Localization) => {
114
+ // Delete all extra keys in the target translation first
115
+ for (const key of Object.keys(t)) {
116
+ if (!(key in s)) {
117
+ delete t[key];
118
+ }
119
+ }
120
+ for (const [key, value] of Object.entries(s)) {
121
+ if (!(key in t)) {
122
+ if (typeof value === 'string') {
123
+ functionMap.set(text.length, translation => t[key] = translation);
124
+ text.push(value);
125
+ } else {
126
+ const newLocalization: Localization = {};
127
+ t[key] = newLocalization;
128
+ process(value, newLocalization);
129
+ }
130
+ } else if (typeof value === 'object') {
131
+ if (typeof t[key] === 'string') {
132
+ t[key] = {};
133
+ }
134
+ process(value, t[key] as Localization);
135
+ }
136
+ }
137
+ };
138
+
139
+ process(source, target);
140
+
141
+ return {
142
+ text,
143
+ localize: (index, value) => functionMap.get(index)!(value)
144
+ };
145
+ }
146
+ }
147
+
148
+ export interface LocalizationMap {
149
+ text: string[]
150
+ localize: (index: number, value: string) => void
151
+ }