@umituz/web-localization 1.1.5 → 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.
@@ -2,31 +2,113 @@ import fs from "fs";
2
2
  import path from "path";
3
3
 
4
4
  /**
5
- * Parses a TypeScript file containing an object export
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
- export function parseTypeScriptFile(filePath: string): Record<string, unknown> {
10
- // Validate file path is within project directory
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)) return {};
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
- // Extract the object part
21
- // This is a naive implementation, but matches the pattern used in the project
22
- const match = content.match(/export (default|const [^=]+ =) (\{[\s\S]*\});?\s*$/);
23
- if (!match) return {};
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 objStr = match[2];
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
- * Simple Rate Limiter
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
- import { RATE_LIMIT_DEFAULT_DELAY } from "../constants/index.js";
6
+ import { DEFAULT_MIN_DELAY } from "../constants/index.js";
7
+
8
+ interface QueuedRequest {
9
+ resolve: () => void;
10
+ priority: number;
11
+ timestamp: number;
12
+ }
7
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
- constructor(minDelay = RATE_LIMIT_DEFAULT_DELAY) {
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
- async waitForSlot(): Promise<void> {
20
- const now = Date.now();
21
- const elapsedTime = now - this.lastRequestTime;
96
+ /**
97
+ * Record response time for dynamic delay adjustment
98
+ */
99
+ recordResponseTime(responseTimeMs: number): void {
100
+ this.responseTimeHistory.push(responseTimeMs);
22
101
 
23
- if (elapsedTime < this.minDelay) {
24
- const waitTime = this.minDelay - elapsedTime;
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;
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
  }
@@ -4,7 +4,6 @@
4
4
 
5
5
  // Default words/patterns to skip during translation
6
6
  const DEFAULT_SKIPLIST = ["@umituz"] as const;
7
- const skiplist = [...DEFAULT_SKIPLIST];
8
7
 
9
8
  /**
10
9
  * Validates if the text is suitable for translation
@@ -20,15 +19,14 @@ export function isValidText(text: unknown): text is string {
20
19
  * Checks if a word should be skipped (e.g., proper nouns, symbols)
21
20
  */
22
21
  export function shouldSkipWord(text: string): boolean {
23
- return skiplist.some(word => text.includes(word));
22
+ return DEFAULT_SKIPLIST.some(word => text.includes(word));
24
23
  }
25
24
 
26
25
  /**
27
26
  * Determines if a key needs translation
28
27
  */
29
- export function needsTranslation(targetValue: unknown, _sourceValue: string): boolean {
28
+ export function needsTranslation(targetValue: unknown): boolean {
30
29
  if (typeof targetValue !== "string") return true;
31
30
  if (targetValue.length === 0) return true; // Empty string means untranslated
32
- // Do NOT return true if target === source anymore, to avoid infinite translations for words that are identical in both languages
33
31
  return false;
34
32
  }
@@ -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
- * Static i18n initialization to simplify main app code.
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
- i18n
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
- resources,
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
- detection,
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
+ }