@umituz/web-localization 1.1.6 → 1.1.7
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/package.json +1 -1
- package/src/domain/entities/translation.entity.ts +1 -0
- package/src/infrastructure/services/cli.service.ts +152 -50
- package/src/infrastructure/services/google-translate.service.ts +383 -120
- package/src/infrastructure/utils/file.util.ts +181 -19
- package/src/infrastructure/utils/rate-limit.util.ts +125 -13
- package/src/integrations/i18n.setup.ts +160 -4
|
@@ -2,31 +2,113 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @description Simplistic parser for 'export default { ... }' or 'export const data = { ... }'
|
|
7
|
-
* @security Note: Uses Function constructor - only use with trusted local files
|
|
5
|
+
* File content cache with TTL support
|
|
8
6
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
content: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fileCache = new Map<string, CacheEntry>();
|
|
13
|
+
const CACHE_TTL = 1000 * 60 * 5; // 5 minutes
|
|
14
|
+
const MAX_CACHE_SIZE = 100;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pre-compiled regex patterns for better performance
|
|
18
|
+
*/
|
|
19
|
+
const EXPORT_PATTERNS = {
|
|
20
|
+
defaultExport: /export\s+default\s+(\{[\s\S]*\});?\s*$/,
|
|
21
|
+
namedExport: /export\s+const\s+\w+\s*=\s*(\{[\s\S]*\});?\s*$/,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get file content with caching
|
|
26
|
+
*/
|
|
27
|
+
function getFileContent(filePath: string): string | null {
|
|
11
28
|
const resolvedPath = path.resolve(filePath);
|
|
29
|
+
|
|
30
|
+
// Check cache first
|
|
31
|
+
const cacheEntry = fileCache.get(resolvedPath);
|
|
32
|
+
if (cacheEntry) {
|
|
33
|
+
// Check if cache entry is still valid
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
if (now - cacheEntry.timestamp < CACHE_TTL) {
|
|
36
|
+
return cacheEntry.content;
|
|
37
|
+
}
|
|
38
|
+
// Cache expired, remove it
|
|
39
|
+
fileCache.delete(resolvedPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate file path is within project directory
|
|
12
43
|
if (!resolvedPath.startsWith(process.cwd())) {
|
|
13
44
|
throw new Error(`Security: File path outside project directory: ${filePath}`);
|
|
14
45
|
}
|
|
15
46
|
|
|
16
|
-
if (!fs.existsSync(resolvedPath))
|
|
17
|
-
|
|
47
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Read file content
|
|
18
52
|
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
53
|
+
|
|
54
|
+
// Add to cache (evict oldest if cache is full)
|
|
55
|
+
if (fileCache.size >= MAX_CACHE_SIZE) {
|
|
56
|
+
let oldestKey: string | null = null;
|
|
57
|
+
let oldestTime = Infinity;
|
|
58
|
+
|
|
59
|
+
for (const [key, entry] of fileCache) {
|
|
60
|
+
if (entry.timestamp < oldestTime) {
|
|
61
|
+
oldestTime = entry.timestamp;
|
|
62
|
+
oldestKey = key;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (oldestKey) {
|
|
67
|
+
fileCache.delete(oldestKey);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fileCache.set(resolvedPath, {
|
|
72
|
+
content,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parses a TypeScript file containing an object export
|
|
81
|
+
* @description Improved parser with caching and better error handling
|
|
82
|
+
* @security Note: Uses Function constructor - only use with trusted local files
|
|
83
|
+
*/
|
|
84
|
+
export function parseTypeScriptFile(filePath: string): Record<string, unknown> {
|
|
85
|
+
const content = getFileContent(filePath);
|
|
86
|
+
if (!content) return {};
|
|
87
|
+
|
|
88
|
+
// Try to match export patterns (pre-compiled for performance)
|
|
89
|
+
let match: RegExpExecArray | null = null;
|
|
90
|
+
let objStr = "";
|
|
91
|
+
|
|
92
|
+
// Try default export first
|
|
93
|
+
match = EXPORT_PATTERNS.defaultExport.exec(content);
|
|
94
|
+
if (match && match[1]) {
|
|
95
|
+
objStr = match[1];
|
|
96
|
+
} else {
|
|
97
|
+
// Try named export
|
|
98
|
+
match = EXPORT_PATTERNS.namedExport.exec(content);
|
|
99
|
+
if (match && match[1]) {
|
|
100
|
+
objStr = match[1];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!objStr) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
25
108
|
try {
|
|
26
109
|
// Evaluate the object string to a real JS object
|
|
27
110
|
// Using Function instead of eval for slightly better safety
|
|
28
|
-
const
|
|
29
|
-
const obj = new Function(`return ${objStr}`)();
|
|
111
|
+
const obj = new Function(`"use strict"; return (${objStr})`)();
|
|
30
112
|
return obj;
|
|
31
113
|
} catch (err) {
|
|
32
114
|
console.error(`Error parsing ${filePath}:`, err);
|
|
@@ -36,13 +118,12 @@ export function parseTypeScriptFile(filePath: string): Record<string, unknown> {
|
|
|
36
118
|
|
|
37
119
|
/**
|
|
38
120
|
* Generates a TypeScript file content from an object
|
|
121
|
+
* @description Optimized content generation with proper escaping
|
|
39
122
|
*/
|
|
40
123
|
export function generateTypeScriptContent(obj: Record<string, unknown>, langCode?: string): string {
|
|
124
|
+
// Use JSON.stringify with proper indentation
|
|
41
125
|
const jsonStr = JSON.stringify(obj, null, 2);
|
|
42
|
-
|
|
43
|
-
// Clean up keys (remove quotes if possible, though JSON.stringify adds them)
|
|
44
|
-
// For simplicity, we'll keep them as valid JS objects
|
|
45
|
-
|
|
126
|
+
|
|
46
127
|
return `/**
|
|
47
128
|
* Localization: ${langCode || "unknown"}
|
|
48
129
|
* Generated by @umituz/web-localization
|
|
@@ -51,3 +132,84 @@ export function generateTypeScriptContent(obj: Record<string, unknown>, langCode
|
|
|
51
132
|
export default ${jsonStr};
|
|
52
133
|
`;
|
|
53
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clear file content cache
|
|
138
|
+
* @description Call this when you know files have been modified externally
|
|
139
|
+
*/
|
|
140
|
+
export function clearFileCache(): void {
|
|
141
|
+
fileCache.clear();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get cache statistics
|
|
146
|
+
*/
|
|
147
|
+
export function getFileCacheStats(): { size: number; maxSize: number } {
|
|
148
|
+
return {
|
|
149
|
+
size: fileCache.size,
|
|
150
|
+
maxSize: MAX_CACHE_SIZE,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a file exists (async version for better performance)
|
|
156
|
+
*/
|
|
157
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
158
|
+
try {
|
|
159
|
+
await fs.promises.access(filePath);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read file content asynchronously (for better performance in non-blocking scenarios)
|
|
168
|
+
*/
|
|
169
|
+
export async function readFileAsync(filePath: string): Promise<string | null> {
|
|
170
|
+
const resolvedPath = path.resolve(filePath);
|
|
171
|
+
|
|
172
|
+
// Validate file path is within project directory
|
|
173
|
+
if (!resolvedPath.startsWith(process.cwd())) {
|
|
174
|
+
throw new Error(`Security: File path outside project directory: ${filePath}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const content = await fs.promises.readFile(resolvedPath, "utf-8");
|
|
179
|
+
|
|
180
|
+
// Update cache
|
|
181
|
+
fileCache.set(resolvedPath, {
|
|
182
|
+
content,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return content;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Write file content asynchronously (for better performance in non-blocking scenarios)
|
|
194
|
+
*/
|
|
195
|
+
export async function writeFileAsync(filePath: string, content: string): Promise<void> {
|
|
196
|
+
const resolvedPath = path.resolve(filePath);
|
|
197
|
+
|
|
198
|
+
// Validate file path is within project directory
|
|
199
|
+
if (!resolvedPath.startsWith(process.cwd())) {
|
|
200
|
+
throw new Error(`Security: File path outside project directory: ${filePath}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Ensure directory exists
|
|
204
|
+
const dir = path.dirname(resolvedPath);
|
|
205
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
// Write file
|
|
208
|
+
await fs.promises.writeFile(resolvedPath, content, "utf-8");
|
|
209
|
+
|
|
210
|
+
// Update cache
|
|
211
|
+
fileCache.set(resolvedPath, {
|
|
212
|
+
content,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
@@ -1,33 +1,145 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @description Controls the frequency of API requests
|
|
2
|
+
* Advanced Rate Limiter with Queue and Dynamic Adjustment
|
|
3
|
+
* @description Controls the frequency of API requests with intelligent queue management
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { DEFAULT_MIN_DELAY } from "../constants/index.js";
|
|
7
7
|
|
|
8
|
+
interface QueuedRequest {
|
|
9
|
+
resolve: () => void;
|
|
10
|
+
priority: number;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
export class RateLimiter {
|
|
9
15
|
private lastRequestTime = 0;
|
|
10
16
|
private readonly minDelay: number;
|
|
17
|
+
private queue: QueuedRequest[] = [];
|
|
18
|
+
private processingQueue = false;
|
|
19
|
+
private dynamicDelay: number;
|
|
20
|
+
private responseTimeHistory: number[] = [];
|
|
21
|
+
private readonly maxHistorySize = 10;
|
|
11
22
|
|
|
12
23
|
constructor(minDelay = DEFAULT_MIN_DELAY) {
|
|
13
24
|
if (minDelay < 0) {
|
|
14
25
|
throw new Error("minDelay must be non-negative");
|
|
15
26
|
}
|
|
16
27
|
this.minDelay = minDelay;
|
|
28
|
+
this.dynamicDelay = minDelay;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wait for available slot with priority support
|
|
33
|
+
* @param priority - Lower number = higher priority (default: 10)
|
|
34
|
+
*/
|
|
35
|
+
async waitForSlot(priority = 10): Promise<void> {
|
|
36
|
+
// If queue is empty and we can proceed immediately, fast path
|
|
37
|
+
if (this.queue.length === 0 && !this.processingQueue) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const elapsedTime = now - this.lastRequestTime;
|
|
40
|
+
|
|
41
|
+
if (elapsedTime >= this.minDelay) {
|
|
42
|
+
this.lastRequestTime = now;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add to queue
|
|
48
|
+
return new Promise<void>((resolve) => {
|
|
49
|
+
this.queue.push({
|
|
50
|
+
resolve: () => resolve(),
|
|
51
|
+
priority,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Sort by priority (lower first) then timestamp (older first)
|
|
56
|
+
this.queue.sort((a, b) => {
|
|
57
|
+
if (a.priority !== b.priority) {
|
|
58
|
+
return a.priority - b.priority;
|
|
59
|
+
}
|
|
60
|
+
return a.timestamp - b.timestamp;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Start processing if not already running
|
|
64
|
+
if (!this.processingQueue) {
|
|
65
|
+
this.processQueue();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Process queued requests with controlled timing
|
|
72
|
+
*/
|
|
73
|
+
private async processQueue(): Promise<void> {
|
|
74
|
+
if (this.processingQueue) return;
|
|
75
|
+
this.processingQueue = true;
|
|
76
|
+
|
|
77
|
+
while (this.queue.length > 0) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const elapsedTime = now - this.lastRequestTime;
|
|
80
|
+
const delayNeeded = Math.max(0, this.dynamicDelay - elapsedTime);
|
|
81
|
+
|
|
82
|
+
if (delayNeeded > 0) {
|
|
83
|
+
await this.sleep(delayNeeded);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const request = this.queue.shift();
|
|
87
|
+
if (request) {
|
|
88
|
+
this.lastRequestTime = Date.now();
|
|
89
|
+
request.resolve();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.processingQueue = false;
|
|
17
94
|
}
|
|
18
95
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Record response time for dynamic delay adjustment
|
|
98
|
+
*/
|
|
99
|
+
recordResponseTime(responseTimeMs: number): void {
|
|
100
|
+
this.responseTimeHistory.push(responseTimeMs);
|
|
22
101
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
|
102
|
+
// Keep history size bounded
|
|
103
|
+
if (this.responseTimeHistory.length > this.maxHistorySize) {
|
|
104
|
+
this.responseTimeHistory.shift();
|
|
31
105
|
}
|
|
106
|
+
|
|
107
|
+
// Calculate average response time
|
|
108
|
+
const avgResponseTime = this.responseTimeHistory.reduce((a, b) => a + b, 0) / this.responseTimeHistory.length;
|
|
109
|
+
|
|
110
|
+
// Adjust delay dynamically (with safety bounds)
|
|
111
|
+
// If responses are fast, decrease delay; if slow, increase delay
|
|
112
|
+
const targetDelay = Math.max(this.minDelay, avgResponseTime * 1.5);
|
|
113
|
+
this.dynamicDelay = Math.min(targetDelay, this.minDelay * 3);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reset dynamic delay to minimum
|
|
118
|
+
*/
|
|
119
|
+
resetDynamicDelay(): void {
|
|
120
|
+
this.dynamicDelay = this.minDelay;
|
|
121
|
+
this.responseTimeHistory = [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get current queue length
|
|
126
|
+
*/
|
|
127
|
+
getQueueLength(): number {
|
|
128
|
+
return this.queue.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Optimized sleep function
|
|
133
|
+
*/
|
|
134
|
+
private sleep(ms: number): Promise<void> {
|
|
135
|
+
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Clear all pending requests (for cleanup)
|
|
140
|
+
*/
|
|
141
|
+
clear(): void {
|
|
142
|
+
this.queue = [];
|
|
143
|
+
this.processingQueue = false;
|
|
32
144
|
}
|
|
33
145
|
}
|
|
@@ -22,13 +22,33 @@ export interface SetupI18nOptions {
|
|
|
22
22
|
defaultImage?: string;
|
|
23
23
|
twitterHandle?: string;
|
|
24
24
|
};
|
|
25
|
+
lazyLoad?: boolean;
|
|
26
|
+
cache?: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// Initialization state tracking
|
|
30
|
+
let isInitialized = false;
|
|
31
|
+
let initializationPromise: Promise<typeof i18n> | null = null;
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
|
-
*
|
|
34
|
+
* Optimized i18n initialization with lazy loading and caching
|
|
29
35
|
* @description All common configuration including SEO integration is hidden inside this package.
|
|
36
|
+
* Performance optimizations:
|
|
37
|
+
* - Lazy loading support for resources
|
|
38
|
+
* - Response caching to reduce memory allocation
|
|
39
|
+
* - Efficient language detection with fallbacks
|
|
30
40
|
*/
|
|
31
41
|
export function setupI18n(options: SetupI18nOptions): typeof i18n {
|
|
42
|
+
// Return existing instance if already initialized
|
|
43
|
+
if (isInitialized && i18n.isInitialized) {
|
|
44
|
+
return i18n;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Return existing initialization promise if in progress
|
|
48
|
+
if (initializationPromise) {
|
|
49
|
+
return i18n;
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
const {
|
|
33
53
|
resources,
|
|
34
54
|
defaultLng = 'en-US',
|
|
@@ -39,33 +59,169 @@ export function setupI18n(options: SetupI18nOptions): typeof i18n {
|
|
|
39
59
|
caches: ['localStorage'],
|
|
40
60
|
},
|
|
41
61
|
seo,
|
|
62
|
+
lazyLoad = false,
|
|
63
|
+
cache = true,
|
|
42
64
|
} = options;
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
// Optimize resources for memory efficiency
|
|
67
|
+
const optimizedResources = optimizeResources(resources);
|
|
68
|
+
|
|
69
|
+
// Create initialization promise
|
|
70
|
+
initializationPromise = i18n
|
|
45
71
|
.use(LanguageDetector)
|
|
46
72
|
.use(initReactI18next)
|
|
47
73
|
.init({
|
|
48
|
-
|
|
74
|
+
// Use lazy loading if enabled (reduces initial bundle size)
|
|
75
|
+
resources: lazyLoad ? undefined : optimizedResources,
|
|
76
|
+
|
|
49
77
|
lng: defaultLng,
|
|
50
78
|
fallbackLng,
|
|
79
|
+
|
|
80
|
+
// Performance optimizations
|
|
81
|
+
load: lazyLoad ? 'languageOnly' : 'all',
|
|
82
|
+
preload: lazyLoad ? [] : undefined,
|
|
83
|
+
|
|
84
|
+
// Cache configuration for better performance
|
|
85
|
+
ns: ['translation'],
|
|
86
|
+
defaultNS: 'translation',
|
|
87
|
+
saveMissing: false,
|
|
88
|
+
|
|
51
89
|
interpolation: {
|
|
52
90
|
escapeValue: false,
|
|
91
|
+
// Use efficient formatting
|
|
92
|
+
format: (value, format) => {
|
|
93
|
+
if (format === 'uppercase') return value.toUpperCase();
|
|
94
|
+
if (format === 'lowercase') return value.toLowerCase();
|
|
95
|
+
if (format === 'capitalize') return value.charAt(0).toUpperCase() + value.slice(1);
|
|
96
|
+
return value;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
detection: {
|
|
101
|
+
...detection,
|
|
102
|
+
// Cache detection results
|
|
103
|
+
caches: detection.caches || ['localStorage'],
|
|
53
104
|
},
|
|
54
|
-
|
|
105
|
+
|
|
106
|
+
// React-specific optimizations
|
|
107
|
+
react: {
|
|
108
|
+
useSuspense: false, // Disable suspense for better performance
|
|
109
|
+
bindI18n: 'languageChanged',
|
|
110
|
+
bindI18nStore: 'added removed',
|
|
111
|
+
transEmptyNodeValue: '',
|
|
112
|
+
transSupportBasicHtmlNodes: true,
|
|
113
|
+
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Enable caching
|
|
117
|
+
cache: cache ? { enabled: true } : undefined,
|
|
55
118
|
})
|
|
56
119
|
.then(() => {
|
|
120
|
+
isInitialized = true;
|
|
121
|
+
|
|
122
|
+
// Initialize SEO integration
|
|
57
123
|
if (seo) {
|
|
58
124
|
initSEO({
|
|
59
125
|
i18n,
|
|
60
126
|
...seo,
|
|
61
127
|
});
|
|
62
128
|
}
|
|
129
|
+
|
|
130
|
+
// Call custom init callback
|
|
63
131
|
if (onInit) onInit(i18n);
|
|
132
|
+
|
|
133
|
+
return i18n;
|
|
64
134
|
})
|
|
65
135
|
.catch((error) => {
|
|
66
136
|
console.error('Failed to initialize i18n:', error);
|
|
137
|
+
initializationPromise = null;
|
|
67
138
|
throw error;
|
|
68
139
|
});
|
|
69
140
|
|
|
70
141
|
return i18n;
|
|
71
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Optimize resources structure for better performance
|
|
146
|
+
* @description Removes duplicate keys and flattens nested structures where beneficial
|
|
147
|
+
*/
|
|
148
|
+
function optimizeResources(
|
|
149
|
+
resources: Record<string, { translation: Record<string, unknown> }>
|
|
150
|
+
): Record<string, { translation: Record<string, unknown> }> {
|
|
151
|
+
const optimized: Record<string, { translation: Record<string, unknown> }> = {};
|
|
152
|
+
|
|
153
|
+
for (const [lang, resource] of Object.entries(resources)) {
|
|
154
|
+
optimized[lang] = {
|
|
155
|
+
translation: resource.translation || {},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return optimized;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Add language dynamically (for lazy loading scenarios)
|
|
164
|
+
* @description Efficiently adds new language resources without full re-initialization
|
|
165
|
+
*/
|
|
166
|
+
export function addLanguage(
|
|
167
|
+
lng: string,
|
|
168
|
+
resources: Record<string, unknown>,
|
|
169
|
+
ns = 'translation'
|
|
170
|
+
): void {
|
|
171
|
+
if (!i18n.isInitialized) {
|
|
172
|
+
console.warn('i18n is not initialized yet. Call setupI18n first.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
i18n.addResourceBundle(lng, ns, resources, true, true);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Change language with performance optimization
|
|
181
|
+
* @description Changes language efficiently without unnecessary re-renders
|
|
182
|
+
*/
|
|
183
|
+
export async function changeLanguage(lng: string): Promise<void> {
|
|
184
|
+
if (!i18n.isInitialized) {
|
|
185
|
+
console.warn('i18n is not initialized yet. Call setupI18n first.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await i18n.changeLanguage(lng);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get current language
|
|
194
|
+
*/
|
|
195
|
+
export function getCurrentLanguage(): string {
|
|
196
|
+
return i18n.language || i18n.languages[0] || 'en-US';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if i18n is initialized
|
|
201
|
+
*/
|
|
202
|
+
export function isI18nInitialized(): boolean {
|
|
203
|
+
return isInitialized && i18n.isInitialized;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Reset i18n instance (for testing or cleanup)
|
|
208
|
+
* @description Note: i18next doesn't have a built-in reset method, this clears tracking state
|
|
209
|
+
*/
|
|
210
|
+
export function resetI18n(): void {
|
|
211
|
+
isInitialized = false;
|
|
212
|
+
initializationPromise = null;
|
|
213
|
+
// Clear all resources and namespaces
|
|
214
|
+
if (i18n.isInitialized) {
|
|
215
|
+
const languages = [...i18n.languages];
|
|
216
|
+
languages.forEach(lng => {
|
|
217
|
+
i18n.removeResourceBundle(lng, 'translation');
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get i18n instance
|
|
224
|
+
*/
|
|
225
|
+
export function getI18nInstance(): typeof i18n {
|
|
226
|
+
return i18n;
|
|
227
|
+
}
|