@umituz/react-native-google-translate 1.0.3 ā 1.0.5
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/dist/domain/entities/Language.entity.js +5 -0
- package/dist/domain/entities/Translation.entity.js +5 -0
- package/dist/domain/entities/index.js +5 -0
- package/dist/domain/index.js +8 -0
- package/dist/domain/interfaces/ITranslationService.interface.js +5 -0
- package/dist/domain/interfaces/index.js +5 -0
- package/dist/infrastructure/constants/api.constants.js +8 -0
- package/dist/infrastructure/constants/index.js +6 -0
- package/dist/infrastructure/constants/languages.constants.js +96 -0
- package/dist/infrastructure/services/GoogleTranslate.service.js +200 -0
- package/dist/infrastructure/services/index.js +7 -0
- package/dist/infrastructure/utils/rateLimit.util.js +23 -0
- package/dist/infrastructure/utils/textValidator.util.js +50 -0
- package/dist/scripts/index.js +8 -0
- package/dist/scripts/setup.js +75 -0
- package/dist/scripts/sync.js +106 -0
- package/dist/scripts/translate.js +86 -0
- package/dist/scripts/utils/file-parser.js +90 -0
- package/dist/scripts/utils/index.js +9 -0
- package/dist/scripts/utils/key-detector.js +39 -0
- package/dist/scripts/utils/key-extractor.js +95 -0
- package/dist/scripts/utils/object-helper.js +43 -0
- package/dist/scripts/utils/sync-helper.js +47 -0
- package/package.json +13 -3
- package/src/infrastructure/index.ts +10 -0
- package/src/infrastructure/services/GoogleTranslate.service.ts +2 -3
- package/src/infrastructure/services/index.ts +16 -1
- package/src/presentation/hooks/useBatchTranslation.hook.ts +3 -1
- package/src/scripts/index.ts +9 -0
- package/src/scripts/setup.ts +95 -0
- package/src/scripts/sync.ts +153 -0
- package/src/scripts/translate.ts +126 -0
- package/src/scripts/utils/file-parser.ts +103 -0
- package/src/scripts/utils/index.ts +10 -0
- package/src/scripts/utils/key-detector.ts +70 -0
- package/src/scripts/utils/key-extractor.ts +108 -0
- package/src/scripts/utils/object-helper.ts +49 -0
- package/src/scripts/utils/sync-helper.ts +78 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Constants
|
|
3
|
+
* @description Google Translate API configuration
|
|
4
|
+
*/
|
|
5
|
+
export const GOOGLE_TRANSLATE_API_URL = "https://translate.googleapis.com/translate_a/single";
|
|
6
|
+
export const DEFAULT_TIMEOUT = 10000;
|
|
7
|
+
export const DEFAULT_MIN_DELAY = 100;
|
|
8
|
+
export const DEFAULT_MAX_RETRIES = 3;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Constants
|
|
3
|
+
* @description Exports all constant definitions
|
|
4
|
+
*/
|
|
5
|
+
export { LANGUAGE_MAP, SKIP_WORDS, LANGUAGE_NAMES, } from "./languages.constants";
|
|
6
|
+
export { GOOGLE_TRANSLATE_API_URL, DEFAULT_TIMEOUT, DEFAULT_MIN_DELAY, DEFAULT_MAX_RETRIES, } from "./api.constants";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Constants
|
|
3
|
+
* @description Language mappings and metadata
|
|
4
|
+
*/
|
|
5
|
+
export const LANGUAGE_MAP = {
|
|
6
|
+
"ar-SA": "ar",
|
|
7
|
+
"bg-BG": "bg",
|
|
8
|
+
"cs-CZ": "cs",
|
|
9
|
+
"da-DK": "da",
|
|
10
|
+
"de-DE": "de",
|
|
11
|
+
"el-GR": "el",
|
|
12
|
+
"en-AU": "en",
|
|
13
|
+
"en-CA": "en",
|
|
14
|
+
"en-GB": "en",
|
|
15
|
+
"es-ES": "es",
|
|
16
|
+
"es-MX": "es",
|
|
17
|
+
"fi-FI": "fi",
|
|
18
|
+
"fr-CA": "fr",
|
|
19
|
+
"fr-FR": "fr",
|
|
20
|
+
"hi-IN": "hi",
|
|
21
|
+
"hr-HR": "hr",
|
|
22
|
+
"hu-HU": "hu",
|
|
23
|
+
"id-ID": "id",
|
|
24
|
+
"it-IT": "it",
|
|
25
|
+
"ja-JP": "ja",
|
|
26
|
+
"ko-KR": "ko",
|
|
27
|
+
"ms-MY": "ms",
|
|
28
|
+
"nl-NL": "nl",
|
|
29
|
+
"no-NO": "no",
|
|
30
|
+
"pl-PL": "pl",
|
|
31
|
+
"pt-BR": "pt",
|
|
32
|
+
"pt-PT": "pt",
|
|
33
|
+
"ro-RO": "ro",
|
|
34
|
+
"ru-RU": "ru",
|
|
35
|
+
"sk-SK": "sk",
|
|
36
|
+
"sl-SI": "sl",
|
|
37
|
+
"sv-SE": "sv",
|
|
38
|
+
"th-TH": "th",
|
|
39
|
+
"tl-PH": "tl",
|
|
40
|
+
"tr-TR": "tr",
|
|
41
|
+
"uk-UA": "uk",
|
|
42
|
+
"vi-VN": "vi",
|
|
43
|
+
"zh-CN": "zh-CN",
|
|
44
|
+
"zh-TW": "zh-TW",
|
|
45
|
+
};
|
|
46
|
+
export const SKIP_WORDS = new Set([
|
|
47
|
+
"Google",
|
|
48
|
+
"Apple",
|
|
49
|
+
"Facebook",
|
|
50
|
+
"Instagram",
|
|
51
|
+
"Twitter",
|
|
52
|
+
"YouTube",
|
|
53
|
+
"WhatsApp",
|
|
54
|
+
]);
|
|
55
|
+
export const LANGUAGE_NAMES = {
|
|
56
|
+
"ar-SA": "Arabic (Saudi Arabia)",
|
|
57
|
+
"bg-BG": "Bulgarian",
|
|
58
|
+
"cs-CZ": "Czech",
|
|
59
|
+
"da-DK": "Danish",
|
|
60
|
+
"de-DE": "German",
|
|
61
|
+
"el-GR": "Greek",
|
|
62
|
+
"en-AU": "English (Australia)",
|
|
63
|
+
"en-CA": "English (Canada)",
|
|
64
|
+
"en-GB": "English (UK)",
|
|
65
|
+
"en-US": "English (US)",
|
|
66
|
+
"es-ES": "Spanish (Spain)",
|
|
67
|
+
"es-MX": "Spanish (Mexico)",
|
|
68
|
+
"fi-FI": "Finnish",
|
|
69
|
+
"fr-CA": "French (Canada)",
|
|
70
|
+
"fr-FR": "French (France)",
|
|
71
|
+
"hi-IN": "Hindi",
|
|
72
|
+
"hr-HR": "Croatian",
|
|
73
|
+
"hu-HU": "Hungarian",
|
|
74
|
+
"id-ID": "Indonesian",
|
|
75
|
+
"it-IT": "Italian",
|
|
76
|
+
"ja-JP": "Japanese",
|
|
77
|
+
"ko-KR": "Korean",
|
|
78
|
+
"ms-MY": "Malay",
|
|
79
|
+
"nl-NL": "Dutch",
|
|
80
|
+
"no-NO": "Norwegian",
|
|
81
|
+
"pl-PL": "Polish",
|
|
82
|
+
"pt-BR": "Portuguese (Brazil)",
|
|
83
|
+
"pt-PT": "Portuguese (Portugal)",
|
|
84
|
+
"ro-RO": "Romanian",
|
|
85
|
+
"ru-RU": "Russian",
|
|
86
|
+
"sk-SK": "Slovak",
|
|
87
|
+
"sl-SI": "Slovenian",
|
|
88
|
+
"sv-SE": "Swedish",
|
|
89
|
+
"th-TH": "Thai",
|
|
90
|
+
"tl-PH": "Tagalog",
|
|
91
|
+
"tr-TR": "Turkish",
|
|
92
|
+
"uk-UA": "Ukrainian",
|
|
93
|
+
"vi-VN": "Vietnamese",
|
|
94
|
+
"zh-CN": "Chinese (Simplified)",
|
|
95
|
+
"zh-TW": "Chinese (Traditional)",
|
|
96
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Translate Service
|
|
3
|
+
* @description Main translation service using Google Translate API
|
|
4
|
+
*/
|
|
5
|
+
import { RateLimiter } from "../utils/rateLimit.util";
|
|
6
|
+
import { shouldSkipWord, needsTranslation, isValidText, } from "../utils/textValidator.util";
|
|
7
|
+
import { GOOGLE_TRANSLATE_API_URL, DEFAULT_MIN_DELAY, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, } from "../constants";
|
|
8
|
+
class GoogleTranslateService {
|
|
9
|
+
config = null;
|
|
10
|
+
rateLimiter = null;
|
|
11
|
+
initialize(config) {
|
|
12
|
+
this.config = {
|
|
13
|
+
minDelay: DEFAULT_MIN_DELAY,
|
|
14
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
15
|
+
timeout: DEFAULT_TIMEOUT,
|
|
16
|
+
...config,
|
|
17
|
+
};
|
|
18
|
+
this.rateLimiter = new RateLimiter(this.config.minDelay);
|
|
19
|
+
}
|
|
20
|
+
isInitialized() {
|
|
21
|
+
return this.config !== null && this.rateLimiter !== null;
|
|
22
|
+
}
|
|
23
|
+
ensureInitialized() {
|
|
24
|
+
if (!this.isInitialized()) {
|
|
25
|
+
throw new Error("GoogleTranslateService is not initialized. Call initialize() first.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async translate(request) {
|
|
29
|
+
this.ensureInitialized();
|
|
30
|
+
const { text, targetLanguage, sourceLanguage = "en" } = request;
|
|
31
|
+
if (!isValidText(text) || shouldSkipWord(text)) {
|
|
32
|
+
return {
|
|
33
|
+
originalText: text,
|
|
34
|
+
translatedText: text,
|
|
35
|
+
sourceLanguage,
|
|
36
|
+
targetLanguage,
|
|
37
|
+
success: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
originalText: text,
|
|
43
|
+
translatedText: text,
|
|
44
|
+
sourceLanguage,
|
|
45
|
+
targetLanguage,
|
|
46
|
+
success: false,
|
|
47
|
+
error: "Invalid target language",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// After ensureInitialized(), rateLimiter is guaranteed to be non-null
|
|
51
|
+
await this.rateLimiter.waitForSlot();
|
|
52
|
+
try {
|
|
53
|
+
const translatedText = await this.callTranslateAPI(text, targetLanguage, sourceLanguage);
|
|
54
|
+
return {
|
|
55
|
+
originalText: text,
|
|
56
|
+
translatedText,
|
|
57
|
+
sourceLanguage,
|
|
58
|
+
targetLanguage,
|
|
59
|
+
success: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
originalText: text,
|
|
65
|
+
translatedText: text,
|
|
66
|
+
sourceLanguage,
|
|
67
|
+
targetLanguage,
|
|
68
|
+
success: false,
|
|
69
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async translateBatch(requests) {
|
|
74
|
+
this.ensureInitialized();
|
|
75
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
totalCount: 0,
|
|
78
|
+
successCount: 0,
|
|
79
|
+
failureCount: 0,
|
|
80
|
+
skippedCount: 0,
|
|
81
|
+
translatedKeys: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const stats = {
|
|
85
|
+
totalCount: requests.length,
|
|
86
|
+
successCount: 0,
|
|
87
|
+
failureCount: 0,
|
|
88
|
+
skippedCount: 0,
|
|
89
|
+
translatedKeys: [],
|
|
90
|
+
};
|
|
91
|
+
for (const request of requests) {
|
|
92
|
+
const result = await this.translate(request);
|
|
93
|
+
if (result.success) {
|
|
94
|
+
if (result.translatedText === result.originalText) {
|
|
95
|
+
stats.skippedCount++;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
stats.successCount++;
|
|
99
|
+
stats.translatedKeys.push({
|
|
100
|
+
key: request.text,
|
|
101
|
+
from: result.originalText,
|
|
102
|
+
to: result.translatedText,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
stats.failureCount++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return stats;
|
|
111
|
+
}
|
|
112
|
+
async translateObject(sourceObject, targetObject, targetLanguage, path = "", stats = {
|
|
113
|
+
totalCount: 0,
|
|
114
|
+
successCount: 0,
|
|
115
|
+
failureCount: 0,
|
|
116
|
+
skippedCount: 0,
|
|
117
|
+
translatedKeys: [],
|
|
118
|
+
}) {
|
|
119
|
+
if (!sourceObject || typeof sourceObject !== "object") {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!targetObject || typeof targetObject !== "object") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const keys = Object.keys(sourceObject);
|
|
129
|
+
for (const key of keys) {
|
|
130
|
+
const enValue = sourceObject[key];
|
|
131
|
+
const targetValue = targetObject[key];
|
|
132
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
133
|
+
if (typeof enValue === "object" && enValue !== null) {
|
|
134
|
+
if (!targetObject[key] ||
|
|
135
|
+
typeof targetObject[key] !== "object") {
|
|
136
|
+
targetObject[key] = {};
|
|
137
|
+
}
|
|
138
|
+
await this.translateObject(enValue, targetObject[key], targetLanguage, currentPath, stats);
|
|
139
|
+
}
|
|
140
|
+
else if (typeof enValue === "string") {
|
|
141
|
+
stats.totalCount++;
|
|
142
|
+
if (needsTranslation(targetValue, enValue)) {
|
|
143
|
+
const request = {
|
|
144
|
+
text: enValue,
|
|
145
|
+
targetLanguage,
|
|
146
|
+
};
|
|
147
|
+
const result = await this.translate(request);
|
|
148
|
+
if (result.success && result.translatedText !== enValue) {
|
|
149
|
+
targetObject[key] = result.translatedText;
|
|
150
|
+
stats.successCount++;
|
|
151
|
+
stats.translatedKeys.push({
|
|
152
|
+
key: currentPath,
|
|
153
|
+
from: enValue,
|
|
154
|
+
to: result.translatedText,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (!result.success) {
|
|
158
|
+
stats.failureCount++;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
stats.skippedCount++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
stats.skippedCount++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async callTranslateAPI(text, targetLanguage, sourceLanguage) {
|
|
171
|
+
const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
|
|
172
|
+
const encodedText = encodeURIComponent(text);
|
|
173
|
+
const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(url, {
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(`API request failed: ${response.status}`);
|
|
182
|
+
}
|
|
183
|
+
const data = await response.json();
|
|
184
|
+
// Type guard for Google Translate API response structure
|
|
185
|
+
if (Array.isArray(data) &&
|
|
186
|
+
data.length > 0 &&
|
|
187
|
+
Array.isArray(data[0]) &&
|
|
188
|
+
data[0].length > 0 &&
|
|
189
|
+
Array.isArray(data[0][0]) &&
|
|
190
|
+
typeof data[0][0][0] === "string") {
|
|
191
|
+
return data[0][0][0];
|
|
192
|
+
}
|
|
193
|
+
return text;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export const googleTranslateService = new GoogleTranslateService();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Services
|
|
3
|
+
* @description Exports all services and utilities
|
|
4
|
+
*/
|
|
5
|
+
export { googleTranslateService } from "./GoogleTranslate.service";
|
|
6
|
+
export { shouldSkipWord, needsTranslation, isValidText, getTargetLanguage, isEnglishVariant, getLanguageDisplayName, } from "../utils/textValidator.util";
|
|
7
|
+
export { LANGUAGE_MAP, SKIP_WORDS, LANGUAGE_NAMES, } from "../constants/languages.constants";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Utility
|
|
3
|
+
* @description Handles rate limiting for API requests
|
|
4
|
+
*/
|
|
5
|
+
export class RateLimiter {
|
|
6
|
+
lastCallTime = 0;
|
|
7
|
+
minDelay;
|
|
8
|
+
constructor(minDelay = 100) {
|
|
9
|
+
this.minDelay = minDelay;
|
|
10
|
+
}
|
|
11
|
+
async waitForSlot() {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const elapsed = now - this.lastCallTime;
|
|
14
|
+
const waitTime = Math.max(0, this.minDelay - elapsed);
|
|
15
|
+
if (waitTime > 0) {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
17
|
+
}
|
|
18
|
+
this.lastCallTime = Date.now();
|
|
19
|
+
}
|
|
20
|
+
reset() {
|
|
21
|
+
this.lastCallTime = 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Validator Utility
|
|
3
|
+
* @description Validates text for translation eligibility
|
|
4
|
+
*/
|
|
5
|
+
import { SKIP_WORDS, LANGUAGE_MAP, LANGUAGE_NAMES } from "../constants";
|
|
6
|
+
export function shouldSkipWord(word) {
|
|
7
|
+
return SKIP_WORDS.has(word);
|
|
8
|
+
}
|
|
9
|
+
export function needsTranslation(value, enValue) {
|
|
10
|
+
if (typeof enValue !== "string" || !enValue.trim()) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (shouldSkipWord(enValue)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
// Skip technical keys (e.g., "scenario.xxx.title")
|
|
17
|
+
const isTechnicalKey = enValue.includes(".") && !enValue.includes(" ");
|
|
18
|
+
if (isTechnicalKey) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
// If value is missing or same as English, it needs translation
|
|
22
|
+
if (!value || typeof value !== "string") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (value === enValue) {
|
|
26
|
+
const isSingleWord = !enValue.includes(" ") && enValue.length < 20;
|
|
27
|
+
return !isSingleWord;
|
|
28
|
+
}
|
|
29
|
+
// Detect outdated template patterns (e.g., {{appName}}, {{variable}})
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
const hasTemplatePattern = value.includes("{{") && value.includes("}}");
|
|
32
|
+
if (hasTemplatePattern && !enValue.includes("{{")) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
export function isValidText(text) {
|
|
39
|
+
return typeof text === "string" && text.length > 0;
|
|
40
|
+
}
|
|
41
|
+
export function getTargetLanguage(langCode) {
|
|
42
|
+
return LANGUAGE_MAP[langCode];
|
|
43
|
+
}
|
|
44
|
+
export function isEnglishVariant(langCode) {
|
|
45
|
+
const targetLang = getTargetLanguage(langCode);
|
|
46
|
+
return targetLang === "en";
|
|
47
|
+
}
|
|
48
|
+
export function getLanguageDisplayName(code) {
|
|
49
|
+
return LANGUAGE_NAMES[code] || code;
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Setup Languages Script
|
|
4
|
+
* Creates stub files for all supported languages (if not exist),
|
|
5
|
+
* then generates index.ts from all available translation files.
|
|
6
|
+
* Usage: node setup.ts [locales-dir]
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { LANGUAGE_MAP, getLanguageDisplayName } from '../infrastructure/services';
|
|
11
|
+
export function setupLanguages(options) {
|
|
12
|
+
const { targetDir } = options;
|
|
13
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
14
|
+
if (!fs.existsSync(localesDir)) {
|
|
15
|
+
console.error(`ā Locales directory not found: ${localesDir}`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
// Create stub files for all supported languages that don't exist yet
|
|
19
|
+
let created = 0;
|
|
20
|
+
for (const langCode of Object.keys(LANGUAGE_MAP)) {
|
|
21
|
+
// Skip English variants ā en-US is the base, others (en-AU, en-GB) are redundant
|
|
22
|
+
if (langCode.startsWith('en-') && langCode !== 'en-US')
|
|
23
|
+
continue;
|
|
24
|
+
const filePath = path.join(localesDir, `${langCode}.ts`);
|
|
25
|
+
if (!fs.existsSync(filePath)) {
|
|
26
|
+
const langName = getLanguageDisplayName(langCode);
|
|
27
|
+
fs.writeFileSync(filePath, `/**\n * ${langName} Translations\n * Auto-synced from en-US.ts\n */\n\nexport default {};\n`);
|
|
28
|
+
console.log(` ā
Created ${langCode}.ts (${langName})`);
|
|
29
|
+
created++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (created > 0) {
|
|
33
|
+
console.log(`\nš¦ Created ${created} new language stubs.\n`);
|
|
34
|
+
}
|
|
35
|
+
// Generate index.ts from all language files
|
|
36
|
+
const files = fs.readdirSync(localesDir)
|
|
37
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/))
|
|
38
|
+
.sort();
|
|
39
|
+
const imports = [];
|
|
40
|
+
const exports = [];
|
|
41
|
+
files.forEach(file => {
|
|
42
|
+
const code = file.replace('.ts', '');
|
|
43
|
+
const varName = code.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase()).replace('-', '');
|
|
44
|
+
imports.push(`import ${varName} from "./${code}";`);
|
|
45
|
+
exports.push(` "${code}": ${varName},`);
|
|
46
|
+
});
|
|
47
|
+
const content = `/**
|
|
48
|
+
* Localization Index
|
|
49
|
+
* Exports all available translation files
|
|
50
|
+
* Auto-generated by scripts/setup.ts
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
${imports.join('\n')}
|
|
54
|
+
|
|
55
|
+
export const translations = {
|
|
56
|
+
${exports.join('\n')}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TranslationKey = keyof typeof translations;
|
|
60
|
+
|
|
61
|
+
export default translations;
|
|
62
|
+
`;
|
|
63
|
+
fs.writeFileSync(path.join(localesDir, 'index.ts'), content);
|
|
64
|
+
console.log(`ā
Generated index.ts with ${files.length} languages`);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
// CLI interface
|
|
68
|
+
export function runSetupLanguages() {
|
|
69
|
+
const targetDir = process.argv[2] || 'src/infrastructure/locales';
|
|
70
|
+
console.log('š Setting up language files...\n');
|
|
71
|
+
setupLanguages({ targetDir });
|
|
72
|
+
}
|
|
73
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
74
|
+
runSetupLanguages();
|
|
75
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sync Translations Script
|
|
4
|
+
* Synchronizes translation keys from en-US.ts to all other language files
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { parseTypeScriptFile, generateTypeScriptContent, } from './utils/file-parser';
|
|
9
|
+
import { addMissingKeys, removeExtraKeys, } from './utils/sync-helper';
|
|
10
|
+
import { detectNewKeys } from './utils/key-detector';
|
|
11
|
+
import { extractUsedKeys } from './utils/key-extractor';
|
|
12
|
+
import { setDeep, countKeys } from './utils/object-helper';
|
|
13
|
+
export function syncLanguageFile(enUSPath, targetPath, langCode) {
|
|
14
|
+
const enUS = parseTypeScriptFile(enUSPath);
|
|
15
|
+
let target;
|
|
16
|
+
try {
|
|
17
|
+
target = parseTypeScriptFile(targetPath);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
target = {};
|
|
21
|
+
}
|
|
22
|
+
const detectedNewKeys = detectNewKeys(enUS, target);
|
|
23
|
+
const addStats = { added: 0, newKeys: [] };
|
|
24
|
+
const removeStats = { removed: 0, removedKeys: [] };
|
|
25
|
+
addMissingKeys(enUS, target, addStats);
|
|
26
|
+
removeExtraKeys(enUS, target, removeStats);
|
|
27
|
+
const changed = (addStats.added || 0) > 0 || (removeStats.removed || 0) > 0;
|
|
28
|
+
if (changed) {
|
|
29
|
+
const content = generateTypeScriptContent(target, langCode);
|
|
30
|
+
fs.writeFileSync(targetPath, content);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
added: addStats.added,
|
|
34
|
+
newKeys: addStats.newKeys,
|
|
35
|
+
removed: removeStats.removed,
|
|
36
|
+
removedKeys: removeStats.removedKeys,
|
|
37
|
+
changed,
|
|
38
|
+
detectedNewKeys,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function processExtraction(srcDir, enUSPath) {
|
|
42
|
+
if (!srcDir)
|
|
43
|
+
return;
|
|
44
|
+
console.log(`š Scanning source code and dependencies: ${srcDir}...`);
|
|
45
|
+
const usedKeyMap = extractUsedKeys(srcDir);
|
|
46
|
+
console.log(` Found ${usedKeyMap.size} unique keys.`);
|
|
47
|
+
const oldEnUS = parseTypeScriptFile(enUSPath);
|
|
48
|
+
const newEnUS = {};
|
|
49
|
+
let addedCount = 0;
|
|
50
|
+
for (const [key, defaultValue] of usedKeyMap) {
|
|
51
|
+
// Try to keep existing translation if it exists
|
|
52
|
+
const existingValue = key.split('.').reduce((obj, k) => {
|
|
53
|
+
if (obj && typeof obj === 'object' && k in obj) {
|
|
54
|
+
return obj[k];
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}, oldEnUS);
|
|
58
|
+
// We treat it as "not translated" if the value is exactly the key string
|
|
59
|
+
const isActuallyTranslated = typeof existingValue === 'string' && existingValue !== key;
|
|
60
|
+
const valueToSet = isActuallyTranslated ? existingValue : defaultValue;
|
|
61
|
+
if (setDeep(newEnUS, key, valueToSet)) {
|
|
62
|
+
if (!isActuallyTranslated)
|
|
63
|
+
addedCount++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const oldTotal = countKeys(oldEnUS);
|
|
67
|
+
const newTotal = countKeys(newEnUS);
|
|
68
|
+
const removedCount = Math.max(0, oldTotal - (newTotal - addedCount));
|
|
69
|
+
console.log(` ⨠Optimized en-US.ts: ${addedCount} keys populated/updated, pruned ${removedCount} unused.`);
|
|
70
|
+
const content = generateTypeScriptContent(newEnUS, 'en-US');
|
|
71
|
+
fs.writeFileSync(enUSPath, content);
|
|
72
|
+
}
|
|
73
|
+
export function syncTranslations(options) {
|
|
74
|
+
const { targetDir, srcDir } = options;
|
|
75
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
76
|
+
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
77
|
+
if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
|
|
78
|
+
console.error(`ā Localization files not found in: ${localesDir}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
processExtraction(srcDir, enUSPath);
|
|
82
|
+
const files = fs.readdirSync(localesDir)
|
|
83
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
|
|
84
|
+
.sort();
|
|
85
|
+
console.log(`š Languages to sync: ${files.length}\n`);
|
|
86
|
+
files.forEach(file => {
|
|
87
|
+
const langCode = file.replace('.ts', '');
|
|
88
|
+
const targetPath = path.join(localesDir, file);
|
|
89
|
+
const result = syncLanguageFile(enUSPath, targetPath, langCode);
|
|
90
|
+
if (result.changed) {
|
|
91
|
+
console.log(` š ${langCode}: āļø +${result.added || 0} keys, -${result.removed || 0} keys`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
console.log(`\nā
Synchronization completed!`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// CLI interface
|
|
98
|
+
export function runSyncTranslations() {
|
|
99
|
+
const targetDir = process.argv[2] || 'src/infrastructure/locales';
|
|
100
|
+
const srcDir = process.argv[3];
|
|
101
|
+
console.log('š Starting translation synchronization...\n');
|
|
102
|
+
syncTranslations({ targetDir, srcDir });
|
|
103
|
+
}
|
|
104
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
105
|
+
runSyncTranslations();
|
|
106
|
+
}
|