@hua-labs/i18n-core 2.0.0 → 2.0.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 (78) hide show
  1. package/README.md +57 -597
  2. package/dist/chunk-F4PDBJLO.mjs +973 -0
  3. package/dist/chunk-F4PDBJLO.mjs.map +1 -0
  4. package/dist/index.d.mts +249 -0
  5. package/dist/index.d.ts +117 -30
  6. package/dist/index.js +1818 -177
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +845 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/dist/server-4TeBq6hp.d.mts +367 -0
  11. package/dist/server-4TeBq6hp.d.ts +367 -0
  12. package/dist/server.d.mts +1 -0
  13. package/dist/server.d.ts +1 -0
  14. package/dist/server.js +977 -0
  15. package/dist/server.js.map +1 -0
  16. package/dist/server.mjs +3 -0
  17. package/dist/server.mjs.map +1 -0
  18. package/package.json +42 -19
  19. package/src/__tests__/debug-tools.test.ts +359 -0
  20. package/src/__tests__/default-translations.test.ts +179 -0
  21. package/src/__tests__/i18n-resource.test.ts +137 -0
  22. package/src/__tests__/lazy-loader.test.ts +109 -0
  23. package/src/__tests__/missing-key-overlay.test.tsx +339 -0
  24. package/src/__tests__/translator-factory.test.ts +120 -0
  25. package/src/__tests__/translator.test.ts +442 -0
  26. package/src/__tests__/types.test.ts +211 -0
  27. package/src/__tests__/useI18n.test.tsx +181 -0
  28. package/src/__tests__/useTranslation.test.tsx +110 -0
  29. package/src/components/MissingKeyOverlay.tsx +1 -1
  30. package/src/core/lazy-loader.ts +2 -2
  31. package/src/core/translator.tsx +151 -62
  32. package/src/hooks/useI18n.tsx +96 -115
  33. package/src/hooks/useTranslation.tsx +12 -10
  34. package/src/index.ts +102 -5
  35. package/src/server.ts +9 -0
  36. package/src/types/index.ts +67 -12
  37. package/LICENSE +0 -21
  38. package/dist/components/MissingKeyOverlay.d.ts +0 -33
  39. package/dist/components/MissingKeyOverlay.d.ts.map +0 -1
  40. package/dist/components/MissingKeyOverlay.js +0 -138
  41. package/dist/components/MissingKeyOverlay.js.map +0 -1
  42. package/dist/core/debug-tools.d.ts +0 -37
  43. package/dist/core/debug-tools.d.ts.map +0 -1
  44. package/dist/core/debug-tools.js +0 -241
  45. package/dist/core/debug-tools.js.map +0 -1
  46. package/dist/core/i18n-resource.d.ts +0 -59
  47. package/dist/core/i18n-resource.d.ts.map +0 -1
  48. package/dist/core/i18n-resource.js +0 -153
  49. package/dist/core/i18n-resource.js.map +0 -1
  50. package/dist/core/lazy-loader.d.ts +0 -82
  51. package/dist/core/lazy-loader.d.ts.map +0 -1
  52. package/dist/core/lazy-loader.js +0 -193
  53. package/dist/core/lazy-loader.js.map +0 -1
  54. package/dist/core/translator-factory.d.ts +0 -50
  55. package/dist/core/translator-factory.d.ts.map +0 -1
  56. package/dist/core/translator-factory.js +0 -117
  57. package/dist/core/translator-factory.js.map +0 -1
  58. package/dist/core/translator.d.ts +0 -202
  59. package/dist/core/translator.d.ts.map +0 -1
  60. package/dist/core/translator.js +0 -912
  61. package/dist/core/translator.js.map +0 -1
  62. package/dist/hooks/useI18n.d.ts +0 -39
  63. package/dist/hooks/useI18n.d.ts.map +0 -1
  64. package/dist/hooks/useI18n.js +0 -531
  65. package/dist/hooks/useI18n.js.map +0 -1
  66. package/dist/hooks/useTranslation.d.ts +0 -55
  67. package/dist/hooks/useTranslation.d.ts.map +0 -1
  68. package/dist/hooks/useTranslation.js +0 -58
  69. package/dist/hooks/useTranslation.js.map +0 -1
  70. package/dist/index.d.ts.map +0 -1
  71. package/dist/types/index.d.ts +0 -162
  72. package/dist/types/index.d.ts.map +0 -1
  73. package/dist/types/index.js +0 -191
  74. package/dist/types/index.js.map +0 -1
  75. package/dist/utils/default-translations.d.ts +0 -20
  76. package/dist/utils/default-translations.d.ts.map +0 -1
  77. package/dist/utils/default-translations.js +0 -123
  78. package/dist/utils/default-translations.js.map +0 -1
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { LazyLoader } from '../core/lazy-loader';
3
+ import { I18nResourceManager } from '../core/i18n-resource';
4
+
5
+ describe('LazyLoader', () => {
6
+ let loader: LazyLoader;
7
+
8
+ beforeEach(() => {
9
+ loader = LazyLoader.getInstance();
10
+ // Reset the resource manager cache
11
+ I18nResourceManager.getInstance().invalidateCache();
12
+ loader.invalidateCache();
13
+ });
14
+
15
+ describe('getInstance', () => {
16
+ it('should return singleton', () => {
17
+ expect(LazyLoader.getInstance()).toBe(LazyLoader.getInstance());
18
+ });
19
+ });
20
+
21
+ describe('loadOnDemand', () => {
22
+ it('should load and return translations', async () => {
23
+ const mockLoader = vi.fn().mockResolvedValue({ key: 'value' });
24
+ const result = await loader.loadOnDemand('ko', 'common', mockLoader);
25
+ expect(result).toEqual({ key: 'value' });
26
+ });
27
+
28
+ it('should deduplicate concurrent loads', async () => {
29
+ const mockLoader = vi.fn().mockResolvedValue({ key: 'value' });
30
+ const [r1, r2] = await Promise.all([
31
+ loader.loadOnDemand('ko', 'dedup', mockLoader),
32
+ loader.loadOnDemand('ko', 'dedup', mockLoader),
33
+ ]);
34
+ expect(r1).toEqual({ key: 'value' });
35
+ expect(r2).toEqual({ key: 'value' });
36
+ // Should only call loader once (via resource manager dedup)
37
+ expect(mockLoader).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it('should return cached data on subsequent calls', async () => {
41
+ const mockLoader = vi.fn().mockResolvedValue({ cached: true });
42
+ await loader.loadOnDemand('ko', 'cached', mockLoader);
43
+ const result = await loader.loadOnDemand('ko', 'cached', mockLoader);
44
+ expect(result).toEqual({ cached: true });
45
+ expect(mockLoader).toHaveBeenCalledTimes(1);
46
+ });
47
+ });
48
+
49
+ describe('preloadNamespace', () => {
50
+ it('should preload a namespace', async () => {
51
+ const mockLoader = vi.fn().mockResolvedValue({ preloaded: true });
52
+ await loader.preloadNamespace('ko', 'preload', mockLoader);
53
+ // Should be available immediately
54
+ const result = await loader.loadOnDemand('ko', 'preload', mockLoader);
55
+ expect(result).toEqual({ preloaded: true });
56
+ });
57
+
58
+ it('should skip already preloaded namespaces', async () => {
59
+ const mockLoader = vi.fn().mockResolvedValue({ data: true });
60
+ await loader.preloadNamespace('ko', 'skip', mockLoader);
61
+ await loader.preloadNamespace('ko', 'skip', mockLoader);
62
+ // Loader should be called only once
63
+ expect(mockLoader).toHaveBeenCalledTimes(1);
64
+ });
65
+
66
+ it('should silently fail on error', async () => {
67
+ const mockLoader = vi.fn().mockRejectedValue(new Error('fail'));
68
+ await expect(loader.preloadNamespace('ko', 'error', mockLoader)).resolves.toBeUndefined();
69
+ });
70
+ });
71
+
72
+ describe('preloadMultipleNamespaces', () => {
73
+ it('should preload multiple namespaces concurrently', async () => {
74
+ const mockLoader = vi.fn().mockResolvedValue({ multi: true });
75
+ await loader.preloadMultipleNamespaces('ko', ['ns1', 'ns2', 'ns3'], mockLoader);
76
+ expect(mockLoader).toHaveBeenCalledTimes(3);
77
+ });
78
+ });
79
+
80
+ describe('getLoadStats', () => {
81
+ it('should return loading statistics', async () => {
82
+ const mockLoader = vi.fn().mockResolvedValue({ stat: true });
83
+ await loader.loadOnDemand('ko', 'stats', mockLoader);
84
+ const stats = loader.getLoadStats();
85
+ expect(stats.loadHistorySize).toBeGreaterThanOrEqual(1);
86
+ });
87
+ });
88
+
89
+ describe('invalidateCache', () => {
90
+ it('should invalidate specific namespace', async () => {
91
+ const mockLoader = vi.fn().mockResolvedValue({ data: 'v1' });
92
+ await loader.loadOnDemand('ko', 'inv', mockLoader);
93
+ loader.invalidateCache('ko', 'inv');
94
+ // After invalidation, should call loader again
95
+ mockLoader.mockResolvedValue({ data: 'v2' });
96
+ const result = await loader.loadOnDemand('ko', 'inv', mockLoader);
97
+ expect(result).toEqual({ data: 'v2' });
98
+ });
99
+
100
+ it('should invalidate all cache when no args', async () => {
101
+ const mockLoader = vi.fn().mockResolvedValue({ data: true });
102
+ await loader.loadOnDemand('ko', 'all1', mockLoader);
103
+ await loader.loadOnDemand('ko', 'all2', mockLoader);
104
+ loader.invalidateCache();
105
+ const stats = loader.getLoadStats();
106
+ expect(stats.preloadedCount).toBe(0);
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,339 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { MissingKeyOverlay, reportMissingKey, useMissingKeyOverlay } from '../components/MissingKeyOverlay';
5
+
6
+ describe('MissingKeyOverlay', () => {
7
+ const originalEnv = process.env.NODE_ENV;
8
+
9
+ beforeEach(() => {
10
+ process.env.NODE_ENV = 'development';
11
+ });
12
+
13
+ afterEach(() => {
14
+ process.env.NODE_ENV = originalEnv;
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('MissingKeyOverlay component', () => {
19
+ it('should not render when disabled', () => {
20
+ const { container } = render(<MissingKeyOverlay enabled={false} />);
21
+ expect(container.firstChild).toBeNull();
22
+ });
23
+
24
+ it('should not render in production by default', () => {
25
+ process.env.NODE_ENV = 'production';
26
+ const { container } = render(<MissingKeyOverlay />);
27
+ expect(container.firstChild).toBeNull();
28
+ });
29
+
30
+ it('should render when enabled and has missing keys', async () => {
31
+ render(<MissingKeyOverlay enabled={true} />);
32
+
33
+ reportMissingKey('test.key', { language: 'ko' });
34
+
35
+ await waitFor(() => {
36
+ expect(screen.getByText(/Missing Translation Keys/i)).toBeInTheDocument();
37
+ });
38
+ });
39
+
40
+ it('should display missing key details', async () => {
41
+ render(<MissingKeyOverlay enabled={true} />);
42
+
43
+ reportMissingKey('user.greeting', {
44
+ language: 'ko',
45
+ namespace: 'common',
46
+ component: 'Header',
47
+ });
48
+
49
+ await waitFor(() => {
50
+ expect(screen.getByText('user.greeting')).toBeInTheDocument();
51
+ expect(screen.getByText(/Lang: ko/)).toBeInTheDocument();
52
+ expect(screen.getByText(/NS: common/)).toBeInTheDocument();
53
+ expect(screen.getByText(/Component: Header/)).toBeInTheDocument();
54
+ });
55
+ });
56
+
57
+ it('should show count of missing keys', async () => {
58
+ render(<MissingKeyOverlay enabled={true} />);
59
+
60
+ reportMissingKey('key1', { language: 'ko' });
61
+ reportMissingKey('key2', { language: 'ko' });
62
+ reportMissingKey('key3', { language: 'en' });
63
+
64
+ await waitFor(() => {
65
+ expect(screen.getByText(/\(3\)/)).toBeInTheDocument();
66
+ });
67
+ });
68
+
69
+ it('should close when close button clicked', async () => {
70
+ const { container } = render(<MissingKeyOverlay enabled={true} />);
71
+
72
+ reportMissingKey('test.key', { language: 'ko' });
73
+
74
+ await waitFor(() => {
75
+ expect(screen.getByText(/Missing Translation Keys/i)).toBeInTheDocument();
76
+ });
77
+
78
+ const closeButton = screen.getByText('×');
79
+ fireEvent.click(closeButton);
80
+
81
+ await waitFor(() => {
82
+ expect(container.firstChild).toBeNull();
83
+ });
84
+ });
85
+
86
+ it('should clear keys when clear button clicked', async () => {
87
+ render(<MissingKeyOverlay enabled={true} />);
88
+
89
+ reportMissingKey('key1', { language: 'ko' });
90
+ reportMissingKey('key2', { language: 'ko' });
91
+
92
+ await waitFor(() => {
93
+ expect(screen.getByText(/\(2\)/)).toBeInTheDocument();
94
+ });
95
+
96
+ const clearButton = screen.getByText('Clear');
97
+ fireEvent.click(clearButton);
98
+
99
+ await waitFor(() => {
100
+ expect(screen.getByText(/\(0\)/)).toBeInTheDocument();
101
+ });
102
+ });
103
+
104
+ it('should show only last 10 keys', async () => {
105
+ render(<MissingKeyOverlay enabled={true} />);
106
+
107
+ for (let i = 0; i < 15; i++) {
108
+ reportMissingKey(`key${i}`, { language: 'ko' });
109
+ }
110
+
111
+ await waitFor(() => {
112
+ expect(screen.getByText(/... and 5 more/)).toBeInTheDocument();
113
+ });
114
+ });
115
+
116
+ it('should display timestamp for each key', async () => {
117
+ render(<MissingKeyOverlay enabled={true} />);
118
+
119
+ reportMissingKey('test.key', { language: 'ko' });
120
+
121
+ await waitFor(() => {
122
+ expect(screen.getByText(/Time:/)).toBeInTheDocument();
123
+ });
124
+ });
125
+
126
+ it('should respect position prop', () => {
127
+ const { container } = render(
128
+ <MissingKeyOverlay enabled={true} position="bottom-left" />
129
+ );
130
+
131
+ reportMissingKey('test.key', { language: 'ko' });
132
+
133
+ waitFor(() => {
134
+ const overlay = container.firstChild as HTMLElement;
135
+ expect(overlay?.style.bottom).toBe('20px');
136
+ expect(overlay?.style.left).toBe('20px');
137
+ });
138
+ });
139
+
140
+ it('should apply custom styles', async () => {
141
+ const customStyle = { backgroundColor: 'blue', zIndex: 5000 };
142
+ const { container } = render(
143
+ <MissingKeyOverlay enabled={true} style={customStyle} />
144
+ );
145
+
146
+ reportMissingKey('test.key', { language: 'ko' });
147
+
148
+ await waitFor(() => {
149
+ const overlay = container.firstChild as HTMLElement;
150
+ expect(overlay?.style.backgroundColor).toBeTruthy();
151
+ expect(overlay?.style.zIndex).toBeTruthy();
152
+ });
153
+ });
154
+
155
+ it('should handle keys without namespace', async () => {
156
+ render(<MissingKeyOverlay enabled={true} />);
157
+
158
+ reportMissingKey('simple.key', { language: 'en' });
159
+
160
+ await waitFor(() => {
161
+ expect(screen.getByText('simple.key')).toBeInTheDocument();
162
+ expect(screen.getByText(/Lang: en/)).toBeInTheDocument();
163
+ });
164
+ });
165
+
166
+ it('should handle keys without component', async () => {
167
+ render(<MissingKeyOverlay enabled={true} />);
168
+
169
+ reportMissingKey('test.key', {
170
+ language: 'ko',
171
+ namespace: 'common',
172
+ });
173
+
174
+ await waitFor(() => {
175
+ expect(screen.getByText('test.key')).toBeInTheDocument();
176
+ expect(screen.queryByText(/Component:/)).not.toBeInTheDocument();
177
+ });
178
+ });
179
+ });
180
+
181
+ describe('reportMissingKey', () => {
182
+ it('should dispatch custom event with key details', () => {
183
+ const eventSpy = vi.spyOn(window, 'dispatchEvent');
184
+
185
+ reportMissingKey('test.key', {
186
+ language: 'ko',
187
+ namespace: 'common',
188
+ component: 'TestComponent',
189
+ });
190
+
191
+ expect(eventSpy).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ type: 'i18n:missing-key',
194
+ detail: expect.objectContaining({
195
+ key: 'test.key',
196
+ language: 'ko',
197
+ namespace: 'common',
198
+ component: 'TestComponent',
199
+ timestamp: expect.any(Number),
200
+ }),
201
+ })
202
+ );
203
+
204
+ eventSpy.mockRestore();
205
+ });
206
+
207
+ it('should log warning to console in development', () => {
208
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
209
+
210
+ reportMissingKey('test.key', { language: 'ko' });
211
+
212
+ expect(warnSpy).toHaveBeenCalledWith(
213
+ expect.stringContaining('Missing translation key: test.key'),
214
+ expect.objectContaining({
215
+ language: 'ko',
216
+ })
217
+ );
218
+
219
+ warnSpy.mockRestore();
220
+ });
221
+
222
+ it('should not dispatch event in production', () => {
223
+ process.env.NODE_ENV = 'production';
224
+ const eventSpy = vi.spyOn(window, 'dispatchEvent');
225
+
226
+ reportMissingKey('test.key', { language: 'ko' });
227
+
228
+ expect(eventSpy).not.toHaveBeenCalled();
229
+ eventSpy.mockRestore();
230
+ });
231
+
232
+ it('should handle missing namespace', () => {
233
+ const eventSpy = vi.spyOn(window, 'dispatchEvent');
234
+
235
+ reportMissingKey('test.key', { language: 'ko' });
236
+
237
+ expect(eventSpy).toHaveBeenCalledWith(
238
+ expect.objectContaining({
239
+ detail: expect.objectContaining({
240
+ key: 'test.key',
241
+ namespace: undefined,
242
+ }),
243
+ })
244
+ );
245
+
246
+ eventSpy.mockRestore();
247
+ });
248
+
249
+ it('should handle missing component', () => {
250
+ const eventSpy = vi.spyOn(window, 'dispatchEvent');
251
+
252
+ reportMissingKey('test.key', {
253
+ language: 'en',
254
+ namespace: 'auth',
255
+ });
256
+
257
+ expect(eventSpy).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ detail: expect.objectContaining({
260
+ key: 'test.key',
261
+ component: undefined,
262
+ }),
263
+ })
264
+ );
265
+
266
+ eventSpy.mockRestore();
267
+ });
268
+ });
269
+
270
+ describe('useMissingKeyOverlay', () => {
271
+ function TestComponent({ enabled }: { enabled?: boolean }) {
272
+ const { showOverlay, setShowOverlay, reportMissingKey: report } = useMissingKeyOverlay(enabled);
273
+
274
+ return (
275
+ <div>
276
+ <div data-testid="show-overlay">{showOverlay ? 'true' : 'false'}</div>
277
+ <button onClick={() => setShowOverlay(false)}>Hide</button>
278
+ <button onClick={() => report('test.key', { language: 'ko' })}>Report</button>
279
+ </div>
280
+ );
281
+ }
282
+
283
+ it('should return showOverlay true by default in development', () => {
284
+ render(<TestComponent />);
285
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('true');
286
+ });
287
+
288
+ it('should return showOverlay false when disabled', () => {
289
+ render(<TestComponent enabled={false} />);
290
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('false');
291
+ });
292
+
293
+ it('should return setShowOverlay function', () => {
294
+ render(<TestComponent />);
295
+ const hideButton = screen.getByText('Hide');
296
+
297
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('true');
298
+
299
+ fireEvent.click(hideButton);
300
+
301
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('false');
302
+ });
303
+
304
+ it('should return reportMissingKey function', () => {
305
+ const eventSpy = vi.spyOn(window, 'dispatchEvent');
306
+ render(<TestComponent />);
307
+
308
+ const reportButton = screen.getByText('Report');
309
+ fireEvent.click(reportButton);
310
+
311
+ expect(eventSpy).toHaveBeenCalledWith(
312
+ expect.objectContaining({
313
+ type: 'i18n:missing-key',
314
+ })
315
+ );
316
+
317
+ eventSpy.mockRestore();
318
+ });
319
+
320
+ it('should respect enabled prop on mount', async () => {
321
+ // When enabled changes from true to false via rerender, the hook's useEffect
322
+ // only sets showOverlay to true, it doesn't set it to false
323
+ // So changing the prop won't change the state after initial render
324
+ const { unmount } = render(<TestComponent enabled={true} />);
325
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('true');
326
+ unmount();
327
+
328
+ // Re-mount with enabled=false should show false
329
+ render(<TestComponent enabled={false} />);
330
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('false');
331
+ });
332
+
333
+ it('should respect initial enabled=false', () => {
334
+ // When initially rendered with enabled=false, showOverlay should be false
335
+ render(<TestComponent enabled={false} />);
336
+ expect(screen.getByTestId('show-overlay')).toHaveTextContent('false');
337
+ });
338
+ });
339
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { TranslatorFactory } from '../core/translator-factory';
3
+ import { I18nConfig } from '../types';
4
+
5
+ function createMockConfig(overrides?: Partial<I18nConfig>): I18nConfig {
6
+ return {
7
+ defaultLanguage: 'ko',
8
+ fallbackLanguage: 'en',
9
+ supportedLanguages: [
10
+ { code: 'ko', name: 'Korean', nativeName: '한국어' },
11
+ { code: 'en', name: 'English', nativeName: 'English' },
12
+ ],
13
+ namespaces: ['common'],
14
+ loadTranslations: vi.fn().mockResolvedValue({}),
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ describe('TranslatorFactory', () => {
20
+ beforeEach(() => {
21
+ TranslatorFactory.clear();
22
+ });
23
+
24
+ describe('create', () => {
25
+ it('should create a new translator instance', () => {
26
+ const config = createMockConfig();
27
+ const translator = TranslatorFactory.create(config);
28
+ expect(translator).toBeDefined();
29
+ expect(TranslatorFactory.getInstanceCount()).toBe(1);
30
+ });
31
+
32
+ it('should return same instance for same config', () => {
33
+ const config = createMockConfig();
34
+ const t1 = TranslatorFactory.create(config);
35
+ const t2 = TranslatorFactory.create(config);
36
+ expect(t1).toBe(t2);
37
+ expect(TranslatorFactory.getInstanceCount()).toBe(1);
38
+ });
39
+
40
+ it('should create different instances for different configs', () => {
41
+ const config1 = createMockConfig({ defaultLanguage: 'ko' });
42
+ const config2 = createMockConfig({ defaultLanguage: 'en' });
43
+ const t1 = TranslatorFactory.create(config1);
44
+ const t2 = TranslatorFactory.create(config2);
45
+ expect(t1).not.toBe(t2);
46
+ expect(TranslatorFactory.getInstanceCount()).toBe(2);
47
+ });
48
+
49
+ it('should evict oldest instance when max reached', () => {
50
+ // MAX_INSTANCES is 10
51
+ for (let i = 0; i < 10; i++) {
52
+ TranslatorFactory.create(createMockConfig({
53
+ defaultLanguage: `lang-${i}`,
54
+ fallbackLanguage: 'en',
55
+ }));
56
+ }
57
+ expect(TranslatorFactory.getInstanceCount()).toBe(10);
58
+
59
+ // Adding 11th should evict the first
60
+ TranslatorFactory.create(createMockConfig({
61
+ defaultLanguage: 'lang-10',
62
+ fallbackLanguage: 'en',
63
+ }));
64
+ expect(TranslatorFactory.getInstanceCount()).toBe(10);
65
+ });
66
+
67
+ it('should recreate instance when config changes', () => {
68
+ const config1 = createMockConfig({ debug: false });
69
+ const t1 = TranslatorFactory.create(config1);
70
+ const config2 = createMockConfig({ debug: true });
71
+ const t2 = TranslatorFactory.create(config2);
72
+ // Different debug means different config key, so different instance
73
+ expect(t1).not.toBe(t2);
74
+ });
75
+ });
76
+
77
+ describe('get', () => {
78
+ it('should return existing instance', () => {
79
+ const config = createMockConfig();
80
+ const created = TranslatorFactory.create(config);
81
+ const retrieved = TranslatorFactory.get(config);
82
+ expect(retrieved).toBe(created);
83
+ });
84
+
85
+ it('should return null for non-existing config', () => {
86
+ const config = createMockConfig({ defaultLanguage: 'fr' });
87
+ expect(TranslatorFactory.get(config)).toBeNull();
88
+ });
89
+ });
90
+
91
+ describe('clear', () => {
92
+ it('should clear all instances', () => {
93
+ TranslatorFactory.create(createMockConfig());
94
+ expect(TranslatorFactory.getInstanceCount()).toBe(1);
95
+ TranslatorFactory.clear();
96
+ expect(TranslatorFactory.getInstanceCount()).toBe(0);
97
+ });
98
+ });
99
+
100
+ describe('clearConfig', () => {
101
+ it('should clear specific config instance', () => {
102
+ const config1 = createMockConfig({ defaultLanguage: 'ko' });
103
+ const config2 = createMockConfig({ defaultLanguage: 'en' });
104
+ TranslatorFactory.create(config1);
105
+ TranslatorFactory.create(config2);
106
+ expect(TranslatorFactory.getInstanceCount()).toBe(2);
107
+ TranslatorFactory.clearConfig(config1);
108
+ expect(TranslatorFactory.getInstanceCount()).toBe(1);
109
+ });
110
+ });
111
+
112
+ describe('debug', () => {
113
+ it('should return debug info', () => {
114
+ TranslatorFactory.create(createMockConfig());
115
+ const info = TranslatorFactory.debug();
116
+ expect(info.instanceCount).toBe(1);
117
+ expect(info.configKeys).toHaveLength(1);
118
+ });
119
+ });
120
+ });