@tuhama/translation-manager 0.2.0 → 0.4.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/LICENSE +21 -21
- package/README.md +71 -59
- package/bin/index.js +45 -45
- package/package.json +66 -66
- package/src/core/Scanner.js +128 -128
- package/src/core/Storage.js +82 -82
- package/src/core/TranslatorManager.js +229 -204
- package/src/core/Utilities.js +55 -55
- package/src/core/services/GoogleTranslator.js +69 -51
- package/web/dist/assets/{index-CvijWYi5.js → index-ByUmql2E.js} +1 -1
- package/web/dist/index.html +18 -18
- package/web/package.json +22 -22
package/src/core/Storage.js
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Interface-like class for managing translation file storage.
|
|
6
|
-
* Defaults to JSON files on local filesystem.
|
|
7
|
-
*/
|
|
8
|
-
class Storage {
|
|
9
|
-
constructor(targetDir, config = {}) {
|
|
10
|
-
this.targetDir = targetDir;
|
|
11
|
-
this.config = config;
|
|
12
|
-
this.defaultPaths = [
|
|
13
|
-
'public/locales',
|
|
14
|
-
'src/locales',
|
|
15
|
-
'src/i18n',
|
|
16
|
-
'locales'
|
|
17
|
-
];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Finds and returns the path to the locales directory.
|
|
22
|
-
*/
|
|
23
|
-
async getLocalesDir() {
|
|
24
|
-
let localesDir = this.config.path;
|
|
25
|
-
|
|
26
|
-
if (!localesDir) {
|
|
27
|
-
for (const p of this.defaultPaths) {
|
|
28
|
-
const fullPath = path.resolve(this.targetDir, p);
|
|
29
|
-
if (await fs.pathExists(fullPath) && (await fs.stat(fullPath)).isDirectory()) {
|
|
30
|
-
return fullPath;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
localesDir = path.resolve(this.targetDir, localesDir);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!localesDir || !(await fs.pathExists(localesDir))) {
|
|
38
|
-
throw new Error('Could not find translation directory. Please specify it in the config or ensure it exists.');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return localesDir;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Reads all translation files from the locales directory.
|
|
46
|
-
*/
|
|
47
|
-
async readAll() {
|
|
48
|
-
const localesDir = await this.getLocalesDir();
|
|
49
|
-
const files = await fs.readdir(localesDir);
|
|
50
|
-
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
51
|
-
|
|
52
|
-
const translations = {};
|
|
53
|
-
for (const file of jsonFiles) {
|
|
54
|
-
const lang = path.basename(file, '.json');
|
|
55
|
-
const content = await fs.readJson(path.join(localesDir, file));
|
|
56
|
-
translations[lang] = content;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return translations;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Writes a single translation file (or all of them).
|
|
64
|
-
*/
|
|
65
|
-
async write(lang, content) {
|
|
66
|
-
const localesDir = await this.getLocalesDir();
|
|
67
|
-
const filePath = path.join(localesDir, `${lang}.json`);
|
|
68
|
-
await fs.writeJson(filePath, content, { spaces: 2 });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Writes all translations.
|
|
73
|
-
*/
|
|
74
|
-
async writeAll(translations) {
|
|
75
|
-
const languages = Object.keys(translations);
|
|
76
|
-
for (const lang of languages) {
|
|
77
|
-
await this.write(lang, translations[lang]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
module.exports = Storage;
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface-like class for managing translation file storage.
|
|
6
|
+
* Defaults to JSON files on local filesystem.
|
|
7
|
+
*/
|
|
8
|
+
class Storage {
|
|
9
|
+
constructor(targetDir, config = {}) {
|
|
10
|
+
this.targetDir = targetDir;
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.defaultPaths = [
|
|
13
|
+
'public/locales',
|
|
14
|
+
'src/locales',
|
|
15
|
+
'src/i18n',
|
|
16
|
+
'locales'
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Finds and returns the path to the locales directory.
|
|
22
|
+
*/
|
|
23
|
+
async getLocalesDir() {
|
|
24
|
+
let localesDir = this.config.path;
|
|
25
|
+
|
|
26
|
+
if (!localesDir) {
|
|
27
|
+
for (const p of this.defaultPaths) {
|
|
28
|
+
const fullPath = path.resolve(this.targetDir, p);
|
|
29
|
+
if (await fs.pathExists(fullPath) && (await fs.stat(fullPath)).isDirectory()) {
|
|
30
|
+
return fullPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
localesDir = path.resolve(this.targetDir, localesDir);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!localesDir || !(await fs.pathExists(localesDir))) {
|
|
38
|
+
throw new Error('Could not find translation directory. Please specify it in the config or ensure it exists.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return localesDir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads all translation files from the locales directory.
|
|
46
|
+
*/
|
|
47
|
+
async readAll() {
|
|
48
|
+
const localesDir = await this.getLocalesDir();
|
|
49
|
+
const files = await fs.readdir(localesDir);
|
|
50
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
51
|
+
|
|
52
|
+
const translations = {};
|
|
53
|
+
for (const file of jsonFiles) {
|
|
54
|
+
const lang = path.basename(file, '.json');
|
|
55
|
+
const content = await fs.readJson(path.join(localesDir, file));
|
|
56
|
+
translations[lang] = content;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return translations;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Writes a single translation file (or all of them).
|
|
64
|
+
*/
|
|
65
|
+
async write(lang, content) {
|
|
66
|
+
const localesDir = await this.getLocalesDir();
|
|
67
|
+
const filePath = path.join(localesDir, `${lang}.json`);
|
|
68
|
+
await fs.writeJson(filePath, content, { spaces: 2 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Writes all translations.
|
|
73
|
+
*/
|
|
74
|
+
async writeAll(translations) {
|
|
75
|
+
const languages = Object.keys(translations);
|
|
76
|
+
for (const lang of languages) {
|
|
77
|
+
await this.write(lang, translations[lang]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = Storage;
|
|
@@ -1,204 +1,229 @@
|
|
|
1
|
-
const lodash = require('lodash');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const Storage = require('./Storage');
|
|
5
|
-
const Scanner = require('./Scanner');
|
|
6
|
-
const Utilities = require('./Utilities');
|
|
7
|
-
const GoogleTranslator = require('./services/GoogleTranslator');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Main manager class for translation management.
|
|
11
|
-
* Acts as an orchestrator.
|
|
12
|
-
*/
|
|
13
|
-
class TranslatorManager {
|
|
14
|
-
constructor(targetDir, config = {}) {
|
|
15
|
-
this.targetDir = targetDir;
|
|
16
|
-
this.config = config;
|
|
17
|
-
this.storage = new Storage(targetDir, config);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Re-scans translations and sources to build a complete picture.
|
|
22
|
-
*/
|
|
23
|
-
async scan() {
|
|
24
|
-
const localesDir = await this.storage.getLocalesDir();
|
|
25
|
-
const translations = await this.storage.readAll();
|
|
26
|
-
const languages = Object.keys(translations);
|
|
27
|
-
|
|
28
|
-
// Flatten all keys across all languages
|
|
29
|
-
const allKeysSet = new Set();
|
|
30
|
-
languages.forEach(lang => Utilities.flattenKeys(translations[lang], '', allKeysSet));
|
|
31
|
-
const allKeys = Array.from(allKeysSet).sort();
|
|
32
|
-
|
|
33
|
-
// Calculate results
|
|
34
|
-
const results = {};
|
|
35
|
-
allKeys.forEach(key => {
|
|
36
|
-
const missing = [];
|
|
37
|
-
languages.forEach(lang => {
|
|
38
|
-
const val = lodash.get(translations[lang], key);
|
|
39
|
-
if (val === undefined || val === '') {
|
|
40
|
-
missing.push(lang);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
results[key] = { missing };
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Scan source for unused and missing keys
|
|
47
|
-
const scanner = new Scanner(this.targetDir, localesDir, this.config);
|
|
48
|
-
const [analysis, missingFromFiles] = await Promise.all([
|
|
49
|
-
scanner.findUnusedKeys(allKeys),
|
|
50
|
-
scanner.findMissingKeys(allKeys)
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
localesDir,
|
|
55
|
-
languages,
|
|
56
|
-
translations,
|
|
57
|
-
allKeys,
|
|
58
|
-
results,
|
|
59
|
-
unused: analysis.unused,
|
|
60
|
-
maybeUsed: analysis.maybeUsed,
|
|
61
|
-
missingFromFiles: missingFromFiles
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Saves a translation key across all language files.
|
|
67
|
-
*/
|
|
68
|
-
async saveTranslation(key, values) {
|
|
69
|
-
const translations = await this.storage.readAll();
|
|
70
|
-
const languages = Object.keys(translations);
|
|
71
|
-
|
|
72
|
-
for (const lang of languages) {
|
|
73
|
-
if (values[lang] !== undefined) {
|
|
74
|
-
lodash.set(translations[lang], key, values[lang]);
|
|
75
|
-
await this.storage.write(lang, translations[lang]);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Deletes one or more translation keys across all language files.
|
|
82
|
-
*/
|
|
83
|
-
async deleteTranslations(keys) {
|
|
84
|
-
const keysToDelete = Array.isArray(keys) ? keys : [keys];
|
|
85
|
-
const translations = await this.storage.readAll();
|
|
86
|
-
const languages = Object.keys(translations);
|
|
87
|
-
|
|
88
|
-
for (const lang of languages) {
|
|
89
|
-
keysToDelete.forEach(key => lodash.unset(translations[lang], key));
|
|
90
|
-
await this.storage.write(lang, translations[lang]);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Normalizes and sorts everything.
|
|
96
|
-
*/
|
|
97
|
-
async normalize() {
|
|
98
|
-
let translations = await this.storage.readAll();
|
|
99
|
-
const languages = Object.keys(translations);
|
|
100
|
-
|
|
101
|
-
const allKeysSet = new Set();
|
|
102
|
-
languages.forEach(lang => Utilities.flattenKeys(translations[lang], '', allKeysSet));
|
|
103
|
-
const allKeys = Array.from(allKeysSet);
|
|
104
|
-
|
|
105
|
-
// Sync and sort
|
|
106
|
-
translations = Utilities.syncKeys(translations, allKeys);
|
|
107
|
-
|
|
108
|
-
// Save back
|
|
109
|
-
await this.storage.writeAll(translations);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Translates a specific text into target language.
|
|
114
|
-
*/
|
|
115
|
-
async translateSingle(text, targetLang, sourceLang = 'en') {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1
|
+
const lodash = require('lodash');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const Storage = require('./Storage');
|
|
5
|
+
const Scanner = require('./Scanner');
|
|
6
|
+
const Utilities = require('./Utilities');
|
|
7
|
+
const GoogleTranslator = require('./services/GoogleTranslator');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main manager class for translation management.
|
|
11
|
+
* Acts as an orchestrator.
|
|
12
|
+
*/
|
|
13
|
+
class TranslatorManager {
|
|
14
|
+
constructor(targetDir, config = {}) {
|
|
15
|
+
this.targetDir = targetDir;
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.storage = new Storage(targetDir, config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Re-scans translations and sources to build a complete picture.
|
|
22
|
+
*/
|
|
23
|
+
async scan() {
|
|
24
|
+
const localesDir = await this.storage.getLocalesDir();
|
|
25
|
+
const translations = await this.storage.readAll();
|
|
26
|
+
const languages = Object.keys(translations);
|
|
27
|
+
|
|
28
|
+
// Flatten all keys across all languages
|
|
29
|
+
const allKeysSet = new Set();
|
|
30
|
+
languages.forEach(lang => Utilities.flattenKeys(translations[lang], '', allKeysSet));
|
|
31
|
+
const allKeys = Array.from(allKeysSet).sort();
|
|
32
|
+
|
|
33
|
+
// Calculate results
|
|
34
|
+
const results = {};
|
|
35
|
+
allKeys.forEach(key => {
|
|
36
|
+
const missing = [];
|
|
37
|
+
languages.forEach(lang => {
|
|
38
|
+
const val = lodash.get(translations[lang], key);
|
|
39
|
+
if (val === undefined || val === '') {
|
|
40
|
+
missing.push(lang);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
results[key] = { missing };
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Scan source for unused and missing keys
|
|
47
|
+
const scanner = new Scanner(this.targetDir, localesDir, this.config);
|
|
48
|
+
const [analysis, missingFromFiles] = await Promise.all([
|
|
49
|
+
scanner.findUnusedKeys(allKeys),
|
|
50
|
+
scanner.findMissingKeys(allKeys)
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
localesDir,
|
|
55
|
+
languages,
|
|
56
|
+
translations,
|
|
57
|
+
allKeys,
|
|
58
|
+
results,
|
|
59
|
+
unused: analysis.unused,
|
|
60
|
+
maybeUsed: analysis.maybeUsed,
|
|
61
|
+
missingFromFiles: missingFromFiles
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Saves a translation key across all language files.
|
|
67
|
+
*/
|
|
68
|
+
async saveTranslation(key, values) {
|
|
69
|
+
const translations = await this.storage.readAll();
|
|
70
|
+
const languages = Object.keys(translations);
|
|
71
|
+
|
|
72
|
+
for (const lang of languages) {
|
|
73
|
+
if (values[lang] !== undefined) {
|
|
74
|
+
lodash.set(translations[lang], key, values[lang]);
|
|
75
|
+
await this.storage.write(lang, translations[lang]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Deletes one or more translation keys across all language files.
|
|
82
|
+
*/
|
|
83
|
+
async deleteTranslations(keys) {
|
|
84
|
+
const keysToDelete = Array.isArray(keys) ? keys : [keys];
|
|
85
|
+
const translations = await this.storage.readAll();
|
|
86
|
+
const languages = Object.keys(translations);
|
|
87
|
+
|
|
88
|
+
for (const lang of languages) {
|
|
89
|
+
keysToDelete.forEach(key => lodash.unset(translations[lang], key));
|
|
90
|
+
await this.storage.write(lang, translations[lang]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalizes and sorts everything.
|
|
96
|
+
*/
|
|
97
|
+
async normalize() {
|
|
98
|
+
let translations = await this.storage.readAll();
|
|
99
|
+
const languages = Object.keys(translations);
|
|
100
|
+
|
|
101
|
+
const allKeysSet = new Set();
|
|
102
|
+
languages.forEach(lang => Utilities.flattenKeys(translations[lang], '', allKeysSet));
|
|
103
|
+
const allKeys = Array.from(allKeysSet);
|
|
104
|
+
|
|
105
|
+
// Sync and sort
|
|
106
|
+
translations = Utilities.syncKeys(translations, allKeys);
|
|
107
|
+
|
|
108
|
+
// Save back
|
|
109
|
+
await this.storage.writeAll(translations);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Translates a specific text into target language.
|
|
114
|
+
*/
|
|
115
|
+
async translateSingle(text, targetLang, sourceLang = 'en') {
|
|
116
|
+
// Support both old and new config formats for backward compatibility
|
|
117
|
+
let translatorConfig;
|
|
118
|
+
if (this.config.googleTranslateApiKey) {
|
|
119
|
+
// Legacy v2 config - provide migration guidance
|
|
120
|
+
throw new Error('Google Translate API v2 is deprecated. Please update your configuration to use v3 with projectId and keyFilename in the googleTranslate object.');
|
|
121
|
+
} else if (this.config.googleTranslate) {
|
|
122
|
+
translatorConfig = this.config.googleTranslate;
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error('Google Translate configuration is missing. Please add googleTranslate.projectId and googleTranslate.keyFilename in Settings.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const translator = new GoogleTranslator(translatorConfig);
|
|
128
|
+
return await translator.translate(text, targetLang, sourceLang);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Gets a report of missing translations.
|
|
133
|
+
*/
|
|
134
|
+
async getBulkTranslateReport(sourceLang) {
|
|
135
|
+
const { languages, translations, allKeys } = await this.scan();
|
|
136
|
+
const report = {};
|
|
137
|
+
|
|
138
|
+
languages.forEach(lang => {
|
|
139
|
+
if (lang === sourceLang) return;
|
|
140
|
+
const missing = allKeys.filter(key => {
|
|
141
|
+
const val = lodash.get(translations[lang], key);
|
|
142
|
+
return val === undefined || val === '';
|
|
143
|
+
});
|
|
144
|
+
if (missing.length > 0) {
|
|
145
|
+
report[lang] = {
|
|
146
|
+
count: missing.length,
|
|
147
|
+
keys: missing
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return report;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Performs bulk translation for all missing keys.
|
|
157
|
+
* Returns an object mapping language to key-value pairs for review.
|
|
158
|
+
*/
|
|
159
|
+
async bulkTranslate(sourceLang) {
|
|
160
|
+
const report = await this.getBulkTranslateReport(sourceLang);
|
|
161
|
+
const translations = await this.storage.readAll();
|
|
162
|
+
|
|
163
|
+
// Support both old and new config formats for backward compatibility
|
|
164
|
+
let translatorConfig;
|
|
165
|
+
if (this.config.googleTranslateApiKey) {
|
|
166
|
+
// Legacy v2 config - provide migration guidance
|
|
167
|
+
throw new Error('Google Translate API v2 is deprecated. Please update your configuration to use v3 with projectId and keyFilename in the googleTranslate object.');
|
|
168
|
+
} else if (this.config.googleTranslate) {
|
|
169
|
+
translatorConfig = this.config.googleTranslate;
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error('Google Translate configuration is missing. Please add googleTranslate.projectId and googleTranslate.keyFilename in Settings.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const translator = new GoogleTranslator(translatorConfig);
|
|
175
|
+
|
|
176
|
+
const preview = {};
|
|
177
|
+
|
|
178
|
+
for (const lang in report) {
|
|
179
|
+
const keys = report[lang].keys;
|
|
180
|
+
const sourceTexts = keys.map(key => lodash.get(translations[sourceLang], key));
|
|
181
|
+
|
|
182
|
+
// Filter out keys that don't have source text
|
|
183
|
+
const validIndices = sourceTexts.map((text, idx) => text ? idx : null).filter(idx => idx !== null);
|
|
184
|
+
const textsToTranslate = validIndices.map(idx => sourceTexts[idx]);
|
|
185
|
+
const validKeys = validIndices.map(idx => keys[idx]);
|
|
186
|
+
|
|
187
|
+
if (textsToTranslate.length > 0) {
|
|
188
|
+
const translatedTexts = await translator.translate(textsToTranslate, lang, sourceLang);
|
|
189
|
+
preview[lang] = {};
|
|
190
|
+
validKeys.forEach((key, idx) => {
|
|
191
|
+
preview[lang][key] = translatedTexts[idx];
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return preview;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Saves multiple translation keys across languages.
|
|
201
|
+
* @param {Object} data - { lang: { key: value } }
|
|
202
|
+
*/
|
|
203
|
+
async saveBulkTranslations(data) {
|
|
204
|
+
const translations = await this.storage.readAll();
|
|
205
|
+
const languages = Object.keys(translations);
|
|
206
|
+
|
|
207
|
+
for (const lang in data) {
|
|
208
|
+
if (languages.includes(lang)) {
|
|
209
|
+
for (const key in data[lang]) {
|
|
210
|
+
lodash.set(translations[lang], key, data[lang][key]);
|
|
211
|
+
}
|
|
212
|
+
await this.storage.write(lang, translations[lang]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Saves the current configuration to the config file.
|
|
219
|
+
*/
|
|
220
|
+
async saveConfig(newConfig) {
|
|
221
|
+
this.config = { ...this.config, ...newConfig };
|
|
222
|
+
// Sync storage config if path changed
|
|
223
|
+
this.storage.config = this.config;
|
|
224
|
+
const configPath = path.resolve(this.targetDir, 'translation.config.json');
|
|
225
|
+
await fs.writeJson(configPath, this.config, { spaces: 2 });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = TranslatorManager;
|