@tuhama/translation-manager 0.3.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;
@@ -113,7 +113,18 @@ class TranslatorManager {
113
113
  * Translates a specific text into target language.
114
114
  */
115
115
  async translateSingle(text, targetLang, sourceLang = 'en') {
116
- const translator = new GoogleTranslator(this.config.googleTranslateApiKey);
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);
117
128
  return await translator.translate(text, targetLang, sourceLang);
118
129
  }
119
130
 
@@ -123,7 +134,7 @@ class TranslatorManager {
123
134
  async getBulkTranslateReport(sourceLang) {
124
135
  const { languages, translations, allKeys } = await this.scan();
125
136
  const report = {};
126
-
137
+
127
138
  languages.forEach(lang => {
128
139
  if (lang === sourceLang) return;
129
140
  const missing = allKeys.filter(key => {
@@ -148,14 +159,26 @@ class TranslatorManager {
148
159
  async bulkTranslate(sourceLang) {
149
160
  const report = await this.getBulkTranslateReport(sourceLang);
150
161
  const translations = await this.storage.readAll();
151
- const translator = new GoogleTranslator(this.config.googleTranslateApiKey);
152
-
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
+
153
176
  const preview = {};
154
177
 
155
178
  for (const lang in report) {
156
179
  const keys = report[lang].keys;
157
180
  const sourceTexts = keys.map(key => lodash.get(translations[sourceLang], key));
158
-
181
+
159
182
  // Filter out keys that don't have source text
160
183
  const validIndices = sourceTexts.map((text, idx) => text ? idx : null).filter(idx => idx !== null);
161
184
  const textsToTranslate = validIndices.map(idx => sourceTexts[idx]);
@@ -1,55 +1,55 @@
1
- const lodash = require('lodash');
2
-
3
- /**
4
- * Utility functions for object manipulation.
5
- */
6
- class Utilities {
7
- /**
8
- * Recursively sorts object keys alphabetically.
9
- */
10
- static sortObject(obj) {
11
- if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
12
- return obj;
13
- }
14
-
15
- const sorted = {};
16
- Object.keys(obj).sort().forEach(key => {
17
- sorted[key] = Utilities.sortObject(obj[key]);
18
- });
19
- return sorted;
20
- }
21
-
22
- /**
23
- * Flattens a nested object into a set of dot-notated keys.
24
- */
25
- static flattenKeys(obj, prefix = '', keySet = new Set()) {
26
- Object.keys(obj).forEach(key => {
27
- const fullKey = prefix ? `${prefix}.${key}` : key;
28
- if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
29
- Utilities.flattenKeys(obj[key], fullKey, keySet);
30
- } else {
31
- keySet.add(fullKey);
32
- }
33
- });
34
- return keySet;
35
- }
36
-
37
- /**
38
- * Normalizes translation keys across all languages.
39
- */
40
- static syncKeys(translations, allKeys) {
41
- const languages = Object.keys(translations);
42
- languages.forEach(lang => {
43
- const content = translations[lang];
44
- allKeys.forEach(key => {
45
- if (lodash.get(content, key) === undefined) {
46
- lodash.set(content, key, '');
47
- }
48
- });
49
- translations[lang] = Utilities.sortObject(content);
50
- });
51
- return translations;
52
- }
53
- }
54
-
55
- module.exports = Utilities;
1
+ const lodash = require('lodash');
2
+
3
+ /**
4
+ * Utility functions for object manipulation.
5
+ */
6
+ class Utilities {
7
+ /**
8
+ * Recursively sorts object keys alphabetically.
9
+ */
10
+ static sortObject(obj) {
11
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
12
+ return obj;
13
+ }
14
+
15
+ const sorted = {};
16
+ Object.keys(obj).sort().forEach(key => {
17
+ sorted[key] = Utilities.sortObject(obj[key]);
18
+ });
19
+ return sorted;
20
+ }
21
+
22
+ /**
23
+ * Flattens a nested object into a set of dot-notated keys.
24
+ */
25
+ static flattenKeys(obj, prefix = '', keySet = new Set()) {
26
+ Object.keys(obj).forEach(key => {
27
+ const fullKey = prefix ? `${prefix}.${key}` : key;
28
+ if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
29
+ Utilities.flattenKeys(obj[key], fullKey, keySet);
30
+ } else {
31
+ keySet.add(fullKey);
32
+ }
33
+ });
34
+ return keySet;
35
+ }
36
+
37
+ /**
38
+ * Normalizes translation keys across all languages.
39
+ */
40
+ static syncKeys(translations, allKeys) {
41
+ const languages = Object.keys(translations);
42
+ languages.forEach(lang => {
43
+ const content = translations[lang];
44
+ allKeys.forEach(key => {
45
+ if (lodash.get(content, key) === undefined) {
46
+ lodash.set(content, key, '');
47
+ }
48
+ });
49
+ translations[lang] = Utilities.sortObject(content);
50
+ });
51
+ return translations;
52
+ }
53
+ }
54
+
55
+ module.exports = Utilities;
@@ -1,12 +1,30 @@
1
- const axios = require('axios');
1
+ const { Translate } = require('@google-cloud/translate').v3;
2
2
 
3
3
  /**
4
- * Service for interacting with Google Cloud Translation API.
4
+ * Service for interacting with Google Cloud Translation API v3.
5
5
  */
6
6
  class GoogleTranslator {
7
- constructor(apiKey) {
8
- this.apiKey = apiKey;
9
- this.baseUrl = 'https://translation.googleapis.com/language/translate/v2';
7
+ constructor(config) {
8
+ // Support both old API key format and new v3 config
9
+ if (typeof config === 'string') {
10
+ // Legacy API key - throw error to guide migration
11
+ throw new Error('Google Translate API v2 is deprecated. Please update your configuration to use v3 with projectId and keyFilename.');
12
+ }
13
+
14
+ this.projectId = config.projectId;
15
+ this.keyFilename = config.keyFilename;
16
+
17
+ if (!this.projectId) {
18
+ throw new Error('Google Cloud Project ID is required for Translate API v3. Please add it in Settings.');
19
+ }
20
+
21
+ // Initialize the translate client
22
+ const clientConfig = { projectId: this.projectId };
23
+ if (this.keyFilename) {
24
+ clientConfig.keyFilename = this.keyFilename;
25
+ }
26
+
27
+ this.translate = new Translate(clientConfig);
10
28
  }
11
29
 
12
30
  /**
@@ -16,8 +34,8 @@ class GoogleTranslator {
16
34
  * @param {string} sourceLang - Source language code (e.g. 'en')
17
35
  */
18
36
  async translate(text, targetLang, sourceLang = 'en') {
19
- if (!this.apiKey) {
20
- throw new Error('Google Translate API Key is missing. Please add it in Settings.');
37
+ if (!this.projectId) {
38
+ throw new Error('Google Cloud Project ID is missing. Please add it in Settings.');
21
39
  }
22
40
 
23
41
  if (!text || (Array.isArray(text) && text.length === 0)) {
@@ -25,24 +43,24 @@ class GoogleTranslator {
25
43
  }
26
44
 
27
45
  try {
28
- const response = await axios.post(
29
- `${this.baseUrl}?key=${this.apiKey}`,
30
- {
31
- q: text,
32
- target: targetLang,
33
- source: sourceLang,
34
- format: 'text'
35
- }
36
- );
37
-
38
- const translations = response.data.data.translations;
39
-
46
+ const location = 'global';
47
+ const request = {
48
+ parent: `projects/${this.projectId}/locations/${location}`,
49
+ contents: Array.isArray(text) ? text : [text],
50
+ mimeType: 'text/plain',
51
+ sourceLanguageCode: sourceLang,
52
+ targetLanguageCode: targetLang,
53
+ };
54
+
55
+ const [response] = await this.translate.translateText(request);
56
+ const translations = response.translations.map(t => t.translatedText);
57
+
40
58
  if (Array.isArray(text)) {
41
- return translations.map(t => t.translatedText);
59
+ return translations;
42
60
  }
43
- return translations[0].translatedText;
61
+ return translations[0];
44
62
  } catch (error) {
45
- const message = error.response?.data?.error?.message || error.message;
63
+ const message = error.message || 'Unknown translation error';
46
64
  throw new Error(`Google Translate Error: ${message}`);
47
65
  }
48
66
  }