@umituz/web-localization 1.1.0 → 1.1.4

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.
Files changed (37) hide show
  1. package/dist/domain/entities/translation.entity.d.ts +0 -1
  2. package/dist/domain/interfaces/translation-service.interface.d.ts +0 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/infrastructure/constants/index.d.ts +0 -1
  6. package/dist/infrastructure/services/cli.service.d.ts +1 -1
  7. package/dist/infrastructure/services/cli.service.js +1 -1
  8. package/dist/infrastructure/services/google-translate.service.d.ts +1 -2
  9. package/dist/infrastructure/services/google-translate.service.js +7 -3
  10. package/dist/infrastructure/utils/file.util.d.ts +0 -1
  11. package/dist/infrastructure/utils/rate-limit.util.d.ts +0 -1
  12. package/dist/infrastructure/utils/text-validator.util.d.ts +0 -1
  13. package/dist/integrations/i18n.setup.d.ts +22 -0
  14. package/dist/integrations/i18n.setup.js +38 -0
  15. package/dist/scripts/cli.d.ts +0 -1
  16. package/dist/scripts/cli.js +1 -0
  17. package/package.json +6 -5
  18. package/src/domain/interfaces/translation-service.interface.ts +2 -1
  19. package/src/index.ts +1 -0
  20. package/src/infrastructure/constants/index.ts +4 -0
  21. package/src/infrastructure/services/cli.service.ts +43 -34
  22. package/src/infrastructure/services/google-translate.service.ts +53 -33
  23. package/src/infrastructure/utils/file.util.ts +10 -2
  24. package/src/infrastructure/utils/rate-limit.util.ts +13 -5
  25. package/src/infrastructure/utils/text-validator.util.ts +5 -2
  26. package/src/integrations/i18n.setup.ts +71 -0
  27. package/src/scripts/cli.ts +11 -3
  28. package/dist/domain/entities/translation.entity.d.ts.map +0 -1
  29. package/dist/domain/interfaces/translation-service.interface.d.ts.map +0 -1
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/infrastructure/constants/index.d.ts.map +0 -1
  32. package/dist/infrastructure/services/cli.service.d.ts.map +0 -1
  33. package/dist/infrastructure/services/google-translate.service.d.ts.map +0 -1
  34. package/dist/infrastructure/utils/file.util.d.ts.map +0 -1
  35. package/dist/infrastructure/utils/rate-limit.util.d.ts.map +0 -1
  36. package/dist/infrastructure/utils/text-validator.util.d.ts.map +0 -1
  37. package/dist/scripts/cli.d.ts.map +0 -1
@@ -27,4 +27,3 @@ export interface TranslationStats {
27
27
  skippedCount: number;
28
28
  translatedKeys: TranslationItem[];
29
29
  }
30
- //# sourceMappingURL=translation.entity.d.ts.map
@@ -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
@@ -8,4 +8,4 @@ export * from "./infrastructure/services/google-translate.service";
8
8
  export * from "./infrastructure/constants";
9
9
  export * from "./infrastructure/utils/text-validator.util";
10
10
  export * from "./infrastructure/utils/rate-limit.util";
11
- //# sourceMappingURL=index.d.ts.map
11
+ export * from "./integrations/i18n.setup";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export * from "./infrastructure/services/google-translate.service";
8
8
  export * from "./infrastructure/constants";
9
9
  export * from "./infrastructure/utils/text-validator.util";
10
10
  export * from "./infrastructure/utils/rate-limit.util";
11
+ export * from "./integrations/i18n.setup";
@@ -8,4 +8,3 @@ 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
- //# sourceMappingURL=index.d.ts.map
@@ -2,10 +2,10 @@ export interface SyncOptions {
2
2
  localesDir?: string;
3
3
  sourceDir?: string;
4
4
  baseLang?: string;
5
+ force?: boolean;
5
6
  }
6
7
  export declare class CLIService {
7
8
  sync(options?: SyncOptions): Promise<void>;
8
9
  translate(options?: SyncOptions): Promise<void>;
9
10
  }
10
11
  export declare const cliService: CLIService;
