@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,359 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createDebugTools, enableDebugTools, disableDebugTools, DebugTools } from '../core/debug-tools';
3
+
4
+ describe('debug-tools', () => {
5
+ const originalEnv = process.env.NODE_ENV;
6
+ const originalWindow = global.window;
7
+
8
+ beforeEach(() => {
9
+ // Mock window object
10
+ global.window = {
11
+ addEventListener: vi.fn(),
12
+ removeEventListener: vi.fn(),
13
+ dispatchEvent: vi.fn(),
14
+ __HUA_I18N_DEBUG__: undefined,
15
+ } as any;
16
+
17
+ // Mock document object
18
+ global.document = {
19
+ body: {
20
+ appendChild: vi.fn(),
21
+ },
22
+ createElement: vi.fn((tag: string) => {
23
+ const element: any = {
24
+ tagName: tag,
25
+ style: {},
26
+ innerHTML: '',
27
+ textContent: '',
28
+ id: '',
29
+ appendChild: vi.fn(),
30
+ addEventListener: vi.fn(),
31
+ removeEventListener: vi.fn(),
32
+ setAttribute: vi.fn(),
33
+ getAttribute: vi.fn(),
34
+ querySelectorAll: vi.fn().mockReturnValue([]),
35
+ querySelector: vi.fn(),
36
+ remove: vi.fn(),
37
+ getBoundingClientRect: vi.fn().mockReturnValue({
38
+ left: 0,
39
+ top: 0,
40
+ bottom: 0,
41
+ right: 0,
42
+ }),
43
+ };
44
+ return element;
45
+ }),
46
+ getElementById: vi.fn(),
47
+ querySelectorAll: vi.fn().mockReturnValue([]),
48
+ } as any;
49
+ });
50
+
51
+ afterEach(() => {
52
+ process.env.NODE_ENV = originalEnv;
53
+ global.window = originalWindow;
54
+ vi.clearAllMocks();
55
+ });
56
+
57
+ describe('createDebugTools', () => {
58
+ it('should return null in production', () => {
59
+ process.env.NODE_ENV = 'production';
60
+ const tools = createDebugTools();
61
+ expect(tools).toBeNull();
62
+ });
63
+
64
+ it('should return DebugTools object in development', () => {
65
+ process.env.NODE_ENV = 'development';
66
+ const tools = createDebugTools();
67
+ expect(tools).not.toBeNull();
68
+ expect(tools).toHaveProperty('highlightMissingKeys');
69
+ expect(tools).toHaveProperty('showTranslationKeys');
70
+ expect(tools).toHaveProperty('performanceMetrics');
71
+ expect(tools).toHaveProperty('devTools');
72
+ expect(tools).toHaveProperty('validateTranslations');
73
+ });
74
+
75
+ it('should return DebugTools object in test environment', () => {
76
+ process.env.NODE_ENV = 'test';
77
+ const tools = createDebugTools();
78
+ expect(tools).not.toBeNull();
79
+ });
80
+
81
+ it('should have performanceMetrics with initial values', () => {
82
+ process.env.NODE_ENV = 'development';
83
+ const tools = createDebugTools();
84
+ expect(tools?.performanceMetrics).toEqual({
85
+ translationCount: 0,
86
+ cacheHits: 0,
87
+ cacheMisses: 0,
88
+ loadTimes: [],
89
+ });
90
+ });
91
+
92
+ it('should have devTools with correct properties', () => {
93
+ process.env.NODE_ENV = 'development';
94
+ const tools = createDebugTools();
95
+ expect(tools?.devTools).toHaveProperty('open');
96
+ expect(tools?.devTools).toHaveProperty('close');
97
+ expect(tools?.devTools).toHaveProperty('isOpen');
98
+ expect(tools?.devTools.isOpen).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe('highlightMissingKeys', () => {
103
+ it('should highlight elements with missing translations', () => {
104
+ process.env.NODE_ENV = 'development';
105
+ const tools = createDebugTools();
106
+
107
+ const mockElement = {
108
+ getAttribute: vi.fn((attr: string) => (attr === 'data-i18n-key' ? 'test.key' : null)),
109
+ textContent: 'test.key',
110
+ style: {},
111
+ title: '',
112
+ };
113
+
114
+ const container = {
115
+ querySelectorAll: vi.fn().mockReturnValue([mockElement]),
116
+ } as any;
117
+
118
+ tools?.highlightMissingKeys(container);
119
+
120
+ expect(container.querySelectorAll).toHaveBeenCalledWith('[data-i18n-key]');
121
+ expect(mockElement.style.backgroundColor).toBe('#ffeb3b');
122
+ expect(mockElement.style.border).toBe('2px solid #f57c00');
123
+ expect(mockElement.title).toBe('Missing translation: test.key');
124
+ });
125
+
126
+ it('should not highlight elements with translated text', () => {
127
+ process.env.NODE_ENV = 'development';
128
+ const tools = createDebugTools();
129
+
130
+ const mockElement = {
131
+ getAttribute: vi.fn((attr: string) => (attr === 'data-i18n-key' ? 'test.key' : null)),
132
+ textContent: 'Translated Text',
133
+ style: {},
134
+ title: '',
135
+ };
136
+
137
+ const container = {
138
+ querySelectorAll: vi.fn().mockReturnValue([mockElement]),
139
+ } as any;
140
+
141
+ tools?.highlightMissingKeys(container);
142
+
143
+ expect(mockElement.style.backgroundColor).toBeUndefined();
144
+ expect(mockElement.style.border).toBeUndefined();
145
+ });
146
+ });
147
+
148
+ describe('showTranslationKeys', () => {
149
+ it('should attach mouseenter and mouseleave event listeners', () => {
150
+ process.env.NODE_ENV = 'development';
151
+ const tools = createDebugTools();
152
+
153
+ const mockElement = {
154
+ getAttribute: vi.fn((attr: string) => (attr === 'data-i18n-key' ? 'test.key' : null)),
155
+ addEventListener: vi.fn(),
156
+ getBoundingClientRect: vi.fn().mockReturnValue({
157
+ left: 100,
158
+ top: 50,
159
+ bottom: 70,
160
+ right: 200,
161
+ }),
162
+ };
163
+
164
+ const container = {
165
+ querySelectorAll: vi.fn().mockReturnValue([mockElement]),
166
+ } as any;
167
+
168
+ tools?.showTranslationKeys(container);
169
+
170
+ expect(mockElement.addEventListener).toHaveBeenCalledWith('mouseenter', expect.any(Function));
171
+ expect(mockElement.addEventListener).toHaveBeenCalledWith('mouseleave', expect.any(Function));
172
+ });
173
+ });
174
+
175
+ describe('validateTranslations', () => {
176
+ it('should identify missing keys (null/undefined values)', () => {
177
+ process.env.NODE_ENV = 'development';
178
+ const tools = createDebugTools();
179
+
180
+ const translations = {
181
+ key1: 'value1',
182
+ key2: null,
183
+ key3: undefined,
184
+ };
185
+
186
+ const result = tools?.validateTranslations(translations);
187
+ expect(result?.missingKeys).toContain('key2');
188
+ expect(result?.missingKeys).toContain('key3');
189
+ });
190
+
191
+ it('should identify invalid keys (empty strings)', () => {
192
+ process.env.NODE_ENV = 'development';
193
+ const tools = createDebugTools();
194
+
195
+ const translations = {
196
+ '': 'value',
197
+ ' ': 'value',
198
+ };
199
+
200
+ const result = tools?.validateTranslations(translations);
201
+ expect(result?.invalidKeys.length).toBeGreaterThan(0);
202
+ });
203
+
204
+ it('should validate nested translations', () => {
205
+ process.env.NODE_ENV = 'development';
206
+ const tools = createDebugTools();
207
+
208
+ const translations = {
209
+ level1: {
210
+ level2: {
211
+ key1: 'value1',
212
+ key2: null,
213
+ },
214
+ },
215
+ };
216
+
217
+ const result = tools?.validateTranslations(translations);
218
+ expect(result?.missingKeys).toContain('level1.level2.key2');
219
+ });
220
+
221
+ it('should return empty arrays for valid translations', () => {
222
+ process.env.NODE_ENV = 'development';
223
+ const tools = createDebugTools();
224
+
225
+ const translations = {
226
+ key1: 'value1',
227
+ key2: 'value2',
228
+ nested: {
229
+ key3: 'value3',
230
+ },
231
+ };
232
+
233
+ const result = tools?.validateTranslations(translations);
234
+ expect(result?.missingKeys).toHaveLength(0);
235
+ expect(result?.duplicateKeys).toHaveLength(0);
236
+ });
237
+
238
+ it('should handle arrays in translations (ignore them)', () => {
239
+ process.env.NODE_ENV = 'development';
240
+ const tools = createDebugTools();
241
+
242
+ const translations = {
243
+ items: ['item1', 'item2', 'item3'],
244
+ key: 'value',
245
+ };
246
+
247
+ const result = tools?.validateTranslations(translations);
248
+ expect(result?.invalidKeys).not.toContain('items');
249
+ });
250
+ });
251
+
252
+ describe('devTools', () => {
253
+ it('should open devTools panel', () => {
254
+ process.env.NODE_ENV = 'development';
255
+ const tools = createDebugTools();
256
+
257
+ const mockButton = {
258
+ addEventListener: vi.fn(),
259
+ };
260
+
261
+ const mockPanel = {
262
+ id: '',
263
+ style: {},
264
+ appendChild: vi.fn(),
265
+ querySelector: vi.fn().mockReturnValue(mockButton),
266
+ };
267
+
268
+ (document.createElement as any).mockReturnValue(mockPanel);
269
+
270
+ tools?.devTools.open();
271
+
272
+ expect(document.body.appendChild).toHaveBeenCalled();
273
+ });
274
+
275
+ it('should close devTools panel', () => {
276
+ process.env.NODE_ENV = 'development';
277
+ const tools = createDebugTools();
278
+
279
+ const mockPanel = {
280
+ remove: vi.fn(),
281
+ };
282
+
283
+ (document.getElementById as any).mockReturnValue(mockPanel);
284
+
285
+ tools?.devTools.close();
286
+
287
+ expect(document.getElementById).toHaveBeenCalledWith('hua-i18n-devtools');
288
+ expect(mockPanel.remove).toHaveBeenCalled();
289
+ });
290
+ });
291
+
292
+ describe('enableDebugTools', () => {
293
+ it('should warn in production', () => {
294
+ process.env.NODE_ENV = 'production';
295
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
296
+
297
+ enableDebugTools();
298
+
299
+ expect(warnSpy).toHaveBeenCalledWith('Debug tools are not available in production');
300
+ warnSpy.mockRestore();
301
+ });
302
+
303
+ it('should set global __HUA_I18N_DEBUG__ in development', () => {
304
+ process.env.NODE_ENV = 'development';
305
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
306
+
307
+ enableDebugTools();
308
+
309
+ expect((window as any).__HUA_I18N_DEBUG__).toBeDefined();
310
+ expect(logSpy).toHaveBeenCalledWith(
311
+ expect.stringContaining('HUA i18n debug tools enabled')
312
+ );
313
+ logSpy.mockRestore();
314
+ });
315
+
316
+ it('should open devTools panel automatically', () => {
317
+ process.env.NODE_ENV = 'development';
318
+ vi.spyOn(console, 'log').mockImplementation(() => {});
319
+
320
+ enableDebugTools();
321
+
322
+ expect(document.body.appendChild).toHaveBeenCalled();
323
+ });
324
+ });
325
+
326
+ describe('disableDebugTools', () => {
327
+ it('should remove global __HUA_I18N_DEBUG__', () => {
328
+ process.env.NODE_ENV = 'development';
329
+ vi.spyOn(console, 'log').mockImplementation(() => {});
330
+
331
+ enableDebugTools();
332
+ expect((window as any).__HUA_I18N_DEBUG__).toBeDefined();
333
+
334
+ disableDebugTools();
335
+ expect((window as any).__HUA_I18N_DEBUG__).toBeUndefined();
336
+ });
337
+
338
+ it('should close devTools panel', () => {
339
+ process.env.NODE_ENV = 'development';
340
+ vi.spyOn(console, 'log').mockImplementation(() => {});
341
+
342
+ const mockPanel = {
343
+ remove: vi.fn(),
344
+ };
345
+ (document.getElementById as any).mockReturnValue(mockPanel);
346
+
347
+ enableDebugTools();
348
+ disableDebugTools();
349
+
350
+ expect(document.getElementById).toHaveBeenCalledWith('hua-i18n-devtools');
351
+ });
352
+
353
+ it('should handle case when debug tools not enabled', () => {
354
+ process.env.NODE_ENV = 'development';
355
+
356
+ expect(() => disableDebugTools()).not.toThrow();
357
+ });
358
+ });
359
+ });
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getDefaultTranslations, getAllDefaultTranslations } from '../utils/default-translations';
3
+
4
+ describe('default-translations', () => {
5
+ describe('getDefaultTranslations', () => {
6
+ it('should return translations for valid language and namespace', () => {
7
+ const koCommon = getDefaultTranslations('ko', 'common');
8
+ expect(koCommon).toBeDefined();
9
+ expect(koCommon.welcome).toBe('환영합니다');
10
+ expect(koCommon.greeting).toBe('안녕하세요');
11
+ expect(koCommon.goodbye).toBe('안녕히 가세요');
12
+ });
13
+
14
+ it('should return Korean common translations', () => {
15
+ const translations = getDefaultTranslations('ko', 'common');
16
+ expect(translations.loading).toBe('로딩 중...');
17
+ expect(translations.error).toBe('오류가 발생했습니다');
18
+ expect(translations.success).toBe('성공했습니다');
19
+ expect(translations.cancel).toBe('취소');
20
+ expect(translations.confirm).toBe('확인');
21
+ });
22
+
23
+ it('should return Korean auth translations', () => {
24
+ const translations = getDefaultTranslations('ko', 'auth');
25
+ expect(translations.login).toBe('로그인');
26
+ expect(translations.logout).toBe('로그아웃');
27
+ expect(translations.register).toBe('회원가입');
28
+ expect(translations.email).toBe('이메일');
29
+ expect(translations.password).toBe('비밀번호');
30
+ expect(translations.forgot_password).toBe('비밀번호 찾기');
31
+ expect(translations.remember_me).toBe('로그인 상태 유지');
32
+ });
33
+
34
+ it('should return Korean errors translations', () => {
35
+ const translations = getDefaultTranslations('ko', 'errors');
36
+ expect(translations.not_found).toBe('페이지를 찾을 수 없습니다');
37
+ expect(translations.server_error).toBe('서버 오류가 발생했습니다');
38
+ expect(translations.network_error).toBe('네트워크 오류가 발생했습니다');
39
+ expect(translations.unauthorized).toBe('인증이 필요합니다');
40
+ expect(translations.forbidden).toBe('접근이 거부되었습니다');
41
+ });
42
+
43
+ it('should return English common translations', () => {
44
+ const translations = getDefaultTranslations('en', 'common');
45
+ expect(translations.welcome).toBe('Welcome');
46
+ expect(translations.greeting).toBe('Hello');
47
+ expect(translations.goodbye).toBe('Goodbye');
48
+ expect(translations.loading).toBe('Loading...');
49
+ expect(translations.error).toBe('An error occurred');
50
+ expect(translations.success).toBe('Success');
51
+ });
52
+
53
+ it('should return English auth translations', () => {
54
+ const translations = getDefaultTranslations('en', 'auth');
55
+ expect(translations.login).toBe('Login');
56
+ expect(translations.logout).toBe('Logout');
57
+ expect(translations.register).toBe('Register');
58
+ expect(translations.email).toBe('Email');
59
+ expect(translations.password).toBe('Password');
60
+ expect(translations.forgot_password).toBe('Forgot Password');
61
+ expect(translations.remember_me).toBe('Remember Me');
62
+ });
63
+
64
+ it('should return English errors translations', () => {
65
+ const translations = getDefaultTranslations('en', 'errors');
66
+ expect(translations.not_found).toBe('Page not found');
67
+ expect(translations.server_error).toBe('Server error occurred');
68
+ expect(translations.network_error).toBe('Network error occurred');
69
+ expect(translations.unauthorized).toBe('Authentication required');
70
+ expect(translations.forbidden).toBe('Access denied');
71
+ });
72
+
73
+ it('should return empty object for invalid language', () => {
74
+ const translations = getDefaultTranslations('fr', 'common');
75
+ expect(translations).toEqual({});
76
+ });
77
+
78
+ it('should return empty object for invalid namespace', () => {
79
+ const translations = getDefaultTranslations('ko', 'invalid');
80
+ expect(translations).toEqual({});
81
+ });
82
+
83
+ it('should return empty object for both invalid language and namespace', () => {
84
+ const translations = getDefaultTranslations('fr', 'invalid');
85
+ expect(translations).toEqual({});
86
+ });
87
+
88
+ it('should return empty object for undefined language', () => {
89
+ const translations = getDefaultTranslations('', 'common');
90
+ expect(translations).toEqual({});
91
+ });
92
+
93
+ it('should return empty object for undefined namespace', () => {
94
+ const translations = getDefaultTranslations('ko', '');
95
+ expect(translations).toEqual({});
96
+ });
97
+ });
98
+
99
+ describe('getAllDefaultTranslations', () => {
100
+ it('should return complete translations structure', () => {
101
+ const all = getAllDefaultTranslations();
102
+ expect(all).toBeDefined();
103
+ expect(all.ko).toBeDefined();
104
+ expect(all.en).toBeDefined();
105
+ });
106
+
107
+ it('should include all Korean namespaces', () => {
108
+ const all = getAllDefaultTranslations();
109
+ expect(all.ko.common).toBeDefined();
110
+ expect(all.ko.auth).toBeDefined();
111
+ expect(all.ko.errors).toBeDefined();
112
+ });
113
+
114
+ it('should include all English namespaces', () => {
115
+ const all = getAllDefaultTranslations();
116
+ expect(all.en.common).toBeDefined();
117
+ expect(all.en.auth).toBeDefined();
118
+ expect(all.en.errors).toBeDefined();
119
+ });
120
+
121
+ it('should have matching keys across languages', () => {
122
+ const all = getAllDefaultTranslations();
123
+ const koCommonKeys = Object.keys(all.ko.common);
124
+ const enCommonKeys = Object.keys(all.en.common);
125
+ expect(koCommonKeys).toEqual(enCommonKeys);
126
+ });
127
+
128
+ it('should have matching keys for auth namespace', () => {
129
+ const all = getAllDefaultTranslations();
130
+ const koAuthKeys = Object.keys(all.ko.auth);
131
+ const enAuthKeys = Object.keys(all.en.auth);
132
+ expect(koAuthKeys).toEqual(enAuthKeys);
133
+ });
134
+
135
+ it('should have matching keys for errors namespace', () => {
136
+ const all = getAllDefaultTranslations();
137
+ const koErrorsKeys = Object.keys(all.ko.errors);
138
+ const enErrorsKeys = Object.keys(all.en.errors);
139
+ expect(koErrorsKeys).toEqual(enErrorsKeys);
140
+ });
141
+
142
+ it('should return same reference on multiple calls', () => {
143
+ const all1 = getAllDefaultTranslations();
144
+ const all2 = getAllDefaultTranslations();
145
+ expect(all1).toBe(all2);
146
+ });
147
+
148
+ it('should contain expected number of common keys', () => {
149
+ const all = getAllDefaultTranslations();
150
+ const commonKeys = Object.keys(all.ko.common);
151
+ expect(commonKeys.length).toBeGreaterThan(15);
152
+ expect(commonKeys).toContain('welcome');
153
+ expect(commonKeys).toContain('greeting');
154
+ expect(commonKeys).toContain('loading');
155
+ expect(commonKeys).toContain('error');
156
+ expect(commonKeys).toContain('success');
157
+ });
158
+
159
+ it('should contain expected auth keys', () => {
160
+ const all = getAllDefaultTranslations();
161
+ const authKeys = Object.keys(all.ko.auth);
162
+ expect(authKeys).toContain('login');
163
+ expect(authKeys).toContain('logout');
164
+ expect(authKeys).toContain('register');
165
+ expect(authKeys).toContain('email');
166
+ expect(authKeys).toContain('password');
167
+ });
168
+
169
+ it('should contain expected error keys', () => {
170
+ const all = getAllDefaultTranslations();
171
+ const errorKeys = Object.keys(all.ko.errors);
172
+ expect(errorKeys).toContain('not_found');
173
+ expect(errorKeys).toContain('server_error');
174
+ expect(errorKeys).toContain('network_error');
175
+ expect(errorKeys).toContain('unauthorized');
176
+ expect(errorKeys).toContain('forbidden');
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { I18nResourceManager } from '../core/i18n-resource';
3
+
4
+ describe('I18nResourceManager', () => {
5
+ let manager: I18nResourceManager;
6
+
7
+ beforeEach(() => {
8
+ // Get singleton and reset it
9
+ manager = I18nResourceManager.getInstance();
10
+ manager.invalidateCache();
11
+ });
12
+
13
+ describe('getInstance', () => {
14
+ it('should return singleton instance', () => {
15
+ const a = I18nResourceManager.getInstance();
16
+ const b = I18nResourceManager.getInstance();
17
+ expect(a).toBe(b);
18
+ });
19
+ });
20
+
21
+ describe('getCachedTranslations', () => {
22
+ it('should call loader and cache result', async () => {
23
+ const loader = vi.fn().mockResolvedValue({ greeting: '안녕하세요' });
24
+ const result = await manager.getCachedTranslations('ko', 'common', loader);
25
+ expect(result).toEqual({ greeting: '안녕하세요' });
26
+ expect(loader).toHaveBeenCalledWith('ko', 'common');
27
+ });
28
+
29
+ it('should return cached result on second call', async () => {
30
+ const loader = vi.fn().mockResolvedValue({ greeting: '안녕하세요' });
31
+ await manager.getCachedTranslations('ko', 'common', loader);
32
+ const result = await manager.getCachedTranslations('ko', 'common', loader);
33
+ expect(result).toEqual({ greeting: '안녕하세요' });
34
+ expect(loader).toHaveBeenCalledTimes(1);
35
+ });
36
+
37
+ it('should deduplicate concurrent requests', async () => {
38
+ let resolvePromise: (value: any) => void;
39
+ const loader = vi.fn().mockImplementation(() => new Promise(resolve => { resolvePromise = resolve; }));
40
+
41
+ const p1 = manager.getCachedTranslations('ko', 'auth', loader);
42
+ const p2 = manager.getCachedTranslations('ko', 'auth', loader);
43
+
44
+ resolvePromise!({ login: '로그인' });
45
+
46
+ const [r1, r2] = await Promise.all([p1, p2]);
47
+ expect(r1).toEqual({ login: '로그인' });
48
+ expect(r2).toEqual({ login: '로그인' });
49
+ expect(loader).toHaveBeenCalledTimes(1);
50
+ });
51
+ });
52
+
53
+ describe('getCachedTranslationsSync', () => {
54
+ it('should return null for uncached', () => {
55
+ expect(manager.getCachedTranslationsSync('fr', 'common')).toBeNull();
56
+ });
57
+
58
+ it('should return cached data', async () => {
59
+ const loader = vi.fn().mockResolvedValue({ key: 'value' });
60
+ await manager.getCachedTranslations('ko', 'test', loader);
61
+ expect(manager.getCachedTranslationsSync('ko', 'test')).toEqual({ key: 'value' });
62
+ });
63
+ });
64
+
65
+ describe('getAllTranslationsForLanguage', () => {
66
+ it('should return all namespaces for a language', async () => {
67
+ const loader = vi.fn().mockImplementation(async (lang: string, ns: string) => ({ [`${ns}_key`]: 'value' }));
68
+ await manager.getCachedTranslations('ko', 'common', loader);
69
+ await manager.getCachedTranslations('ko', 'auth', loader);
70
+
71
+ const all = manager.getAllTranslationsForLanguage('ko');
72
+ expect(Object.keys(all)).toContain('common');
73
+ expect(Object.keys(all)).toContain('auth');
74
+ });
75
+ });
76
+
77
+ describe('invalidateCache', () => {
78
+ it('should invalidate specific language/namespace', async () => {
79
+ const loader = vi.fn().mockResolvedValue({ key: 'value' });
80
+ await manager.getCachedTranslations('ko', 'common', loader);
81
+ manager.invalidateCache('ko', 'common');
82
+ expect(manager.getCachedTranslationsSync('ko', 'common')).toBeNull();
83
+ });
84
+
85
+ it('should invalidate all namespaces for a language', async () => {
86
+ const loader = vi.fn().mockResolvedValue({ key: 'value' });
87
+ await manager.getCachedTranslations('ko', 'common', loader);
88
+ await manager.getCachedTranslations('ko', 'auth', loader);
89
+ manager.invalidateCache('ko');
90
+ expect(manager.getCachedTranslationsSync('ko', 'common')).toBeNull();
91
+ expect(manager.getCachedTranslationsSync('ko', 'auth')).toBeNull();
92
+ });
93
+
94
+ it('should invalidate all cache when no args', async () => {
95
+ const loader = vi.fn().mockResolvedValue({ key: 'value' });
96
+ await manager.getCachedTranslations('ko', 'common', loader);
97
+ await manager.getCachedTranslations('en', 'common', loader);
98
+ manager.invalidateCache();
99
+ expect(manager.getCachedTranslationsSync('ko', 'common')).toBeNull();
100
+ expect(manager.getCachedTranslationsSync('en', 'common')).toBeNull();
101
+ });
102
+ });
103
+
104
+ describe('hydrateFromSSR', () => {
105
+ it('should populate cache from SSR data', () => {
106
+ manager.hydrateFromSSR({
107
+ ko: { common: { title: '제목' } },
108
+ en: { common: { title: 'Title' } },
109
+ });
110
+ expect(manager.getCachedTranslationsSync('ko', 'common')).toEqual({ title: '제목' });
111
+ expect(manager.getCachedTranslationsSync('en', 'common')).toEqual({ title: 'Title' });
112
+ });
113
+ });
114
+
115
+ describe('getCacheStats', () => {
116
+ it('should track hits and misses', async () => {
117
+ const loader = vi.fn().mockResolvedValue({ key: 'value' });
118
+ await manager.getCachedTranslations('ko', 'stats', loader); // miss
119
+ await manager.getCachedTranslations('ko', 'stats', loader); // hit
120
+ const stats = manager.getCacheStats();
121
+ expect(stats.misses).toBeGreaterThanOrEqual(1);
122
+ expect(stats.hits).toBeGreaterThanOrEqual(1);
123
+ });
124
+ });
125
+
126
+ describe('setCacheLimit', () => {
127
+ it('should evict entries when over limit', async () => {
128
+ const loader = vi.fn().mockImplementation(async (lang: string, ns: string) => ({ key: ns }));
129
+ await manager.getCachedTranslations('ko', 'ns1', loader);
130
+ await manager.getCachedTranslations('ko', 'ns2', loader);
131
+ await manager.getCachedTranslations('ko', 'ns3', loader);
132
+ manager.setCacheLimit(1);
133
+ const stats = manager.getCacheStats();
134
+ expect(stats.size).toBe(1);
135
+ });
136
+ });
137
+ });