@theia/localization-manager 1.53.0-next.55 → 1.53.0-next.64

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,91 +1,91 @@
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-only 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
-
81
- it('should keep placeholders intact', async () => {
82
- const input = {
83
- key: '{1} {0}'
84
- };
85
- const target = {};
86
- await manager.translateLanguage(input, target, 'EN', defaultOptions);
87
- assert.deepStrictEqual(target, {
88
- key: '[{1} {0}]'
89
- });
90
- });
91
- });
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-only 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
+
81
+ it('should keep placeholders intact', async () => {
82
+ const input = {
83
+ key: '{1} {0}'
84
+ };
85
+ const target = {};
86
+ await manager.translateLanguage(input, target, 'EN', defaultOptions);
87
+ assert.deepStrictEqual(target, {
88
+ key: '[{1} {0}]'
89
+ });
90
+ });
91
+ });
@@ -1,168 +1,168 @@
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-only 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 { Localization, sortLocalization } from './common';
21
- import { deepl, DeeplLanguage, DeeplParameters, defaultLanguages, isSupportedLanguage } from './deepl-api';
22
-
23
- export interface LocalizationOptions {
24
- freeApi: Boolean
25
- authKey: string
26
- sourceFile: string
27
- sourceLanguage?: string
28
- targetLanguages: string[]
29
- }
30
-
31
- export type LocalizationFunction = (parameters: DeeplParameters) => Promise<string[]>;
32
-
33
- export class LocalizationManager {
34
-
35
- constructor(private localizationFn = deepl) { }
36
-
37
- async localize(options: LocalizationOptions): Promise<boolean> {
38
- let source: Localization = {};
39
- const cwd = process.env.INIT_CWD || process.cwd();
40
- const sourceFile = path.resolve(cwd, options.sourceFile);
41
- try {
42
- source = await fs.readJson(sourceFile);
43
- } catch {
44
- console.log(chalk.red(`Could not read file "${options.sourceFile}"`));
45
- process.exit(1);
46
- }
47
- const languages: string[] = [];
48
- for (const targetLanguage of options.targetLanguages) {
49
- if (!isSupportedLanguage(targetLanguage)) {
50
- console.log(chalk.yellow(`Language "${targetLanguage}" is not supported for automatic localization`));
51
- } else {
52
- languages.push(targetLanguage);
53
- }
54
- }
55
- if (languages.length === 0) {
56
- // No supported languages were found, default to all supported languages
57
- console.log('No languages were specified, defaulting to all supported languages for VS Code');
58
- languages.push(...defaultLanguages);
59
- }
60
- const existingTranslations: Map<string, Localization> = new Map();
61
- for (const targetLanguage of languages) {
62
- try {
63
- const targetPath = this.translationFileName(sourceFile, targetLanguage);
64
- existingTranslations.set(targetLanguage, await fs.readJson(targetPath));
65
- } catch {
66
- existingTranslations.set(targetLanguage, {});
67
- }
68
- }
69
- const results = await Promise.all(languages.map(language => this.translateLanguage(source, existingTranslations.get(language)!, language, options)));
70
- let result = results.reduce((acc, val) => acc && val, true);
71
-
72
- for (const targetLanguage of languages) {
73
- const targetPath = this.translationFileName(sourceFile, targetLanguage);
74
- try {
75
- const translation = existingTranslations.get(targetLanguage)!;
76
- await fs.writeJson(targetPath, sortLocalization(translation), { spaces: 2 });
77
- } catch {
78
- console.error(chalk.red(`Error writing translated file to '${targetPath}'`));
79
- result = false;
80
- }
81
- }
82
- return result;
83
- }
84
-
85
- protected translationFileName(original: string, language: string): string {
86
- const directory = path.dirname(original);
87
- const fileName = path.basename(original, '.json');
88
- return path.join(directory, `${fileName}.${language.toLowerCase()}.json`);
89
- }
90
-
91
- async translateLanguage(source: Localization, target: Localization, targetLanguage: string, options: LocalizationOptions): Promise<boolean> {
92
- const map = this.buildLocalizationMap(source, target);
93
- if (map.text.length > 0) {
94
- try {
95
- const translationResponse = await this.localizationFn({
96
- auth_key: options.authKey,
97
- free_api: options.freeApi,
98
- target_lang: targetLanguage.toUpperCase() as DeeplLanguage,
99
- source_lang: options.sourceLanguage?.toUpperCase() as DeeplLanguage,
100
- text: map.text.map(e => this.addIgnoreTags(e)),
101
- tag_handling: ['xml'],
102
- ignore_tags: ['x']
103
- });
104
- translationResponse.translations.forEach(({ text }, i) => {
105
- map.localize(i, this.removeIgnoreTags(text));
106
- });
107
- console.log(chalk.green(`Successfully translated ${map.text.length} value${map.text.length > 1 ? 's' : ''} for language "${targetLanguage}"`));
108
- return true;
109
- } catch (e) {
110
- console.log(chalk.red(`Could not translate into language "${targetLanguage}"`), e);
111
- return false;
112
- }
113
- } else {
114
- console.log(`No translation necessary for language "${targetLanguage}"`);
115
- return true;
116
- }
117
- }
118
-
119
- protected addIgnoreTags(text: string): string {
120
- return text.replace(/(\{\d*\})/g, '<x>$1</x>');
121
- }
122
-
123
- protected removeIgnoreTags(text: string): string {
124
- return text.replace(/<x>(\{\d+\})<\/x>/g, '$1');
125
- }
126
-
127
- protected buildLocalizationMap(source: Localization, target: Localization): LocalizationMap {
128
- const functionMap = new Map<number, (value: string) => void>();
129
- const text: string[] = [];
130
- const process = (s: Localization, t: Localization) => {
131
- // Delete all extra keys in the target translation first
132
- for (const key of Object.keys(t)) {
133
- if (!(key in s)) {
134
- delete t[key];
135
- }
136
- }
137
- for (const [key, value] of Object.entries(s)) {
138
- if (!(key in t)) {
139
- if (typeof value === 'string') {
140
- functionMap.set(text.length, translation => t[key] = translation);
141
- text.push(value);
142
- } else {
143
- const newLocalization: Localization = {};
144
- t[key] = newLocalization;
145
- process(value, newLocalization);
146
- }
147
- } else if (typeof value === 'object') {
148
- if (typeof t[key] === 'string') {
149
- t[key] = {};
150
- }
151
- process(value, t[key] as Localization);
152
- }
153
- }
154
- };
155
-
156
- process(source, target);
157
-
158
- return {
159
- text,
160
- localize: (index, value) => functionMap.get(index)!(value)
161
- };
162
- }
163
- }
164
-
165
- export interface LocalizationMap {
166
- text: string[]
167
- localize: (index: number, value: string) => void
168
- }
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-only 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 { Localization, sortLocalization } from './common';
21
+ import { deepl, DeeplLanguage, DeeplParameters, defaultLanguages, isSupportedLanguage } from './deepl-api';
22
+
23
+ export interface LocalizationOptions {
24
+ freeApi: Boolean
25
+ authKey: string
26
+ sourceFile: string
27
+ sourceLanguage?: string
28
+ targetLanguages: string[]
29
+ }
30
+
31
+ export type LocalizationFunction = (parameters: DeeplParameters) => Promise<string[]>;
32
+
33
+ export class LocalizationManager {
34
+
35
+ constructor(private localizationFn = deepl) { }
36
+
37
+ async localize(options: LocalizationOptions): Promise<boolean> {
38
+ let source: Localization = {};
39
+ const cwd = process.env.INIT_CWD || process.cwd();
40
+ const sourceFile = path.resolve(cwd, options.sourceFile);
41
+ try {
42
+ source = await fs.readJson(sourceFile);
43
+ } catch {
44
+ console.log(chalk.red(`Could not read file "${options.sourceFile}"`));
45
+ process.exit(1);
46
+ }
47
+ const languages: string[] = [];
48
+ for (const targetLanguage of options.targetLanguages) {
49
+ if (!isSupportedLanguage(targetLanguage)) {
50
+ console.log(chalk.yellow(`Language "${targetLanguage}" is not supported for automatic localization`));
51
+ } else {
52
+ languages.push(targetLanguage);
53
+ }
54
+ }
55
+ if (languages.length === 0) {
56
+ // No supported languages were found, default to all supported languages
57
+ console.log('No languages were specified, defaulting to all supported languages for VS Code');
58
+ languages.push(...defaultLanguages);
59
+ }
60
+ const existingTranslations: Map<string, Localization> = new Map();
61
+ for (const targetLanguage of languages) {
62
+ try {
63
+ const targetPath = this.translationFileName(sourceFile, targetLanguage);
64
+ existingTranslations.set(targetLanguage, await fs.readJson(targetPath));
65
+ } catch {
66
+ existingTranslations.set(targetLanguage, {});
67
+ }
68
+ }
69
+ const results = await Promise.all(languages.map(language => this.translateLanguage(source, existingTranslations.get(language)!, language, options)));
70
+ let result = results.reduce((acc, val) => acc && val, true);
71
+
72
+ for (const targetLanguage of languages) {
73
+ const targetPath = this.translationFileName(sourceFile, targetLanguage);
74
+ try {
75
+ const translation = existingTranslations.get(targetLanguage)!;
76
+ await fs.writeJson(targetPath, sortLocalization(translation), { spaces: 2 });
77
+ } catch {
78
+ console.error(chalk.red(`Error writing translated file to '${targetPath}'`));
79
+ result = false;
80
+ }
81
+ }
82
+ return result;
83
+ }
84
+
85
+ protected translationFileName(original: string, language: string): string {
86
+ const directory = path.dirname(original);
87
+ const fileName = path.basename(original, '.json');
88
+ return path.join(directory, `${fileName}.${language.toLowerCase()}.json`);
89
+ }
90
+
91
+ async translateLanguage(source: Localization, target: Localization, targetLanguage: string, options: LocalizationOptions): Promise<boolean> {
92
+ const map = this.buildLocalizationMap(source, target);
93
+ if (map.text.length > 0) {
94
+ try {
95
+ const translationResponse = await this.localizationFn({
96
+ auth_key: options.authKey,
97
+ free_api: options.freeApi,
98
+ target_lang: targetLanguage.toUpperCase() as DeeplLanguage,
99
+ source_lang: options.sourceLanguage?.toUpperCase() as DeeplLanguage,
100
+ text: map.text.map(e => this.addIgnoreTags(e)),
101
+ tag_handling: ['xml'],
102
+ ignore_tags: ['x']
103
+ });
104
+ translationResponse.translations.forEach(({ text }, i) => {
105
+ map.localize(i, this.removeIgnoreTags(text));
106
+ });
107
+ console.log(chalk.green(`Successfully translated ${map.text.length} value${map.text.length > 1 ? 's' : ''} for language "${targetLanguage}"`));
108
+ return true;
109
+ } catch (e) {
110
+ console.log(chalk.red(`Could not translate into language "${targetLanguage}"`), e);
111
+ return false;
112
+ }
113
+ } else {
114
+ console.log(`No translation necessary for language "${targetLanguage}"`);
115
+ return true;
116
+ }
117
+ }
118
+
119
+ protected addIgnoreTags(text: string): string {
120
+ return text.replace(/(\{\d*\})/g, '<x>$1</x>');
121
+ }
122
+
123
+ protected removeIgnoreTags(text: string): string {
124
+ return text.replace(/<x>(\{\d+\})<\/x>/g, '$1');
125
+ }
126
+
127
+ protected buildLocalizationMap(source: Localization, target: Localization): LocalizationMap {
128
+ const functionMap = new Map<number, (value: string) => void>();
129
+ const text: string[] = [];
130
+ const process = (s: Localization, t: Localization) => {
131
+ // Delete all extra keys in the target translation first
132
+ for (const key of Object.keys(t)) {
133
+ if (!(key in s)) {
134
+ delete t[key];
135
+ }
136
+ }
137
+ for (const [key, value] of Object.entries(s)) {
138
+ if (!(key in t)) {
139
+ if (typeof value === 'string') {
140
+ functionMap.set(text.length, translation => t[key] = translation);
141
+ text.push(value);
142
+ } else {
143
+ const newLocalization: Localization = {};
144
+ t[key] = newLocalization;
145
+ process(value, newLocalization);
146
+ }
147
+ } else if (typeof value === 'object') {
148
+ if (typeof t[key] === 'string') {
149
+ t[key] = {};
150
+ }
151
+ process(value, t[key] as Localization);
152
+ }
153
+ }
154
+ };
155
+
156
+ process(source, target);
157
+
158
+ return {
159
+ text,
160
+ localize: (index, value) => functionMap.get(index)!(value)
161
+ };
162
+ }
163
+ }
164
+
165
+ export interface LocalizationMap {
166
+ text: string[]
167
+ localize: (index: number, value: string) => void
168
+ }