@umituz/web-localization 1.1.2 → 1.1.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/translation.entity.d.ts +0 -1
- package/dist/domain/interfaces/translation-service.interface.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/infrastructure/constants/index.d.ts +0 -1
- package/dist/infrastructure/services/cli.service.d.ts +0 -1
- package/dist/infrastructure/services/google-translate.service.d.ts +0 -1
- package/dist/infrastructure/utils/file.util.d.ts +0 -1
- package/dist/infrastructure/utils/rate-limit.util.d.ts +0 -1
- package/dist/infrastructure/utils/text-validator.util.d.ts +0 -1
- package/dist/integrations/i18n.setup.d.ts +0 -1
- package/dist/scripts/cli.d.ts +0 -1
- package/package.json +1 -5
- package/src/domain/interfaces/translation-service.interface.ts +2 -1
- package/src/infrastructure/constants/index.ts +4 -0
- package/src/infrastructure/services/cli.service.ts +40 -33
- package/src/infrastructure/services/google-translate.service.ts +44 -29
- package/src/infrastructure/utils/file.util.ts +10 -2
- package/src/infrastructure/utils/rate-limit.util.ts +13 -5
- package/src/infrastructure/utils/text-validator.util.ts +5 -2
- package/src/integrations/i18n.setup.ts +14 -5
- package/src/scripts/cli.ts +10 -3
- package/dist/domain/entities/translation.entity.d.ts.map +0 -1
- package/dist/domain/interfaces/translation-service.interface.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/infrastructure/constants/index.d.ts.map +0 -1
- package/dist/infrastructure/services/cli.service.d.ts.map +0 -1
- package/dist/infrastructure/services/google-translate.service.d.ts.map +0 -1
- package/dist/infrastructure/utils/file.util.d.ts.map +0 -1
- package/dist/infrastructure/utils/rate-limit.util.d.ts.map +0 -1
- package/dist/infrastructure/utils/text-validator.util.d.ts.map +0 -1
- package/dist/integrations/i18n.setup.d.ts.map +0 -1
- package/dist/scripts/cli.d.ts.map +0 -1
|
@@ -18,4 +18,3 @@ export interface ITranslationService {
|
|
|
18
18
|
translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
|
|
19
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>;
|
|
20
20
|
}
|
|
21
|
-
//# sourceMappingURL=translation-service.interface.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare function parseTypeScriptFile(filePath: string): Record<string, un
|
|
|
7
7
|
* Generates a TypeScript file content from an object
|
|
8
8
|
*/
|
|
9
9
|
export declare function generateTypeScriptContent(obj: Record<string, unknown>, langCode?: string): string;
|
|
10
|
-
//# sourceMappingURL=file.util.d.ts.map
|
package/dist/scripts/cli.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-localization",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Google Translate integrated localization package for web applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./src/index.ts",
|
|
14
|
-
"./services": "./src/infrastructure/services/index.ts",
|
|
15
|
-
"./utils": "./src/infrastructure/utils/index.ts",
|
|
16
14
|
"./setup": "./src/integrations/i18n.setup.ts",
|
|
17
15
|
"./package.json": "./package.json"
|
|
18
16
|
},
|
|
@@ -38,8 +36,6 @@
|
|
|
38
36
|
"dependencies": {
|
|
39
37
|
"chalk": "^5.3.0",
|
|
40
38
|
"commander": "^12.0.0",
|
|
41
|
-
"dotenv": "^16.4.5",
|
|
42
|
-
"ts-morph": "^27.0.2",
|
|
43
39
|
"i18next": "^23.11.2",
|
|
44
40
|
"react-i18next": "^14.1.1",
|
|
45
41
|
"i18next-browser-languagedetector": "^7.2.1",
|
|
@@ -28,6 +28,7 @@ export interface ITranslationService {
|
|
|
28
28
|
targetLanguage: string,
|
|
29
29
|
path?: string,
|
|
30
30
|
stats?: TranslationStats,
|
|
31
|
-
onTranslate?: (key: string, from: string, to: string) => void
|
|
31
|
+
onTranslate?: (key: string, from: string, to: string) => void,
|
|
32
|
+
force?: boolean
|
|
32
33
|
): Promise<void>;
|
|
33
34
|
}
|
|
@@ -11,3 +11,7 @@ export const DEFAULT_TIMEOUT = 10000;
|
|
|
11
11
|
export const DEFAULT_LOCALES_DIR = "src/locales";
|
|
12
12
|
export const DEFAULT_SOURCE_DIR = "src";
|
|
13
13
|
export const DEFAULT_BASE_LANGUAGE = "en-US";
|
|
14
|
+
|
|
15
|
+
// Rate limiter defaults
|
|
16
|
+
export const RATE_LIMIT_DEFAULT_DELAY = 100;
|
|
17
|
+
export const RATE_LIMIT_MIN_DELAY = 0;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import type { TranslationStats } from "../../domain/entities/translation.entity.js";
|
|
4
5
|
import { googleTranslateService } from "./google-translate.service.js";
|
|
5
6
|
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util.js";
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_LOCALES_DIR,
|
|
8
|
-
DEFAULT_BASE_LANGUAGE
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_LOCALES_DIR,
|
|
9
|
+
DEFAULT_BASE_LANGUAGE
|
|
9
10
|
} from "../constants/index.js";
|
|
10
11
|
|
|
11
12
|
export interface SyncOptions {
|
|
@@ -15,6 +16,32 @@ export interface SyncOptions {
|
|
|
15
16
|
force?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// Extracted outside loop for better performance
|
|
20
|
+
const syncObject = (
|
|
21
|
+
source: Record<string, unknown>,
|
|
22
|
+
target: Record<string, unknown>
|
|
23
|
+
): Record<string, unknown> => {
|
|
24
|
+
const result = { ...target };
|
|
25
|
+
for (const key in source) {
|
|
26
|
+
if (typeof source[key] === "object" && source[key] !== null) {
|
|
27
|
+
result[key] = syncObject(
|
|
28
|
+
source[key] as Record<string, unknown>,
|
|
29
|
+
(target[key] as Record<string, unknown>) || {}
|
|
30
|
+
);
|
|
31
|
+
} else if (target[key] === undefined) {
|
|
32
|
+
// Let empty string indicate untranslated state
|
|
33
|
+
result[key] = typeof source[key] === "string" ? "" : source[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Remove extra keys
|
|
37
|
+
for (const key in target) {
|
|
38
|
+
if (source[key] === undefined) {
|
|
39
|
+
delete result[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
|
|
18
45
|
export class CLIService {
|
|
19
46
|
async sync(options: SyncOptions = {}): Promise<void> {
|
|
20
47
|
const localesDir = path.resolve(process.cwd(), options.localesDir || DEFAULT_LOCALES_DIR);
|
|
@@ -33,7 +60,7 @@ export class CLIService {
|
|
|
33
60
|
|
|
34
61
|
const baseData = parseTypeScriptFile(baseLangPath);
|
|
35
62
|
const files = fs.readdirSync(localesDir)
|
|
36
|
-
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}
|
|
63
|
+
.filter(f => f.match(/^[a-z]{2}(-[A-Z]{2})?\.ts$/) && f !== `${baseLang}.ts`)
|
|
37
64
|
.sort();
|
|
38
65
|
|
|
39
66
|
console.log(chalk.blue(`📊 Found ${files.length} languages to sync with ${baseLang}.\n`));
|
|
@@ -41,30 +68,7 @@ export class CLIService {
|
|
|
41
68
|
for (const file of files) {
|
|
42
69
|
const targetPath = path.join(localesDir, file);
|
|
43
70
|
const targetData = parseTypeScriptFile(targetPath);
|
|
44
|
-
const langCode =
|
|
45
|
-
|
|
46
|
-
// Deep merge with base data structure
|
|
47
|
-
const syncObject = (source: Record<string, unknown>, target: Record<string, unknown>): Record<string, unknown> => {
|
|
48
|
-
const result = { ...target };
|
|
49
|
-
for (const key in source) {
|
|
50
|
-
if (typeof source[key] === "object" && source[key] !== null) {
|
|
51
|
-
result[key] = syncObject(
|
|
52
|
-
source[key] as Record<string, unknown>,
|
|
53
|
-
(target[key] as Record<string, unknown>) || {}
|
|
54
|
-
);
|
|
55
|
-
} else if (target[key] === undefined) {
|
|
56
|
-
// Let empty string indicate untranslated state
|
|
57
|
-
result[key] = typeof source[key] === "string" ? "" : source[key];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// Remove extra keys
|
|
61
|
-
for (const key in target) {
|
|
62
|
-
if (source[key] === undefined) {
|
|
63
|
-
delete result[key];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return result;
|
|
67
|
-
};
|
|
71
|
+
const langCode = path.basename(file, ".ts");
|
|
68
72
|
|
|
69
73
|
const syncedData = syncObject(baseData, targetData);
|
|
70
74
|
fs.writeFileSync(targetPath, generateTypeScriptContent(syncedData, langCode));
|
|
@@ -87,7 +91,7 @@ export class CLIService {
|
|
|
87
91
|
googleTranslateService.initialize({});
|
|
88
92
|
const baseData = parseTypeScriptFile(baseLangPath);
|
|
89
93
|
const files = fs.readdirSync(localesDir)
|
|
90
|
-
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}
|
|
94
|
+
.filter(f => f.match(/^[a-z]{2}(-[A-Z]{2})?\.ts$/) && f !== `${baseLang}.ts`)
|
|
91
95
|
.sort();
|
|
92
96
|
|
|
93
97
|
console.log(chalk.blue.bold(`🚀 Starting automatic translation for ${files.length} languages...\n`));
|
|
@@ -95,9 +99,9 @@ export class CLIService {
|
|
|
95
99
|
for (const file of files) {
|
|
96
100
|
const targetPath = path.join(localesDir, file);
|
|
97
101
|
const targetData = parseTypeScriptFile(targetPath);
|
|
98
|
-
const langCode =
|
|
99
|
-
|
|
100
|
-
const stats = {
|
|
102
|
+
const langCode = path.basename(file, ".ts");
|
|
103
|
+
|
|
104
|
+
const stats: TranslationStats = {
|
|
101
105
|
totalCount: 0,
|
|
102
106
|
successCount: 0,
|
|
103
107
|
failureCount: 0,
|
|
@@ -107,10 +111,13 @@ export class CLIService {
|
|
|
107
111
|
|
|
108
112
|
console.log(chalk.yellow(`🌍 Translating ${langCode}...`));
|
|
109
113
|
|
|
114
|
+
// Extract ISO 639-1 language code (e.g., "en" from "en-US")
|
|
115
|
+
const targetLang = langCode.includes("-") ? langCode.split("-")[0] : langCode;
|
|
116
|
+
|
|
110
117
|
await googleTranslateService.translateObject(
|
|
111
118
|
baseData,
|
|
112
119
|
targetData,
|
|
113
|
-
|
|
120
|
+
targetLang,
|
|
114
121
|
"",
|
|
115
122
|
stats,
|
|
116
123
|
(key, from, to) => {
|
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
GOOGLE_TRANSLATE_API_URL,
|
|
23
23
|
DEFAULT_MIN_DELAY,
|
|
24
24
|
DEFAULT_TIMEOUT,
|
|
25
|
+
DEFAULT_MAX_RETRIES,
|
|
25
26
|
} from "../constants/index.js";
|
|
26
27
|
|
|
27
28
|
class GoogleTranslateService implements ITranslationService {
|
|
28
29
|
private config: TranslationServiceConfig | null = null;
|
|
29
|
-
private
|
|
30
|
+
private _rateLimiter: RateLimiter | null = null;
|
|
30
31
|
|
|
31
32
|
initialize(config: TranslationServiceConfig): void {
|
|
32
33
|
this.config = {
|
|
@@ -34,11 +35,18 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
34
35
|
timeout: DEFAULT_TIMEOUT,
|
|
35
36
|
...config,
|
|
36
37
|
};
|
|
37
|
-
this.
|
|
38
|
+
this._rateLimiter = new RateLimiter(this.config.minDelay);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
isInitialized(): boolean {
|
|
41
|
-
return this.config !== null && this.
|
|
42
|
+
return this.config !== null && this._rateLimiter !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private get rateLimiter(): RateLimiter {
|
|
46
|
+
if (!this._rateLimiter) {
|
|
47
|
+
throw new Error("RateLimiter not initialized");
|
|
48
|
+
}
|
|
49
|
+
return this._rateLimiter;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
private ensureInitialized(): void {
|
|
@@ -75,7 +83,7 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
75
83
|
};
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
await this.rateLimiter
|
|
86
|
+
await this.rateLimiter.waitForSlot();
|
|
79
87
|
|
|
80
88
|
try {
|
|
81
89
|
const translatedText = await this.callTranslateAPI(
|
|
@@ -127,9 +135,9 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
for (const chunk of chunks) {
|
|
130
|
-
const results = await Promise.
|
|
138
|
+
const results = await Promise.allSettled(
|
|
131
139
|
chunk.map(async (request) => {
|
|
132
|
-
await this.rateLimiter
|
|
140
|
+
await this.rateLimiter.waitForSlot();
|
|
133
141
|
return this.callTranslateAPI(
|
|
134
142
|
request.text,
|
|
135
143
|
request.targetLanguage,
|
|
@@ -140,19 +148,22 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
140
148
|
|
|
141
149
|
for (let i = 0; i < chunk.length; i++) {
|
|
142
150
|
const request = chunk[i];
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
const result = results[i];
|
|
152
|
+
|
|
153
|
+
if (result.status === "fulfilled") {
|
|
154
|
+
const translatedText = result.value;
|
|
155
|
+
if (translatedText && translatedText !== request.text) {
|
|
156
|
+
stats.successCount++;
|
|
157
|
+
stats.translatedKeys.push({
|
|
158
|
+
key: request.text,
|
|
159
|
+
from: request.text,
|
|
160
|
+
to: translatedText,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
stats.skippedCount++;
|
|
164
|
+
}
|
|
154
165
|
} else {
|
|
155
|
-
stats.
|
|
166
|
+
stats.failureCount++;
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
}
|
|
@@ -221,16 +232,20 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
221
232
|
}))
|
|
222
233
|
);
|
|
223
234
|
|
|
224
|
-
|
|
235
|
+
// Create a map for quick lookup of translations
|
|
236
|
+
const translationMap = new Map<string, string>();
|
|
237
|
+
for (const item of results.translatedKeys) {
|
|
238
|
+
translationMap.set(item.from, item.to);
|
|
239
|
+
}
|
|
240
|
+
|
|
225
241
|
for (let j = 0; j < batch.length; j++) {
|
|
226
242
|
const {key, enValue, currentPath} = batch[j];
|
|
227
|
-
const
|
|
243
|
+
const translatedText = translationMap.get(enValue);
|
|
228
244
|
|
|
229
|
-
if (
|
|
230
|
-
targetObject[key] =
|
|
245
|
+
if (translatedText && translatedText !== enValue) {
|
|
246
|
+
targetObject[key] = translatedText;
|
|
231
247
|
stats.successCount++;
|
|
232
|
-
if (onTranslate) onTranslate(currentPath, enValue,
|
|
233
|
-
resultIndex++;
|
|
248
|
+
if (onTranslate) onTranslate(currentPath, enValue, translatedText);
|
|
234
249
|
} else {
|
|
235
250
|
stats.failureCount++;
|
|
236
251
|
}
|
|
@@ -243,7 +258,7 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
243
258
|
text: string,
|
|
244
259
|
targetLanguage: string,
|
|
245
260
|
sourceLanguage: string,
|
|
246
|
-
retries =
|
|
261
|
+
retries = DEFAULT_MAX_RETRIES,
|
|
247
262
|
backoffMs = 2000
|
|
248
263
|
): Promise<string> {
|
|
249
264
|
// 1. Variable Protection (Extract {{variables}})
|
|
@@ -251,7 +266,7 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
251
266
|
let counter = 0;
|
|
252
267
|
|
|
253
268
|
// Find all {{something}} patterns
|
|
254
|
-
|
|
269
|
+
const safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
|
|
255
270
|
const placeholder = `_VAR${counter}_`; // Using a simple token less likely to be split
|
|
256
271
|
varMap.set(placeholder, match);
|
|
257
272
|
counter++;
|
|
@@ -262,7 +277,7 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
262
277
|
const encodedText = encodeURIComponent(safeText);
|
|
263
278
|
const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
|
|
264
279
|
|
|
265
|
-
for (let attempt = 0; attempt
|
|
280
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
266
281
|
const controller = new AbortController();
|
|
267
282
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
268
283
|
|
|
@@ -273,7 +288,7 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
273
288
|
|
|
274
289
|
if (!response.ok) {
|
|
275
290
|
if (response.status === 429 || response.status >= 500) {
|
|
276
|
-
if (attempt < retries) {
|
|
291
|
+
if (attempt < retries - 1) {
|
|
277
292
|
clearTimeout(timeoutId);
|
|
278
293
|
// Exponential backoff
|
|
279
294
|
const delay = backoffMs * Math.pow(2, attempt);
|
|
@@ -294,7 +309,7 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
294
309
|
data[0].length > 0 &&
|
|
295
310
|
typeof data[0][0][0] === "string"
|
|
296
311
|
) {
|
|
297
|
-
translatedStr = data[0].map((item:
|
|
312
|
+
translatedStr = data[0].map((item: unknown[]) => item[0] as string).join('');
|
|
298
313
|
}
|
|
299
314
|
|
|
300
315
|
// 2. Re-inject Variables
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Parses a TypeScript file containing an object export
|
|
5
6
|
* @description Simplistic parser for 'export default { ... }' or 'export const data = { ... }'
|
|
7
|
+
* @security Note: Uses Function constructor - only use with trusted local files
|
|
6
8
|
*/
|
|
7
9
|
export function parseTypeScriptFile(filePath: string): Record<string, unknown> {
|
|
8
|
-
|
|
10
|
+
// Validate file path is within project directory
|
|
11
|
+
const resolvedPath = path.resolve(filePath);
|
|
12
|
+
if (!resolvedPath.startsWith(process.cwd())) {
|
|
13
|
+
throw new Error(`Security: File path outside project directory: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(resolvedPath)) return {};
|
|
9
17
|
|
|
10
|
-
const content = fs.readFileSync(
|
|
18
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
11
19
|
|
|
12
20
|
// Extract the object part
|
|
13
21
|
// This is a naive implementation, but matches the pattern used in the project
|
|
@@ -3,23 +3,31 @@
|
|
|
3
3
|
* @description Controls the frequency of API requests
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { RATE_LIMIT_DEFAULT_DELAY } from "../constants/index.js";
|
|
7
|
+
|
|
6
8
|
export class RateLimiter {
|
|
7
9
|
private lastRequestTime = 0;
|
|
8
|
-
private minDelay: number;
|
|
10
|
+
private readonly minDelay: number;
|
|
9
11
|
|
|
10
|
-
constructor(minDelay =
|
|
12
|
+
constructor(minDelay = RATE_LIMIT_DEFAULT_DELAY) {
|
|
13
|
+
if (minDelay < 0) {
|
|
14
|
+
throw new Error("minDelay must be non-negative");
|
|
15
|
+
}
|
|
11
16
|
this.minDelay = minDelay;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
async waitForSlot(): Promise<void> {
|
|
15
20
|
const now = Date.now();
|
|
16
21
|
const elapsedTime = now - this.lastRequestTime;
|
|
17
|
-
|
|
22
|
+
|
|
18
23
|
if (elapsedTime < this.minDelay) {
|
|
19
24
|
const waitTime = this.minDelay - elapsedTime;
|
|
20
25
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
26
|
+
// Set to the time when we can make the next request
|
|
27
|
+
this.lastRequestTime = Date.now();
|
|
28
|
+
} else {
|
|
29
|
+
// No wait needed, update to current time
|
|
30
|
+
this.lastRequestTime = now;
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
this.lastRequestTime = Date.now();
|
|
24
32
|
}
|
|
25
33
|
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Text Validation Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Default words/patterns to skip during translation
|
|
6
|
+
const DEFAULT_SKIPLIST = ["@umituz"] as const;
|
|
7
|
+
const skiplist = [...DEFAULT_SKIPLIST];
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Validates if the text is suitable for translation
|
|
7
11
|
*/
|
|
@@ -16,14 +20,13 @@ export function isValidText(text: unknown): text is string {
|
|
|
16
20
|
* Checks if a word should be skipped (e.g., proper nouns, symbols)
|
|
17
21
|
*/
|
|
18
22
|
export function shouldSkipWord(text: string): boolean {
|
|
19
|
-
const skiplist = ["@umituz"];
|
|
20
23
|
return skiplist.some(word => text.includes(word));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Determines if a key needs translation
|
|
25
28
|
*/
|
|
26
|
-
export function needsTranslation(targetValue: unknown,
|
|
29
|
+
export function needsTranslation(targetValue: unknown, _sourceValue: string): boolean {
|
|
27
30
|
if (typeof targetValue !== "string") return true;
|
|
28
31
|
if (targetValue.length === 0) return true; // Empty string means untranslated
|
|
29
32
|
// Do NOT return true if target === source anymore, to avoid infinite translations for words that are identical in both languages
|
|
@@ -3,12 +3,19 @@ import { initReactI18next } from 'react-i18next';
|
|
|
3
3
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
4
4
|
import { initSEO } from '@umituz/web-seo';
|
|
5
5
|
|
|
6
|
+
export interface DetectionOptions {
|
|
7
|
+
order?: string[];
|
|
8
|
+
caches?: string[];
|
|
9
|
+
lookupLocalStorage?: string;
|
|
10
|
+
lookupSessionstorage?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface SetupI18nOptions {
|
|
7
|
-
resources: Record<string, { translation:
|
|
14
|
+
resources: Record<string, { translation: Record<string, unknown> }>;
|
|
8
15
|
defaultLng?: string;
|
|
9
16
|
fallbackLng?: string;
|
|
10
17
|
onInit?: (instance: typeof i18n) => void;
|
|
11
|
-
detection?:
|
|
18
|
+
detection?: DetectionOptions;
|
|
12
19
|
seo?: {
|
|
13
20
|
titleKey: string;
|
|
14
21
|
descriptionKey: string;
|
|
@@ -21,7 +28,7 @@ export interface SetupI18nOptions {
|
|
|
21
28
|
* Static i18n initialization to simplify main app code.
|
|
22
29
|
* @description All common configuration including SEO integration is hidden inside this package.
|
|
23
30
|
*/
|
|
24
|
-
export function setupI18n(options: SetupI18nOptions) {
|
|
31
|
+
export function setupI18n(options: SetupI18nOptions): typeof i18n {
|
|
25
32
|
const {
|
|
26
33
|
resources,
|
|
27
34
|
defaultLng = 'en-US',
|
|
@@ -54,9 +61,11 @@ export function setupI18n(options: SetupI18nOptions) {
|
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
if (onInit) onInit(i18n);
|
|
64
|
+
})
|
|
65
|
+
.catch((error) => {
|
|
66
|
+
console.error('Failed to initialize i18n:', error);
|
|
67
|
+
throw error;
|
|
57
68
|
});
|
|
58
69
|
|
|
59
70
|
return i18n;
|
|
60
71
|
}
|
|
61
|
-
|
|
62
|
-
export default i18n;
|
package/src/scripts/cli.ts
CHANGED
|
@@ -6,21 +6,28 @@
|
|
|
6
6
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { cliService } from "../infrastructure/services/cli.service.js";
|
|
9
|
+
import type { SyncOptions } from "../infrastructure/services/cli.service.js";
|
|
9
10
|
import chalk from "chalk";
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
|
|
10
17
|
|
|
11
18
|
const program = new Command();
|
|
12
19
|
|
|
13
20
|
program
|
|
14
21
|
.name("web-loc")
|
|
15
22
|
.description("Localization CLI tool for web applications")
|
|
16
|
-
.version(
|
|
23
|
+
.version(packageJson.version);
|
|
17
24
|
|
|
18
25
|
program
|
|
19
26
|
.command("sync")
|
|
20
27
|
.description("Synchronize missing keys from base language to other languages")
|
|
21
28
|
.option("-d, --locales-dir <dir>", "Directory containing locale files", "src/locales")
|
|
22
29
|
.option("-b, --base-lang <lang>", "Base language code", "en-US")
|
|
23
|
-
.action(async (options) => {
|
|
30
|
+
.action(async (options: SyncOptions) => {
|
|
24
31
|
try {
|
|
25
32
|
await cliService.sync(options);
|
|
26
33
|
} catch (error) {
|
|
@@ -35,7 +42,7 @@ program
|
|
|
35
42
|
.option("-d, --locales-dir <dir>", "Directory containing locale files", "src/locales")
|
|
36
43
|
.option("-b, --base-lang <lang>", "Base language code", "en-US")
|
|
37
44
|
.option("-f, --force", "Force re-translation of all strings", false)
|
|
38
|
-
.action(async (options) => {
|
|
45
|
+
.action(async (options: SyncOptions & { force?: boolean }) => {
|
|
39
46
|
try {
|
|
40
47
|
await cliService.translate(options);
|
|
41
48
|
} catch (error) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translation.entity.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/translation.entity.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,eAAe,EAAE,CAAC;CACnC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translation-service.interface.d.ts","sourceRoot":"","sources":["../../../src/domain/interfaces/translation-service.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,gCAAgC,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI,CAAC;IACnD,aAAa,IAAI,OAAO,CAAC;IACzB,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACrE,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E,eAAe,CACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,MAAM,EACtB,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,gBAAgB,EACxB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,GAC5D,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB"}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,sCAAsC,CAAC;AACrD,cAAc,mDAAmD,CAAC;AAClE,cAAc,oDAAoD,CAAC;AACnE,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,2BAA2B,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/constants/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,wBAAwB,wDAAwD,CAAC;AAE9F,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AACjD,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AACxC,eAAO,MAAM,qBAAqB,UAAU,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/cli.service.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,UAAU;IACf,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0D9C,SAAS,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAuD1D;AAED,eAAO,MAAM,UAAU,YAAmB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"google-translate.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/google-translate.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,0DAA0D,CAAC;AAalE,cAAM,sBAAuB,YAAW,mBAAmB;IACzD,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,WAAW,CAA4B;IAE/C,UAAU,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI;IASlD,aAAa,IAAI,OAAO;IAIxB,OAAO,CAAC,iBAAiB;IAQnB,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsDpE,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyDzE,eAAe,CACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,MAAM,EACtB,IAAI,SAAK,EACT,KAAK,GAAE,gBAMN,EACD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,EAC7D,KAAK,UAAQ,GACZ,OAAO,CAAC,IAAI,CAAC;YAiEF,gBAAgB;CAkF/B;AAED,eAAO,MAAM,sBAAsB,wBAA+B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file.util.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/utils/file.util.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoB7E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAajG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.util.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/utils/rate-limit.util.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,WAAW;IACtB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,SAAM;IAIpB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;CAWnC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"text-validator.util.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/utils/text-validator.util.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAKzD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAKnF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.setup.d.ts","sourceRoot":"","sources":["../../src/integrations/i18n.setup.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,SAAS,CAAC;AAK3B,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,IAAI,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,GAAG,CAAC,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,0BAoClD;AAED,eAAe,IAAI,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/scripts/cli.ts"],"names":[],"mappings":";AAEA;;GAEG"}
|