@umituz/web-localization 1.1.8 → 1.1.10
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 -0
- package/dist/domain/entities/translation.entity.d.ts +1 -0
- package/dist/domain/interfaces/translation-service.interface.d.ts +1 -2
- package/dist/infrastructure/constants/index.d.ts +2 -0
- package/dist/infrastructure/constants/index.js +3 -0
- package/dist/infrastructure/services/cli.service.d.ts +1 -0
- package/dist/infrastructure/services/cli.service.js +147 -55
- package/dist/infrastructure/services/google-translate.service.d.ts +45 -3
- package/dist/infrastructure/services/google-translate.service.js +324 -108
- package/dist/infrastructure/utils/file.util.d.ts +27 -1
- package/dist/infrastructure/utils/file.util.js +150 -11
- package/dist/infrastructure/utils/rate-limit.util.d.ts +37 -4
- package/dist/infrastructure/utils/rate-limit.util.js +109 -10
- package/dist/infrastructure/utils/text-validator.util.d.ts +1 -1
- package/dist/infrastructure/utils/text-validator.util.js +4 -4
- package/dist/integrations/i18n.setup.d.ts +43 -5
- package/dist/integrations/i18n.setup.js +133 -6
- package/dist/scripts/cli.js +6 -1
- package/package.json +10 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 umituz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -4,7 +4,6 @@ import type { TranslationRequest, TranslationResponse, TranslationStats } from "
|
|
|
4
4
|
*/
|
|
5
5
|
export interface TranslationServiceConfig {
|
|
6
6
|
minDelay?: number;
|
|
7
|
-
maxRetries?: number;
|
|
8
7
|
timeout?: number;
|
|
9
8
|
apiKey?: string;
|
|
10
9
|
}
|
|
@@ -16,5 +15,5 @@ export interface ITranslationService {
|
|
|
16
15
|
isInitialized(): boolean;
|
|
17
16
|
translate(request: TranslationRequest): Promise<TranslationResponse>;
|
|
18
17
|
translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
|
|
19
|
-
translateObject(sourceObject: Record<string, unknown>, targetObject: Record<string, unknown>, targetLanguage: string, path?: string, stats?: TranslationStats, onTranslate?: (key: string, from: string, to: string) => void): Promise<void>;
|
|
18
|
+
translateObject(sourceObject: Record<string, unknown>, targetObject: Record<string, unknown>, targetLanguage: string, path?: string, stats?: TranslationStats, onTranslate?: (key: string, from: string, to: string) => void, force?: boolean): Promise<void>;
|
|
20
19
|
}
|
|
@@ -8,3 +8,5 @@ export declare const DEFAULT_TIMEOUT = 10000;
|
|
|
8
8
|
export declare const DEFAULT_LOCALES_DIR = "src/locales";
|
|
9
9
|
export declare const DEFAULT_SOURCE_DIR = "src";
|
|
10
10
|
export declare const DEFAULT_BASE_LANGUAGE = "en-US";
|
|
11
|
+
export declare const TRANSLATION_BATCH_SIZE = 50;
|
|
12
|
+
export declare const TRANSLATION_CONCURRENCY_LIMIT = 10;
|
|
@@ -8,3 +8,6 @@ export const DEFAULT_TIMEOUT = 10000;
|
|
|
8
8
|
export const DEFAULT_LOCALES_DIR = "src/locales";
|
|
9
9
|
export const DEFAULT_SOURCE_DIR = "src";
|
|
10
10
|
export const DEFAULT_BASE_LANGUAGE = "en-US";
|
|
11
|
+
// Translation batch settings
|
|
12
|
+
export const TRANSLATION_BATCH_SIZE = 50;
|
|
13
|
+
export const TRANSLATION_CONCURRENCY_LIMIT = 10;
|
|
@@ -3,7 +3,63 @@ import path from "path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { googleTranslateService } from "./google-translate.service.js";
|
|
5
5
|
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util.js";
|
|
6
|
-
import { DEFAULT_LOCALES_DIR, DEFAULT_BASE_LANGUAGE } from "../constants/index.js";
|
|
6
|
+
import { DEFAULT_LOCALES_DIR, DEFAULT_BASE_LANGUAGE, TRANSLATION_CONCURRENCY_LIMIT, } from "../constants/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Semaphore for controlling concurrent operations
|
|
9
|
+
*/
|
|
10
|
+
class Semaphore {
|
|
11
|
+
permits;
|
|
12
|
+
queue = [];
|
|
13
|
+
constructor(permits) {
|
|
14
|
+
this.permits = permits;
|
|
15
|
+
}
|
|
16
|
+
async acquire() {
|
|
17
|
+
if (this.permits > 0) {
|
|
18
|
+
this.permits--;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
this.queue.push(resolve);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
release() {
|
|
26
|
+
this.permits++;
|
|
27
|
+
const next = this.queue.shift();
|
|
28
|
+
if (next) {
|
|
29
|
+
this.permits--;
|
|
30
|
+
next();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async run(fn) {
|
|
34
|
+
await this.acquire();
|
|
35
|
+
try {
|
|
36
|
+
return await fn();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
this.release();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Extracted outside loop for better performance
|
|
44
|
+
const syncObject = (source, target) => {
|
|
45
|
+
const result = { ...target };
|
|
46
|
+
for (const key in source) {
|
|
47
|
+
if (typeof source[key] === "object" && source[key] !== null) {
|
|
48
|
+
result[key] = syncObject(source[key], target[key] || {});
|
|
49
|
+
}
|
|
50
|
+
else if (target[key] === undefined) {
|
|
51
|
+
// Let empty string indicate untranslated state
|
|
52
|
+
result[key] = typeof source[key] === "string" ? "" : source[key];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Remove extra keys
|
|
56
|
+
for (const key in target) {
|
|
57
|
+
if (source[key] === undefined) {
|
|
58
|
+
delete result[key];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
7
63
|
export class CLIService {
|
|
8
64
|
async sync(options = {}) {
|
|
9
65
|
const localesDir = path.resolve(process.cwd(), options.localesDir || DEFAULT_LOCALES_DIR);
|
|
@@ -18,38 +74,27 @@ export class CLIService {
|
|
|
18
74
|
return;
|
|
19
75
|
}
|
|
20
76
|
const baseData = parseTypeScriptFile(baseLangPath);
|
|
77
|
+
// Pre-compile regex for better performance
|
|
78
|
+
const localeFileRegex = /^[a-z]{2}(-[A-Z]{2})?\.ts$/;
|
|
21
79
|
const files = fs.readdirSync(localesDir)
|
|
22
|
-
.filter(f =>
|
|
80
|
+
.filter(f => localeFileRegex.test(f) && f !== `${baseLang}.ts`)
|
|
23
81
|
.sort();
|
|
24
82
|
console.log(chalk.blue(`📊 Found ${files.length} languages to sync with ${baseLang}.\n`));
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
// Remove extra keys
|
|
42
|
-
for (const key in target) {
|
|
43
|
-
if (source[key] === undefined) {
|
|
44
|
-
delete result[key];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
};
|
|
49
|
-
const syncedData = syncObject(baseData, targetData);
|
|
50
|
-
fs.writeFileSync(targetPath, generateTypeScriptContent(syncedData, langCode));
|
|
51
|
-
console.log(chalk.green(` 🌍 ${langCode}: Synced structure.`));
|
|
52
|
-
}
|
|
83
|
+
// Process files in parallel with controlled concurrency
|
|
84
|
+
const concurrency = options.concurrency || TRANSLATION_CONCURRENCY_LIMIT;
|
|
85
|
+
const semaphore = new Semaphore(concurrency);
|
|
86
|
+
const syncPromises = files.map(async (file) => {
|
|
87
|
+
return semaphore.run(async () => {
|
|
88
|
+
const targetPath = path.join(localesDir, file);
|
|
89
|
+
const targetData = parseTypeScriptFile(targetPath);
|
|
90
|
+
const langCode = path.basename(file, ".ts");
|
|
91
|
+
const syncedData = syncObject(baseData, targetData);
|
|
92
|
+
fs.writeFileSync(targetPath, generateTypeScriptContent(syncedData, langCode));
|
|
93
|
+
// Non-blocking progress update
|
|
94
|
+
process.stdout.write(chalk.green(` 🌍 ${langCode}: Synced structure.\n`));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
await Promise.all(syncPromises);
|
|
53
98
|
console.log(chalk.bold.green("\n✅ Synchronization completed!"));
|
|
54
99
|
}
|
|
55
100
|
async translate(options = {}) {
|
|
@@ -62,35 +107,82 @@ export class CLIService {
|
|
|
62
107
|
}
|
|
63
108
|
googleTranslateService.initialize({});
|
|
64
109
|
const baseData = parseTypeScriptFile(baseLangPath);
|
|
110
|
+
// Pre-compile regex for better performance
|
|
111
|
+
const localeFileRegex = /^[a-z]{2}(-[A-Z]{2})?\.ts$/;
|
|
65
112
|
const files = fs.readdirSync(localesDir)
|
|
66
|
-
.filter(f =>
|
|
113
|
+
.filter(f => localeFileRegex.test(f) && f !== `${baseLang}.ts`)
|
|
67
114
|
.sort();
|
|
68
115
|
console.log(chalk.blue.bold(`🚀 Starting automatic translation for ${files.length} languages...\n`));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
// Process languages in parallel with controlled concurrency
|
|
117
|
+
const concurrency = options.concurrency || TRANSLATION_CONCURRENCY_LIMIT;
|
|
118
|
+
const semaphore = new Semaphore(concurrency);
|
|
119
|
+
// Shared statistics tracking
|
|
120
|
+
const totalStats = {
|
|
121
|
+
totalLanguages: files.length,
|
|
122
|
+
completedLanguages: 0,
|
|
123
|
+
totalSuccess: 0,
|
|
124
|
+
totalFailure: 0,
|
|
125
|
+
};
|
|
126
|
+
const translatePromises = files.map(async (file) => {
|
|
127
|
+
return semaphore.run(async () => {
|
|
128
|
+
const targetPath = path.join(localesDir, file);
|
|
129
|
+
const targetData = parseTypeScriptFile(targetPath);
|
|
130
|
+
const langCode = path.basename(file, ".ts");
|
|
131
|
+
const stats = {
|
|
132
|
+
totalCount: 0,
|
|
133
|
+
successCount: 0,
|
|
134
|
+
failureCount: 0,
|
|
135
|
+
skippedCount: 0,
|
|
136
|
+
translatedKeys: []
|
|
137
|
+
};
|
|
138
|
+
// Non-blocking progress update with language name
|
|
139
|
+
const langName = langCode.padEnd(6);
|
|
140
|
+
process.stdout.write(chalk.yellow(`🌍 Translating ${langName}...\n`));
|
|
141
|
+
try {
|
|
142
|
+
// Extract ISO 639-1 language code (e.g., "en" from "en-US")
|
|
143
|
+
const targetLang = langCode.includes("-") ? langCode.split("-")[0] : langCode;
|
|
144
|
+
await googleTranslateService.translateObject(baseData, targetData, targetLang, "", stats, (_key, _from, _to) => {
|
|
145
|
+
// Non-blocking per-key progress (only in verbose mode or for debugging)
|
|
146
|
+
// Commented out to reduce console spam
|
|
147
|
+
// process.stdout.write(chalk.gray(` • ${key}: ${from.substring(0, 15)}... → ${to.substring(0, 15)}...\r`));
|
|
148
|
+
}, options.force);
|
|
149
|
+
// Write translated content
|
|
150
|
+
if (stats.successCount > 0) {
|
|
151
|
+
fs.writeFileSync(targetPath, generateTypeScriptContent(targetData, langCode));
|
|
152
|
+
totalStats.totalSuccess += stats.successCount;
|
|
153
|
+
process.stdout.write(chalk.green(` ✅ ${langName} Successfully translated ${stats.successCount} keys.\n`));
|
|
154
|
+
}
|
|
155
|
+
else if (stats.failureCount > 0) {
|
|
156
|
+
totalStats.totalFailure += stats.failureCount;
|
|
157
|
+
process.stdout.write(chalk.red(` ❌ ${langName} Failed to translate ${stats.failureCount} keys.\n`));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
process.stdout.write(chalk.gray(` ✨ ${langName} Already up to date.\n`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
totalStats.totalFailure += stats.failureCount;
|
|
165
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
166
|
+
process.stdout.write(chalk.red(` ❌ ${langName} Error: ${errorMsg}\n`));
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
totalStats.completedLanguages++;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
await Promise.all(translatePromises);
|
|
174
|
+
// Final summary
|
|
93
175
|
console.log(chalk.bold.green("\n✅ All translations completed!"));
|
|
176
|
+
console.log(chalk.gray(` 📊 Processed ${totalStats.completedLanguages}/${totalStats.totalLanguages} languages`));
|
|
177
|
+
if (totalStats.totalSuccess > 0) {
|
|
178
|
+
console.log(chalk.green(` ✅ Total keys translated: ${totalStats.totalSuccess}`));
|
|
179
|
+
}
|
|
180
|
+
if (totalStats.totalFailure > 0) {
|
|
181
|
+
console.log(chalk.red(` ❌ Total keys failed: ${totalStats.totalFailure}`));
|
|
182
|
+
}
|
|
183
|
+
// Display cache statistics
|
|
184
|
+
const cacheStats = googleTranslateService.getCacheStats();
|
|
185
|
+
console.log(chalk.gray(` 💾 Cache hit rate: ${cacheStats.size}/${cacheStats.maxSize}`));
|
|
94
186
|
}
|
|
95
187
|
}
|
|
96
188
|
export const cliService = new CLIService();
|
|
@@ -1,19 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Google Translate Service
|
|
3
|
-
* @description Main translation service using Google Translate API
|
|
2
|
+
* Google Translate Service with Performance Optimizations
|
|
3
|
+
* @description Main translation service using Google Translate API with caching and pooling
|
|
4
4
|
*/
|
|
5
5
|
import type { TranslationRequest, TranslationResponse, TranslationStats } from "../../domain/entities/translation.entity.js";
|
|
6
6
|
import type { ITranslationService, TranslationServiceConfig } from "../../domain/interfaces/translation-service.interface.js";
|
|
7
7
|
declare class GoogleTranslateService implements ITranslationService {
|
|
8
8
|
private config;
|
|
9
|
-
private
|
|
9
|
+
private _rateLimiter;
|
|
10
|
+
private translationCache;
|
|
11
|
+
private readonly maxCacheSize;
|
|
12
|
+
private readonly cacheTTL;
|
|
13
|
+
private readonly mapPool;
|
|
14
|
+
private activeRequests;
|
|
15
|
+
private readonly maxConcurrentRequests;
|
|
10
16
|
initialize(config: TranslationServiceConfig): void;
|
|
11
17
|
isInitialized(): boolean;
|
|
18
|
+
private get rateLimiter();
|
|
12
19
|
private ensureInitialized;
|
|
20
|
+
/**
|
|
21
|
+
* Generate cache key for translation
|
|
22
|
+
*/
|
|
23
|
+
private getCacheKey;
|
|
24
|
+
/**
|
|
25
|
+
* Get translation from cache
|
|
26
|
+
*/
|
|
27
|
+
private getFromCache;
|
|
28
|
+
/**
|
|
29
|
+
* Store translation in cache with automatic eviction
|
|
30
|
+
*/
|
|
31
|
+
private storeInCache;
|
|
32
|
+
/**
|
|
33
|
+
* Clear translation cache
|
|
34
|
+
*/
|
|
35
|
+
clearCache(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get cache statistics
|
|
38
|
+
*/
|
|
39
|
+
getCacheStats(): {
|
|
40
|
+
size: number;
|
|
41
|
+
maxSize: number;
|
|
42
|
+
};
|
|
13
43
|
translate(request: TranslationRequest): Promise<TranslationResponse>;
|
|
14
44
|
translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
|
|
15
45
|
translateObject(sourceObject: Record<string, unknown>, targetObject: Record<string, unknown>, targetLanguage: string, path?: string, stats?: TranslationStats, onTranslate?: (key: string, from: string, to: string) => void, force?: boolean): Promise<void>;
|
|
16
46
|
private callTranslateAPI;
|
|
47
|
+
/**
|
|
48
|
+
* Optimized sleep function
|
|
49
|
+
*/
|
|
50
|
+
private sleep;
|
|
51
|
+
/**
|
|
52
|
+
* Get number of active requests
|
|
53
|
+
*/
|
|
54
|
+
getActiveRequestCount(): number;
|
|
55
|
+
/**
|
|
56
|
+
* Clean up resources
|
|
57
|
+
*/
|
|
58
|
+
dispose(): void;
|
|
17
59
|
}
|
|
18
60
|
export declare const googleTranslateService: GoogleTranslateService;
|
|
19
61
|
export { GoogleTranslateService };
|