@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.
- package/README.md +39 -2
- package/lib/common.d.ts +5 -0
- package/lib/common.d.ts.map +1 -0
- package/lib/common.js +27 -0
- package/lib/common.js.map +1 -0
- package/lib/deepl-api.d.ts +27 -0
- package/lib/deepl-api.d.ts.map +1 -0
- package/lib/deepl-api.js +94 -0
- package/lib/deepl-api.js.map +1 -0
- package/lib/index.d.ts +2 -15
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +18 -16
- package/lib/index.js.map +1 -1
- package/lib/localization-extractor.d.ts +5 -21
- package/lib/localization-extractor.d.ts.map +1 -1
- package/lib/localization-extractor.js +76 -39
- package/lib/localization-extractor.js.map +1 -1
- package/lib/localization-extractor.spec.d.ts +0 -15
- package/lib/localization-extractor.spec.d.ts.map +1 -1
- package/lib/localization-extractor.spec.js +27 -26
- package/lib/localization-extractor.spec.js.map +1 -1
- package/lib/localization-manager.d.ts +23 -0
- package/lib/localization-manager.d.ts.map +1 -0
- package/lib/localization-manager.js +141 -0
- package/lib/localization-manager.js.map +1 -0
- package/lib/localization-manager.spec.d.ts +2 -0
- package/lib/localization-manager.spec.d.ts.map +1 -0
- package/lib/localization-manager.spec.js +75 -0
- package/lib/localization-manager.spec.js.map +1 -0
- package/package.json +7 -4
- package/src/common.ts +27 -0
- package/src/deepl-api.ts +153 -0
- package/src/index.ts +17 -15
- package/src/localization-extractor.spec.ts +22 -21
- package/src/localization-extractor.ts +80 -44
- package/src/localization-manager.spec.ts +80 -0
- package/src/localization-manager.ts +151 -0
package/src/index.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
33
|
-
output
|
|
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
|
|
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
|
|
94
|
-
if (options.merge && await fs.pathExists(
|
|
95
|
-
const existing = await fs.readJson(
|
|
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
|
-
|
|
99
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
+
}
|