@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.
@@ -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
- const translator = new GoogleTranslator(this.config.googleTranslateApiKey);
117
- return await translator.translate(text, targetLang, sourceLang);
118
- }
119
-
120
- /**
121
- * Gets a report of missing translations.
122
- */
123
- async getBulkTranslateReport(sourceLang) {
124
- const { languages, translations, allKeys } = await this.scan();
125
- const report = {};
126
-
127
- languages.forEach(lang => {
128
- if (lang === sourceLang) return;
129
- const missing = allKeys.filter(key => {
130
- const val = lodash.get(translations[lang], key);
131
- return val === undefined || val === '';
132
- });
133
- if (missing.length > 0) {
134
- report[lang] = {
135
- count: missing.length,
136
- keys: missing
137
- };
138
- }
139
- });
140
-
141
- return report;
142
- }
143
-
144
- /**
145
- * Performs bulk translation for all missing keys.
146
- * Returns an object mapping language to key-value pairs for review.
147
- */
148
- async bulkTranslate(sourceLang) {
149
- const report = await this.getBulkTranslateReport(sourceLang);
150
- const translations = await this.storage.readAll();
151
- const translator = new GoogleTranslator(this.config.googleTranslateApiKey);
152
-
153
- const preview = {};
154
-
155
- for (const lang in report) {
156
- const keys = report[lang].keys;
157
- const sourceTexts = keys.map(key => lodash.get(translations[sourceLang], key));
158
-
159
- // Filter out keys that don't have source text
160
- const validIndices = sourceTexts.map((text, idx) => text ? idx : null).filter(idx => idx !== null);
161
- const textsToTranslate = validIndices.map(idx => sourceTexts[idx]);
162
- const validKeys = validIndices.map(idx => keys[idx]);
163
-
164
- if (textsToTranslate.length > 0) {
165
- const translatedTexts = await translator.translate(textsToTranslate, lang, sourceLang);
166
- preview[lang] = {};
167
- validKeys.forEach((key, idx) => {
168
- preview[lang][key] = translatedTexts[idx];
169
- });
170
- }
171
- }
172
-
173
- return preview;
174
- }
175
-
176
- /**
177
- * Saves multiple translation keys across languages.
178
- * @param {Object} data - { lang: { key: value } }
179
- */
180
- async saveBulkTranslations(data) {
181
- const translations = await this.storage.readAll();
182
- const languages = Object.keys(translations);
183
-
184
- for (const lang in data) {
185
- if (languages.includes(lang)) {
186
- for (const key in data[lang]) {
187
- lodash.set(translations[lang], key, data[lang][key]);
188
- }
189
- await this.storage.write(lang, translations[lang]);
190
- }
191
- }
192
- }
193
-
194
- /**
195
- * Saves the current configuration to the config file.
196
- */
197
- async saveConfig(newConfig) {
198
- this.config = { ...this.config, ...newConfig };
199
- const configPath = path.resolve(this.targetDir, 'translation.config.json');
200
- await fs.writeJson(configPath, this.config, { spaces: 2 });
201
- }
202
- }
203
-
204
- module.exports = TranslatorManager;
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;