11
- //# sourceMappingURL=cli.service.d.ts.map
@@ -81,7 +81,7 @@ export class CLIService {
81
81
  await googleTranslateService.translateObject(baseData, targetData, langCode.split("-")[0], // ISO 639-1
82
82
  "", stats, (key, from, to) => {
83
83
  process.stdout.write(chalk.gray(` • ${key}: ${from.substring(0, 15)}... → ${to.substring(0, 15)}...\r`));
84
- });
84
+ }, options.force);
85
85
  if (stats.successCount > 0) {
86
86
  fs.writeFileSync(targetPath, generateTypeScriptContent(targetData, langCode));
87
87
  console.log(chalk.green(` ✅ Successfully translated ${stats.successCount} keys.`));
@@ -12,9 +12,8 @@ declare class GoogleTranslateService implements ITranslationService {
12
12
  private ensureInitialized;
13
13
  translate(request: TranslationRequest): Promise<TranslationResponse>;
14
14
  translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
15
- 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>;
15
+ 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
16
  private callTranslateAPI;
17
17
  }
18
18
  export declare const googleTranslateService: GoogleTranslateService;
19
19
  export { GoogleTranslateService };
20
- //# sourceMappingURL=google-translate.service.d.ts.map
@@ -118,7 +118,7 @@ class GoogleTranslateService {
118
118
  failureCount: 0,
119
119
  skippedCount: 0,
120
120
  translatedKeys: [],
121
- }, onTranslate) {
121
+ }, onTranslate, force = false) {
122
122
  if (!sourceObject || typeof sourceObject !== "object")
123
123
  return;
124
124
  if (!targetObject || typeof targetObject !== "object")
@@ -135,11 +135,11 @@ class GoogleTranslateService {
135
135
  if (!targetObject[key] || typeof targetObject[key] !== "object") {
136
136
  targetObject[key] = {};
137
137
  }
138
- await this.translateObject(enValue, targetObject[key], targetLanguage, currentPath, stats, onTranslate);
138
+ await this.translateObject(enValue, targetObject[key], targetLanguage, currentPath, stats, onTranslate, force);
139
139
  }
140
140
  else if (typeof enValue === "string") {
141
141
  stats.totalCount++;
142
- if (needsTranslation(targetValue, enValue)) {
142
+ if (force || needsTranslation(targetValue, enValue)) {
143
143
  textsToTranslate.push({ key, enValue, currentPath });
144
144
  }
145
145
  else {
@@ -161,10 +161,14 @@ class GoogleTranslateService {
161
161
  const translatedItem = results.translatedKeys[resultIndex];
162
162
  if (translatedItem && translatedItem.from === enValue && translatedItem.to !== enValue) {
163
163
  targetObject[key] = translatedItem.to;
164
+ stats.successCount++;
164
165
  if (onTranslate)
165
166
  onTranslate(currentPath, enValue, translatedItem.to);
166
167
  resultIndex++;
167
168
  }
169
+ else {
170
+ stats.failureCount++;
171
+ }
168
172
  }
169
173
  }
170
174
  }
@@ -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
@@ -8,4 +8,3 @@ export declare class RateLimiter {
8
8
  constructor(minDelay?: number);
9
9
  waitForSlot(): Promise<void>;
10
10
  }
11
- //# sourceMappingURL=rate-limit.util.d.ts.map
@@ -13,4 +13,3 @@ export declare function shouldSkipWord(text: string): boolean;
13
13
  * Determines if a key needs translation
14
14
  */
15
15
  export declare function needsTranslation(targetValue: unknown, sourceValue: string): boolean;
16
- //# sourceMappingURL=text-validator.util.d.ts.map
@@ -0,0 +1,22 @@
1
+ import i18n from 'i18next';
2
+ export interface SetupI18nOptions {
3
+ resources: Record<string, {
4
+ translation: any;
5
+ }>;
6
+ defaultLng?: string;
7
+ fallbackLng?: string;
8
+ onInit?: (instance: typeof i18n) => void;
9
+ detection?: any;
10
+ seo?: {
11
+ titleKey: string;
12
+ descriptionKey: string;
13
+ defaultImage?: string;
14
+ twitterHandle?: string;
15
+ };
16
+ }
17
+ /**
18
+ * Static i18n initialization to simplify main app code.
19
+ * @description All common configuration including SEO integration is hidden inside this package.
20
+ */
21
+ export declare function setupI18n(options: SetupI18nOptions): import("i18next").i18n;
22
+ export default i18n;
@@ -0,0 +1,38 @@
1
+ import i18n from 'i18next';
2
+ import { initReactI18next } from 'react-i18next';
3
+ import LanguageDetector from 'i18next-browser-languagedetector';
4
+ import { initSEO } from '@umituz/web-seo';
5
+ /**
6
+ * Static i18n initialization to simplify main app code.
7
+ * @description All common configuration including SEO integration is hidden inside this package.
8
+ */
9
+ export function setupI18n(options) {
10
+ const { resources, defaultLng = 'en-US', fallbackLng = 'en-US', onInit, detection = {
11
+ order: ['localStorage', 'navigator'],
12
+ caches: ['localStorage'],
13
+ }, seo, } = options;
14
+ i18n
15
+ .use(LanguageDetector)
16
+ .use(initReactI18next)
17
+ .init({
18
+ resources,
19
+ lng: defaultLng,
20
+ fallbackLng,
21
+ interpolation: {
22
+ escapeValue: false,
23
+ },
24
+ detection,
25
+ })
26
+ .then(() => {
27
+ if (seo) {
28
+ initSEO({
29
+ i18n,
30
+ ...seo,
31
+ });
32
+ }
33
+ if (onInit)
34
+ onInit(i18n);
35
+ });
36
+ return i18n;
37
+ }
38
+ export default i18n;
@@ -3,4 +3,3 @@
3
3
  * CLI Tool for @umituz/web-localization
4
4
  */
5
5
  export {};
6
- //# sourceMappingURL=cli.d.ts.map
@@ -29,6 +29,7 @@ program
29
29
  .description("Automatically translate missing strings using Google Translate")
30
30
  .option("-d, --locales-dir <dir>", "Directory containing locale files", "src/locales")
31
31
  .option("-b, --base-lang <lang>", "Base language code", "en-US")
32
+ .option("-f, --force", "Force re-translation of all strings", false)
32
33
  .action(async (options) => {
33
34
  try {
34
35
  await cliService.translate(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-localization",
3
- "version": "1.1.0",
3
+ "version": "1.1.4",
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,7 @@
11
11
  },
12
12
  "exports": {
13
13
  ".": "./src/index.ts",
14
- "./services": "./src/infrastructure/services/index.ts",
15
- "./utils": "./src/infrastructure/utils/index.ts",
14
+ "./setup": "./src/integrations/i18n.setup.ts",
16
15
  "./package.json": "./package.json"
17
16
  },
18
17
  "scripts": {
@@ -37,8 +36,10 @@
37
36
  "dependencies": {
38
37
  "chalk": "^5.3.0",
39
38
  "commander": "^12.0.0",
40
- "dotenv": "^16.4.5",
41
- "ts-morph": "^27.0.2"
39
+ "i18next": "^23.11.2",
40
+ "react-i18next": "^14.1.1",
41
+ "i18next-browser-languagedetector": "^7.2.1",
42
+ "@umituz/web-seo": "file:../web-seo"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^20.12.7",
@@ -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
  }
package/src/index.ts CHANGED
@@ -9,3 +9,4 @@ export * from "./infrastructure/services/google-translate.service";
9
9
  export * from "./infrastructure/constants";
10
10
  export * from "./infrastructure/utils/text-validator.util";
11
11
  export * from "./infrastructure/utils/rate-limit.util";
12
+ export * from "./integrations/i18n.setup";
@@ -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,19 +1,47 @@
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 {
12
13
  localesDir?: string;
13
14
  sourceDir?: string;
14
15
  baseLang?: string;
16
+ force?: boolean;
15
17
  }
16
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
+
17
45
  export class CLIService {
18
46
  async sync(options: SyncOptions = {}): Promise<void> {
19
47
  const localesDir = path.resolve(process.cwd(), options.localesDir || DEFAULT_LOCALES_DIR);
@@ -32,7 +60,7 @@ export class CLIService {
32
60
 
33
61
  const baseData = parseTypeScriptFile(baseLangPath);
34
62
  const files = fs.readdirSync(localesDir)
35
- .filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== `${baseLang}.ts`)
63
+ .filter(f => f.match(/^[a-z]{2}(-[A-Z]{2})?\.ts$/) && f !== `${baseLang}.ts`)
36
64
  .sort();
37
65
 
38
66
  console.log(chalk.blue(`📊 Found ${files.length} languages to sync with ${baseLang}.\n`));
@@ -40,30 +68,7 @@ export class CLIService {
40
68
  for (const file of files) {
41
69
  const targetPath = path.join(localesDir, file);
42
70
  const targetData = parseTypeScriptFile(targetPath);
43
- const langCode = file.replace(".ts", "");
44
-
45
- // Deep merge with base data structure
46
- const syncObject = (source: Record<string, unknown>, target: Record<string, unknown>): Record<string, unknown> => {
47
- const result = { ...target };
48
- for (const key in source) {
49
- if (typeof source[key] === "object" && source[key] !== null) {
50
- result[key] = syncObject(
51
- source[key] as Record<string, unknown>,
52
- (target[key] as Record<string, unknown>) || {}
53
- );
54
- } else if (target[key] === undefined) {
55
- // Let empty string indicate untranslated state
56
- result[key] = typeof source[key] === "string" ? "" : source[key];
57
- }
58
- }
59
- // Remove extra keys
60
- for (const key in target) {
61
- if (source[key] === undefined) {
62
- delete result[key];
63
- }
64
- }
65
- return result;
66
- };
71
+ const langCode = path.basename(file, ".ts");
67
72
 
68
73
  const syncedData = syncObject(baseData, targetData);
69
74
  fs.writeFileSync(targetPath, generateTypeScriptContent(syncedData, langCode));
@@ -86,7 +91,7 @@ export class CLIService {
86
91
  googleTranslateService.initialize({});
87
92
  const baseData = parseTypeScriptFile(baseLangPath);
88
93
  const files = fs.readdirSync(localesDir)
89
- .filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== `${baseLang}.ts`)
94
+ .filter(f => f.match(/^[a-z]{2}(-[A-Z]{2})?\.ts$/) && f !== `${baseLang}.ts`)
90
95
  .sort();
91
96
 
92
97
  console.log(chalk.blue.bold(`🚀 Starting automatic translation for ${files.length} languages...\n`));
@@ -94,9 +99,9 @@ export class CLIService {
94
99
  for (const file of files) {
95
100
  const targetPath = path.join(localesDir, file);
96
101
  const targetData = parseTypeScriptFile(targetPath);
97
- const langCode = file.replace(".ts", "");
98
-
99
- const stats = {
102
+ const langCode = path.basename(file, ".ts");
103
+
104
+ const stats: TranslationStats = {
100
105
  totalCount: 0,
101
106
  successCount: 0,
102
107
  failureCount: 0,
@@ -106,15 +111,19 @@ export class CLIService {
106
111
 
107
112
  console.log(chalk.yellow(`🌍 Translating ${langCode}...`));
108
113
 
114
+ // Extract ISO 639-1 language code (e.g., "en" from "en-US")
115
+ const targetLang = langCode.includes("-") ? langCode.split("-")[0] : langCode;
116
+
109
117
  await googleTranslateService.translateObject(
110
118
  baseData,
111
119
  targetData,
112
- langCode.split("-")[0], // ISO 639-1
120
+ targetLang,
113
121
  "",
114
122
  stats,
115
123
  (key, from, to) => {
116
124
  process.stdout.write(chalk.gray(` • ${key}: ${from.substring(0, 15)}... → ${to.substring(0, 15)}...\r`));
117
- }
125
+ },
126
+ options.force
118
127
  );
119
128
 
120
129
  if (stats.successCount > 0) {
@@ -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 rateLimiter: RateLimiter | null = null;
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.rateLimiter = new RateLimiter(this.config.minDelay);
38
+ this._rateLimiter = new RateLimiter(this.config.minDelay);
38
39
  }
39
40
 
40
41
  isInitialized(): boolean {
41
- return this.config !== null && this.rateLimiter !== null;
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!.waitForSlot();
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.all(
138
+ const results = await Promise.allSettled(
131
139
  chunk.map(async (request) => {
132
- await this.rateLimiter!.waitForSlot();
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 translatedText = results[i];
144
-
145
- if (translatedText && translatedText !== request.text) {
146
- stats.successCount++;
147
- stats.translatedKeys.push({
148
- key: request.text,
149
- from: request.text,
150
- to: translatedText,
151
- });
152
- } else if (!translatedText) {
153
- stats.failureCount++;
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.skippedCount++;
166
+ stats.failureCount++;
156
167
  }
157
168
  }
158
169
  }
@@ -172,7 +183,8 @@ class GoogleTranslateService implements ITranslationService {
172
183
  skippedCount: 0,
173
184
  translatedKeys: [],
174
185
  },
175
- onTranslate?: (key: string, from: string, to: string) => void
186
+ onTranslate?: (key: string, from: string, to: string) => void,
187
+ force = false
176
188
  ): Promise<void> {
177
189
  if (!sourceObject || typeof sourceObject !== "object") return;
178
190
  if (!targetObject || typeof targetObject !== "object") return;
@@ -196,11 +208,12 @@ class GoogleTranslateService implements ITranslationService {
196
208
  targetLanguage,
197
209
  currentPath,
198
210
  stats,
199
- onTranslate
211
+ onTranslate,
212
+ force
200
213
  );
201
214
  } else if (typeof enValue === "string") {
202
215
  stats.totalCount++;
203
- if (needsTranslation(targetValue, enValue)) {
216
+ if (force || needsTranslation(targetValue, enValue)) {
204
217
  textsToTranslate.push({key, enValue, currentPath});
205
218
  } else {
206
219
  stats.skippedCount++;
@@ -219,15 +232,22 @@ class GoogleTranslateService implements ITranslationService {
219
232
  }))
220
233
  );
221
234
 
222
- let resultIndex = 0;
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
+
223
241
  for (let j = 0; j < batch.length; j++) {
224
242
  const {key, enValue, currentPath} = batch[j];
225
- const translatedItem = results.translatedKeys[resultIndex];
226
-
227
- if (translatedItem && translatedItem.from === enValue && translatedItem.to !== enValue) {
228
- targetObject[key] = translatedItem.to;
229
- if (onTranslate) onTranslate(currentPath, enValue, translatedItem.to);
230
- resultIndex++;
243
+ const translatedText = translationMap.get(enValue);
244
+
245
+ if (translatedText && translatedText !== enValue) {
246
+ targetObject[key] = translatedText;
247
+ stats.successCount++;
248
+ if (onTranslate) onTranslate(currentPath, enValue, translatedText);
249
+ } else {
250
+ stats.failureCount++;
231
251
  }
232
252
  }
233
253
  }
@@ -238,7 +258,7 @@ class GoogleTranslateService implements ITranslationService {
238
258
  text: string,
239
259
  targetLanguage: string,
240
260
  sourceLanguage: string,
241
- retries = 3,
261
+ retries = DEFAULT_MAX_RETRIES,
242
262
  backoffMs = 2000
243
263
  ): Promise<string> {
244
264
  // 1. Variable Protection (Extract {{variables}})
@@ -246,7 +266,7 @@ class GoogleTranslateService implements ITranslationService {
246
266
  let counter = 0;
247
267
 
248
268
  // Find all {{something}} patterns
249
- let safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
269
+ const safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
250
270
  const placeholder = `_VAR${counter}_`; // Using a simple token less likely to be split
251
271
  varMap.set(placeholder, match);
252
272
  counter++;
@@ -257,7 +277,7 @@ class GoogleTranslateService implements ITranslationService {
257
277
  const encodedText = encodeURIComponent(safeText);
258
278
  const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
259
279
 
260
- for (let attempt = 0; attempt <= retries; attempt++) {
280
+ for (let attempt = 0; attempt < retries; attempt++) {
261
281
  const controller = new AbortController();
262
282
  const timeoutId = setTimeout(() => controller.abort(), timeout);
263
283
 
@@ -268,7 +288,7 @@ class GoogleTranslateService implements ITranslationService {
268
288
 
269
289
  if (!response.ok) {
270
290
  if (response.status === 429 || response.status >= 500) {
271
- if (attempt < retries) {
291
+ if (attempt < retries - 1) {
272
292
  clearTimeout(timeoutId);
273
293
  // Exponential backoff
274
294
  const delay = backoffMs * Math.pow(2, attempt);
@@ -289,7 +309,7 @@ class GoogleTranslateService implements ITranslationService {
289
309
  data[0].length > 0 &&
290
310
  typeof data[0][0][0] === "string"
291
311
  ) {
292
- translatedStr = data[0].map((item: any) => item[0]).join('');
312
+ translatedStr = data[0].map((item: unknown[]) => item[0] as string).join('');
293
313
  }
294
314
 
295
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
- if (!fs.existsSync(filePath)) return {};
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(filePath, "utf-8");
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 = 100) {
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, sourceValue: string): boolean {
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
@@ -0,0 +1,71 @@
1
+ import i18n from 'i18next';
2
+ import { initReactI18next } from 'react-i18next';
3
+ import LanguageDetector from 'i18next-browser-languagedetector';
4
+ import { initSEO } from '@umituz/web-seo';
5
+
6
+ export interface DetectionOptions {
7
+ order?: string[];
8
+ caches?: string[];
9
+ lookupLocalStorage?: string;
10
+ lookupSessionstorage?: string;
11
+ }
12
+
13
+ export interface SetupI18nOptions {
14
+ resources: Record<string, { translation: Record<string, unknown> }>;
15
+ defaultLng?: string;
16
+ fallbackLng?: string;
17
+ onInit?: (instance: typeof i18n) => void;
18
+ detection?: DetectionOptions;
19
+ seo?: {
20
+ titleKey: string;
21
+ descriptionKey: string;
22
+ defaultImage?: string;
23
+ twitterHandle?: string;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Static i18n initialization to simplify main app code.
29
+ * @description All common configuration including SEO integration is hidden inside this package.
30
+ */
31
+ export function setupI18n(options: SetupI18nOptions): typeof i18n {
32
+ const {
33
+ resources,
34
+ defaultLng = 'en-US',
35
+ fallbackLng = 'en-US',
36
+ onInit,
37
+ detection = {
38
+ order: ['localStorage', 'navigator'],
39
+ caches: ['localStorage'],
40
+ },
41
+ seo,
42
+ } = options;
43
+
44
+ i18n
45
+ .use(LanguageDetector)
46
+ .use(initReactI18next)
47
+ .init({
48
+ resources,
49
+ lng: defaultLng,
50
+ fallbackLng,
51
+ interpolation: {
52
+ escapeValue: false,
53
+ },
54
+ detection,
55
+ })
56
+ .then(() => {
57
+ if (seo) {
58
+ initSEO({
59
+ i18n,
60
+ ...seo,
61
+ });
62
+ }
63
+ if (onInit) onInit(i18n);
64
+ })
65
+ .catch((error) => {
66
+ console.error('Failed to initialize i18n:', error);
67
+ throw error;
68
+ });
69
+
70
+ return i18n;
71
+ }
@@ -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("1.0.0");
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) {
@@ -34,7 +41,8 @@ program
34
41
  .description("Automatically translate missing strings using Google Translate")
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
- .action(async (options) => {
44
+ .option("-f, --force", "Force re-translation of all strings", false)
45
+ .action(async (options: SyncOptions & { force?: boolean }) => {
38
46
  try {
39
47
  await cliService.translate(options);
40
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"}
@@ -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"}
@@ -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;CACnB;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;CAsD1D;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,GAC5D,OAAO,CAAC,IAAI,CAAC;YA6DF,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":"cli.d.ts","sourceRoot":"","sources":["../../src/scripts/cli.ts"],"names":[],"mappings":";AAEA;;GAEG"}