@hua-labs/i18n-core 1.0.0 → 1.1.0-alpha.0.1

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.
@@ -1,595 +1,595 @@
1
- "use client";
2
- import { useState, useEffect, useCallback, useContext, createContext, useMemo } from 'react';
3
- import { Translator } from '../core/translator';
4
- import { TranslatorFactory } from '../core/translator-factory';
5
- import {
6
- I18nConfig,
7
- I18nContextType,
8
- TranslationParams,
9
- TranslationError,
10
- validateI18nConfig
11
- } from '../types';
12
- import { getDefaultTranslations } from '../utils/default-translations';
13
-
14
- // React Context
15
- const I18nContext = createContext<I18nContextType | null>(null);
16
-
17
- /**
18
- * I18n Provider 컴포넌트
19
- */
20
- export function I18nProvider({
21
- config,
22
- children
23
- }: {
24
- config: I18nConfig & { autoLanguageSync?: boolean };
25
- children: React.ReactNode;
26
- }) {
27
- const [currentLanguage, setCurrentLanguageState] = useState(config.defaultLanguage);
28
- const [isLoading, setIsLoading] = useState(true);
29
- const [isInitialized, setIsInitialized] = useState(false);
30
- const [error, setError] = useState<TranslationError | null>(null);
31
- // 번역 로드 완료 시 리렌더링을 위한 상태
32
- const [translationVersion, setTranslationVersion] = useState(0);
33
-
34
- // config.defaultLanguage가 변경되면 currentLanguage도 업데이트
35
- // 단, 초기화 전에만 적용 (초기화 후에는 외부에서 언어 변경 가능)
36
- useEffect(() => {
37
- if (!isInitialized && config.defaultLanguage !== currentLanguage) {
38
- setCurrentLanguageState(config.defaultLanguage);
39
- }
40
- }, [config.defaultLanguage, currentLanguage, isInitialized]);
41
-
42
- // Translator 인스턴스 초기화 (메모이제이션)
43
- const translator = useMemo(() => {
44
- if (!validateI18nConfig(config)) {
45
- throw new Error('Invalid I18nConfig provided to I18nProvider');
46
- }
47
- return TranslatorFactory.create(config);
48
- }, [config]);
49
-
50
- // 초기화는 한 번만 수행
51
- useEffect(() => {
52
- if (isInitialized) {
53
- // 이미 초기화되어 있으면 언어만 변경
54
- // 단, translator의 현재 언어와 다를 때만 변경 (무한 루프 방지)
55
- const translatorLang = translator.getCurrentLanguage();
56
- if (translatorLang !== currentLanguage) {
57
- // translator의 언어를 currentLanguage로 변경
58
- // 이는 외부에서 setLanguage를 호출했을 때 발생하는 정상적인 동기화
59
- if (config.debug) {
60
- console.log(`🔄 [USEI18N] Syncing translator language: ${translatorLang} -> ${currentLanguage} (already initialized)`);
61
- }
62
- translator.setLanguage(currentLanguage);
63
- }
64
- return;
65
- }
66
-
67
- if (config.debug) {
68
- console.log('🔄 [USEI18N] useEffect triggered:', {
69
- hasTranslator: !!translator,
70
- currentLanguage,
71
- debug: config.debug,
72
- isInitialized
73
- });
74
- }
75
-
76
- const initializeTranslator = async () => {
77
- try {
78
- setIsLoading(true);
79
- setError(null);
80
-
81
- if (config.debug) {
82
- console.log('🚀 [USEI18N] Starting translator initialization...');
83
- }
84
-
85
- translator.setLanguage(currentLanguage);
86
-
87
- // 모든 번역 데이터 미리 로드
88
- await translator.initialize();
89
- setIsInitialized(true);
90
-
91
- if (config.debug) {
92
- console.log('✅ [USEI18N] Translator initialization completed successfully');
93
- }
94
- } catch (err) {
95
- const initError = err as TranslationError;
96
- setError(initError);
97
- if (config.debug) {
98
- console.error('❌ [USEI18N] Failed to initialize translator:', initError);
99
- }
100
- // 에러가 발생해도 초기화 완료로 표시 (기본 번역 사용)
101
- setIsInitialized(true);
102
- } finally {
103
- setIsLoading(false);
104
- }
105
- };
106
-
107
- initializeTranslator();
108
- }, [translator, currentLanguage, config.debug, isInitialized]);
109
-
110
- // 번역 로드 완료 이벤트 감지 (리렌더링 트리거)
111
- useEffect(() => {
112
- if (!translator || !isInitialized) {
113
- return;
114
- }
115
-
116
- const unsubscribe = translator.onTranslationLoaded(() => {
117
- // 번역이 로드되면 상태를 업데이트하여 리렌더링 트리거
118
- setTranslationVersion(prev => prev + 1);
119
- if (config.debug) {
120
- console.log('🔄 [USEI18N] Translation loaded, triggering re-render');
121
- }
122
- });
123
-
124
- return unsubscribe;
125
- }, [translator, isInitialized, config.debug]);
126
-
127
- // Translator의 언어 변경 감지 (외부에서 translator.setLanguage() 호출 시 동기화)
128
- useEffect(() => {
129
- if (!translator || !isInitialized) {
130
- return;
131
- }
132
-
133
- // 언어 변경 이벤트 구독
134
- const unsubscribe = translator.onLanguageChanged((newLanguage: string) => {
135
- if (newLanguage !== currentLanguage) {
136
- if (config.debug) {
137
- console.log(`🔄 [USEI18N] Language changed event: ${currentLanguage} -> ${newLanguage}`);
138
- }
139
- setCurrentLanguageState(newLanguage);
140
- setTranslationVersion(prev => prev + 1); // 리렌더링 트리거
141
- }
142
- });
143
-
144
- return unsubscribe;
145
- }, [translator, isInitialized, currentLanguage, config.debug]);
146
-
147
- // 자동 언어 전환 이벤트 처리
148
- useEffect(() => {
149
- if (!config.autoLanguageSync || typeof window === 'undefined') {
150
- return;
151
- }
152
-
153
- const handleLanguageChange = (event: CustomEvent) => {
154
- const newLanguage = event.detail;
155
- if (typeof newLanguage === 'string' && newLanguage !== currentLanguage) {
156
- if (config.debug) {
157
- console.log('🌐 Auto language sync:', newLanguage);
158
- }
159
- setLanguage(newLanguage);
160
- }
161
- };
162
-
163
- // hua-i18n-sdk 언어 전환 이벤트 감지
164
- window.addEventListener('huaI18nLanguageChange', handleLanguageChange as EventListener);
165
-
166
- // 일반적인 언어 변경 이벤트도 감지
167
- window.addEventListener('i18nLanguageChanged', handleLanguageChange as EventListener);
168
-
169
- return () => {
170
- window.removeEventListener('huaI18nLanguageChange', handleLanguageChange as EventListener);
171
- window.removeEventListener('i18nLanguageChanged', handleLanguageChange as EventListener);
172
- };
173
- }, [config.autoLanguageSync, currentLanguage]);
174
-
175
- // 언어 변경 함수 (메모이제이션)
176
- const setLanguage = useCallback(async (language: string) => {
177
- if (!translator) {
178
- return;
179
- }
180
-
181
- // 현재 언어와 동일하면 스킵 (무한 루프 방지)
182
- const currentLang = translator.getCurrentLanguage();
183
- if (currentLang === language) {
184
- if (config.debug) {
185
- console.log(`⏭️ [USEI18N] Language unchanged, skipping: ${language}`);
186
- }
187
- return;
188
- }
189
-
190
- if (config.debug) {
191
- if (config.debug) {
192
- console.log(`🔄 [USEI18N] setLanguage called: ${currentLang} -> ${language}`);
193
- }
194
- }
195
-
196
- setIsLoading(true);
197
-
198
- try {
199
- // 언어 변경 (translate 함수에서 이전 언어의 번역을 임시로 반환하므로 깜빡임 방지)
200
- translator.setLanguage(language);
201
- setCurrentLanguageState(language);
202
-
203
- // 새로운 언어의 번역 데이터가 이미 로드되어 있는지 확인
204
- // 로드되지 않은 네임스페이스는 자동으로 로드됨 (translator 내부에서 처리)
205
- // 언어 변경 시 리렌더링 트리거 (번역 로드 완료 이벤트가 자동으로 발생)
206
- await new Promise(resolve => setTimeout(resolve, 0)); // 다음 틱에서 리렌더링
207
-
208
- if (config.debug) {
209
- console.log(`✅ [USEI18N] Language changed to ${language}`);
210
- }
211
- } catch (error) {
212
- if (config.debug) {
213
- console.error(`❌ [USEI18N] Failed to change language to ${language}:`, error);
214
- }
215
- } finally {
216
- setIsLoading(false);
217
- }
218
- }, [translator, config.debug]);
219
-
220
- // parseKey 함수를 메모이제이션하여 성능 최적화
221
- const parseKey = useCallback((key: string) => {
222
- const parts = key.split(':');
223
- if (parts.length >= 2) {
224
- return { namespace: parts[0], key: parts.slice(1).join(':') };
225
- }
226
- return { namespace: 'common', key };
227
- }, []);
228
-
229
- // SSR 번역에서 찾기
230
- const findInSSRTranslations = useCallback((key: string, targetLang: string): string | null => {
231
- if (!config.initialTranslations) {
232
- return null;
233
- }
234
-
235
- const { namespace, key: actualKey } = parseKey(key);
236
-
237
- // 현재 언어의 SSR 번역 확인
238
- const ssrTranslations = config.initialTranslations[targetLang]?.[namespace];
239
- if (ssrTranslations && ssrTranslations[actualKey]) {
240
- const value = ssrTranslations[actualKey];
241
- if (typeof value === 'string') {
242
- return value;
243
- }
244
- }
245
-
246
- // 폴백 언어의 SSR 번역 확인
247
- const fallbackLang = config.fallbackLanguage || 'en';
248
- if (targetLang !== fallbackLang) {
249
- const fallbackTranslations = config.initialTranslations[fallbackLang]?.[namespace];
250
- if (fallbackTranslations && fallbackTranslations[actualKey]) {
251
- const value = fallbackTranslations[actualKey];
252
- if (typeof value === 'string') {
253
- return value;
254
- }
255
- }
256
- }
257
-
258
- return null;
259
- }, [config.initialTranslations, config.fallbackLanguage, parseKey]);
260
-
261
- // 기본 번역에서 찾기
262
- const findInDefaultTranslations = useCallback((key: string, targetLang: string): string | null => {
263
- const { namespace, key: actualKey } = parseKey(key);
264
- const defaultTranslations = getDefaultTranslations(targetLang, namespace);
265
- const fallbackTranslations = getDefaultTranslations(config.fallbackLanguage || 'en', namespace);
266
-
267
- return defaultTranslations[actualKey] || fallbackTranslations[actualKey] || null;
268
- }, [config.fallbackLanguage, parseKey]);
269
-
270
- // hua-api 스타일의 간단한 번역 함수 (메모이제이션)
271
- // translationVersion과 currentLanguage에 의존하여 번역 로드 및 언어 변경 시 리렌더링 트리거
272
- const t = useCallback((key: string, language?: string) => {
273
- // translationVersion과 currentLanguage를 참조하여 번역 로드 및 언어 변경 시 리렌더링 트리거
274
- // 의존성 배열에 포함되어 있어서 값이 변경되면 함수가 재생성됨
275
- void translationVersion;
276
- void currentLanguage;
277
-
278
- if (!translator) {
279
- return key;
280
- }
281
-
282
- const targetLang = language || currentLanguage;
283
-
284
- // 1단계: translator.translate() 시도
285
- try {
286
- const result = translator.translate(key, language);
287
- if (result && result !== key && result !== '') {
288
- return result;
289
- }
290
- } catch (error) {
291
- // translator.translate() 실패 시 다음 단계로 진행
292
- }
293
-
294
- // 2단계: SSR 번역 데이터에서 찾기
295
- const ssrResult = findInSSRTranslations(key, targetLang);
296
- if (ssrResult) {
297
- return ssrResult;
298
- }
299
-
300
- // 3단계: 기본 번역 데이터에서 찾기
301
- const defaultResult = findInDefaultTranslations(key, targetLang);
302
- if (defaultResult) {
303
- return defaultResult;
304
- }
305
-
306
- // 모든 단계에서 번역을 찾지 못한 경우
307
- if (config.debug) {
308
- return key; // 개발 환경에서는 키를 표시하여 디버깅 가능
309
- }
310
- return ''; // 프로덕션에서는 빈 문자열 반환하여 미싱 키 노출 방지
311
- }, [translator, config.debug, currentLanguage, config.fallbackLanguage, translationVersion, findInSSRTranslations, findInDefaultTranslations]) as (key: string, language?: string) => string;
312
-
313
- // 파라미터가 있는 번역 함수 (메모이제이션)
314
- const tWithParams = useCallback((key: string, params?: TranslationParams, language?: string) => {
315
- if (!translator || !isInitialized) {
316
- return key;
317
- }
318
- return translator.translateWithParams(key, params, language);
319
- }, [translator, isInitialized]);
320
-
321
- // 기존 비동기 번역 함수 (하위 호환성)
322
- const tAsync = useCallback(async (key: string, params?: TranslationParams) => {
323
- if (!translator) {
324
- if (config.debug) {
325
- console.warn('Translator not initialized');
326
- }
327
- return key;
328
- }
329
-
330
- setIsLoading(true);
331
- try {
332
- const result = await translator.translateAsync(key, params);
333
- return result;
334
- } catch (error) {
335
- if (config.debug) {
336
- console.error('Translation error:', error);
337
- }
338
- return key;
339
- } finally {
340
- setIsLoading(false);
341
- }
342
- }, [translator, config.debug]);
343
-
344
- // 기존 동기 번역 함수 (하위 호환성)
345
- const tSync = useCallback((key: string, namespace?: string, params?: TranslationParams) => {
346
- if (!translator) {
347
- if (config.debug) {
348
- console.warn('Translator not initialized');
349
- }
350
- return key;
351
- }
352
-
353
- return translator.translateSync(key, params);
354
- }, [translator, config.debug]);
355
-
356
- // 원시 값 가져오기 (배열, 객체 포함)
357
- const getRawValue = useCallback((key: string, language?: string): unknown => {
358
- if (!translator || !isInitialized) {
359
- return undefined;
360
- }
361
- return translator.getRawValue(key, language);
362
- }, [translator, isInitialized]);
363
-
364
- // 개발자 도구 (메모이제이션)
365
- const debug = useMemo(() => ({
366
- getCurrentLanguage: () => {
367
- try {
368
- return translator?.getCurrentLanguage() || currentLanguage;
369
- } catch {
370
- return currentLanguage;
371
- }
372
- },
373
- getSupportedLanguages: () => {
374
- try {
375
- return translator?.getSupportedLanguages() || config.supportedLanguages?.map(l => l.code) || [];
376
- } catch {
377
- return config.supportedLanguages?.map(l => l.code) || [];
378
- }
379
- },
380
- getLoadedNamespaces: () => {
381
- try {
382
- const debugInfo = translator?.debug();
383
- if (debugInfo && debugInfo.loadedNamespaces) {
384
- return Array.from(debugInfo.loadedNamespaces);
385
- }
386
- // 번역 데이터가 있으면 네임스페이스 추정
387
- if (debugInfo && debugInfo.allTranslations) {
388
- const namespaces = new Set<string>();
389
- Object.values(debugInfo.allTranslations).forEach((langData: unknown) => {
390
- if (langData && typeof langData === 'object') {
391
- Object.keys(langData).forEach(namespace => {
392
- namespaces.add(namespace);
393
- });
394
- }
395
- });
396
- return Array.from(namespaces);
397
- }
398
- return [];
399
- } catch (error) {
400
- return [];
401
- }
402
- },
403
- getAllTranslations: () => {
404
- try {
405
- return translator?.debug()?.allTranslations || {};
406
- } catch (error) {
407
- return {};
408
- }
409
- },
410
- isReady: () => {
411
- try {
412
- return translator?.isReady() || isInitialized;
413
- } catch {
414
- return isInitialized;
415
- }
416
- },
417
- getInitializationError: () => {
418
- try {
419
- return translator?.getInitializationError() || error;
420
- } catch {
421
- return error;
422
- }
423
- },
424
- clearCache: () => {
425
- try {
426
- translator?.clearCache();
427
- } catch {
428
- // 무시
429
- }
430
- },
431
- getCacheStats: () => {
432
- try {
433
- const debugInfo = translator?.debug();
434
- if (debugInfo && debugInfo.cacheStats) {
435
- return {
436
- size: debugInfo.cacheSize || 0,
437
- hits: debugInfo.cacheStats.hits || 0,
438
- misses: debugInfo.cacheStats.misses || 0
439
- };
440
- }
441
- return { size: 0, hits: 0, misses: 0 };
442
- } catch (error) {
443
- return { size: 0, hits: 0, misses: 0 };
444
- }
445
- },
446
- reloadTranslations: async () => {
447
- if (translator) {
448
- setIsLoading(true);
449
- setError(null);
450
- try {
451
- await translator.initialize();
452
- } catch (err) {
453
- setError(err as TranslationError);
454
- } finally {
455
- setIsLoading(false);
456
- }
457
- }
458
- },
459
- }), [translator, currentLanguage, error, isInitialized, config.supportedLanguages]);
460
-
461
- const value: I18nContextType = useMemo(() => ({
462
- currentLanguage,
463
- setLanguage,
464
- t,
465
- tWithParams,
466
- tAsync,
467
- tSync,
468
- getRawValue,
469
- isLoading,
470
- error,
471
- supportedLanguages: config.supportedLanguages,
472
- debug,
473
- isInitialized, // 추가: 초기화 상태 직접 노출
474
- translationVersion, // 번역 로드 완료 시 리렌더링 트리거
475
- }), [currentLanguage, setLanguage, t, tWithParams, tAsync, tSync, getRawValue, isLoading, error, config.supportedLanguages, debug, isInitialized, translationVersion]);
476
-
477
- // 의존성 배열은 이미 최적화되어 있음
478
- // t, tWithParams, tAsync, tSync, getRawValue는 모두 useCallback으로 메모이제이션됨
479
-
480
- return (
481
- <I18nContext.Provider value={value}>
482
- {children}
483
- </I18nContext.Provider>
484
- );
485
- }
486
-
487
- /**
488
- * I18n 훅
489
- */
490
- export function useI18n(): I18nContextType {
491
- const context = useContext(I18nContext);
492
- if (!context) {
493
- // Provider 밖에서 호출되면 기본값 반환
494
- return {
495
- currentLanguage: 'ko',
496
- setLanguage: () => {},
497
- t: (key: string) => key,
498
- tWithParams: (key: string) => key,
499
- tAsync: async (key: string) => key,
500
- tSync: (key: string) => key,
501
- getRawValue: () => undefined,
502
- isLoading: false,
503
- error: null,
504
- supportedLanguages: [
505
- { code: 'ko', name: 'Korean', nativeName: '한국어' },
506
- { code: 'en', name: 'English', nativeName: 'English' },
507
- ],
508
- isInitialized: false,
509
- debug: {
510
- getCurrentLanguage: () => 'ko',
511
- getSupportedLanguages: () => ['ko', 'en'],
512
- getLoadedNamespaces: () => [],
513
- getAllTranslations: () => ({}),
514
- isReady: () => false,
515
- getInitializationError: () => null,
516
- clearCache: () => {},
517
- getCacheStats: () => ({ size: 0, hits: 0, misses: 0 }),
518
- reloadTranslations: async () => {},
519
- },
520
- };
521
- }
522
- return context;
523
- }
524
-
525
- /**
526
- * 간단한 번역 훅 (hua-api 스타일)
527
- */
528
- export function useTranslation() {
529
- const { t, tWithParams, currentLanguage, setLanguage, isLoading, error, supportedLanguages } = useI18n();
530
-
531
- return {
532
- t,
533
- tWithParams,
534
- currentLanguage,
535
- setLanguage,
536
- isLoading,
537
- error,
538
- supportedLanguages,
539
- };
540
- }
541
-
542
- /**
543
- * 언어 변경 훅
544
- */
545
- export function useLanguageChange() {
546
- const context = useContext(I18nContext);
547
-
548
- // Provider 밖에서 호출되면 기본값 반환
549
- if (!context) {
550
- return {
551
- currentLanguage: 'ko',
552
- changeLanguage: () => {},
553
- supportedLanguages: [
554
- { code: 'ko', name: 'Korean', nativeName: '한국어' },
555
- { code: 'en', name: 'English', nativeName: 'English' },
556
- ],
557
- };
558
- }
559
-
560
- const { currentLanguage, setLanguage, supportedLanguages } = context;
561
-
562
- const changeLanguage = useCallback((language: string) => {
563
- const supported = supportedLanguages.find(lang => lang.code === language);
564
- if (supported) {
565
- setLanguage(language);
566
- } else {
567
- console.warn(`Language ${language} is not supported`);
568
- }
569
- }, [setLanguage, supportedLanguages]);
570
-
571
- return {
572
- currentLanguage,
573
- changeLanguage,
574
- supportedLanguages,
575
- };
576
- }
577
-
578
- // 기존 훅들 (하위 호환성을 위해 유지)
579
- export function usePreloadTranslations() {
580
- const context = useContext(I18nContext);
581
-
582
- const preload = useCallback(async (namespaces: string[]) => {
583
- if (!context) return;
584
-
585
- // 이미 초기화되어 있으므로 별도 로딩 불필요
586
- console.warn('usePreloadTranslations is deprecated. Translations are now preloaded automatically.');
587
- }, [context]);
588
-
589
- return { preload };
590
- }
591
-
592
- export function useAutoLoadNamespace(namespace: string) {
593
- // 이미 초기화되어 있으므로 별도 로딩 불필요
594
- console.warn('useAutoLoadNamespace is deprecated. All namespaces are now loaded automatically.');
1
+ "use client";
2
+ import { useState, useEffect, useCallback, useContext, createContext, useMemo } from 'react';
3
+ import { Translator } from '../core/translator';
4
+ import { TranslatorFactory } from '../core/translator-factory';
5
+ import {
6
+ I18nConfig,
7
+ I18nContextType,
8
+ TranslationParams,
9
+ TranslationError,
10
+ validateI18nConfig
11
+ } from '../types';
12
+ import { getDefaultTranslations } from '../utils/default-translations';
13
+
14
+ // React Context
15
+ const I18nContext = createContext<I18nContextType | null>(null);
16
+
17
+ /**
18
+ * I18n Provider 컴포넌트
19
+ */
20
+ export function I18nProvider({
21
+ config,
22
+ children
23
+ }: {
24
+ config: I18nConfig & { autoLanguageSync?: boolean };
25
+ children: React.ReactNode;
26
+ }) {
27
+ const [currentLanguage, setCurrentLanguageState] = useState(config.defaultLanguage);
28
+ const [isLoading, setIsLoading] = useState(true);
29
+ const [isInitialized, setIsInitialized] = useState(false);
30
+ const [error, setError] = useState<TranslationError | null>(null);
31
+ // 번역 로드 완료 시 리렌더링을 위한 상태
32
+ const [translationVersion, setTranslationVersion] = useState(0);
33
+
34
+ // config.defaultLanguage가 변경되면 currentLanguage도 업데이트
35
+ // 단, 초기화 전에만 적용 (초기화 후에는 외부에서 언어 변경 가능)
36
+ useEffect(() => {
37
+ if (!isInitialized && config.defaultLanguage !== currentLanguage) {
38
+ setCurrentLanguageState(config.defaultLanguage);
39
+ }
40
+ }, [config.defaultLanguage, currentLanguage, isInitialized]);
41
+
42
+ // Translator 인스턴스 초기화 (메모이제이션)
43
+ const translator = useMemo(() => {
44
+ if (!validateI18nConfig(config)) {
45
+ throw new Error('Invalid I18nConfig provided to I18nProvider');
46
+ }
47
+ return TranslatorFactory.create(config);
48
+ }, [config]);
49
+
50
+ // 초기화는 한 번만 수행
51
+ useEffect(() => {
52
+ if (isInitialized) {
53
+ // 이미 초기화되어 있으면 언어만 변경
54
+ // 단, translator의 현재 언어와 다를 때만 변경 (무한 루프 방지)
55
+ const translatorLang = translator.getCurrentLanguage();
56
+ if (translatorLang !== currentLanguage) {
57
+ // translator의 언어를 currentLanguage로 변경
58
+ // 이는 외부에서 setLanguage를 호출했을 때 발생하는 정상적인 동기화
59
+ if (config.debug) {
60
+ console.log(`🔄 [USEI18N] Syncing translator language: ${translatorLang} -> ${currentLanguage} (already initialized)`);
61
+ }
62
+ translator.setLanguage(currentLanguage);
63
+ }
64
+ return;
65
+ }
66
+
67
+ if (config.debug) {
68
+ console.log('🔄 [USEI18N] useEffect triggered:', {
69
+ hasTranslator: !!translator,
70
+ currentLanguage,
71
+ debug: config.debug,
72
+ isInitialized
73
+ });
74
+ }
75
+
76
+ const initializeTranslator = async () => {
77
+ try {
78
+ setIsLoading(true);
79
+ setError(null);
80
+
81
+ if (config.debug) {
82
+ console.log('🚀 [USEI18N] Starting translator initialization...');
83
+ }
84
+
85
+ translator.setLanguage(currentLanguage);
86
+
87
+ // 모든 번역 데이터 미리 로드
88
+ await translator.initialize();
89
+ setIsInitialized(true);
90
+
91
+ if (config.debug) {
92
+ console.log('✅ [USEI18N] Translator initialization completed successfully');
93
+ }
94
+ } catch (err) {
95
+ const initError = err as TranslationError;
96
+ setError(initError);
97
+ if (config.debug) {
98
+ console.error('❌ [USEI18N] Failed to initialize translator:', initError);
99
+ }
100
+ // 에러가 발생해도 초기화 완료로 표시 (기본 번역 사용)
101
+ setIsInitialized(true);
102
+ } finally {
103
+ setIsLoading(false);
104
+ }
105
+ };
106
+
107
+ initializeTranslator();
108
+ }, [translator, currentLanguage, config.debug, isInitialized]);
109
+
110
+ // 번역 로드 완료 이벤트 감지 (리렌더링 트리거)
111
+ useEffect(() => {
112
+ if (!translator || !isInitialized) {
113
+ return;
114
+ }
115
+
116
+ const unsubscribe = translator.onTranslationLoaded(() => {
117
+ // 번역이 로드되면 상태를 업데이트하여 리렌더링 트리거
118
+ setTranslationVersion(prev => prev + 1);
119
+ if (config.debug) {
120
+ console.log('🔄 [USEI18N] Translation loaded, triggering re-render');
121
+ }
122
+ });
123
+
124
+ return unsubscribe;
125
+ }, [translator, isInitialized, config.debug]);
126
+
127
+ // Translator의 언어 변경 감지 (외부에서 translator.setLanguage() 호출 시 동기화)
128
+ useEffect(() => {
129
+ if (!translator || !isInitialized) {
130
+ return;
131
+ }
132
+
133
+ // 언어 변경 이벤트 구독
134
+ const unsubscribe = translator.onLanguageChanged((newLanguage: string) => {
135
+ if (newLanguage !== currentLanguage) {
136
+ if (config.debug) {
137
+ console.log(`🔄 [USEI18N] Language changed event: ${currentLanguage} -> ${newLanguage}`);
138
+ }
139
+ setCurrentLanguageState(newLanguage);
140
+ setTranslationVersion(prev => prev + 1); // 리렌더링 트리거
141
+ }
142
+ });
143
+
144
+ return unsubscribe;
145
+ }, [translator, isInitialized, currentLanguage, config.debug]);
146
+
147
+ // 자동 언어 전환 이벤트 처리
148
+ useEffect(() => {
149
+ if (!config.autoLanguageSync || typeof window === 'undefined') {
150
+ return;
151
+ }
152
+
153
+ const handleLanguageChange = (event: CustomEvent) => {
154
+ const newLanguage = event.detail;
155
+ if (typeof newLanguage === 'string' && newLanguage !== currentLanguage) {
156
+ if (config.debug) {
157
+ console.log('🌐 Auto language sync:', newLanguage);
158
+ }
159
+ setLanguage(newLanguage);
160
+ }
161
+ };
162
+
163
+ // hua-i18n-sdk 언어 전환 이벤트 감지
164
+ window.addEventListener('huaI18nLanguageChange', handleLanguageChange as EventListener);
165
+
166
+ // 일반적인 언어 변경 이벤트도 감지
167
+ window.addEventListener('i18nLanguageChanged', handleLanguageChange as EventListener);
168
+
169
+ return () => {
170
+ window.removeEventListener('huaI18nLanguageChange', handleLanguageChange as EventListener);
171
+ window.removeEventListener('i18nLanguageChanged', handleLanguageChange as EventListener);
172
+ };
173
+ }, [config.autoLanguageSync, currentLanguage]);
174
+
175
+ // 언어 변경 함수 (메모이제이션)
176
+ const setLanguage = useCallback(async (language: string) => {
177
+ if (!translator) {
178
+ return;
179
+ }
180
+
181
+ // 현재 언어와 동일하면 스킵 (무한 루프 방지)
182
+ const currentLang = translator.getCurrentLanguage();
183
+ if (currentLang === language) {
184
+ if (config.debug) {
185
+ console.log(`⏭️ [USEI18N] Language unchanged, skipping: ${language}`);
186
+ }
187
+ return;
188
+ }
189
+
190
+ if (config.debug) {
191
+ if (config.debug) {
192
+ console.log(`🔄 [USEI18N] setLanguage called: ${currentLang} -> ${language}`);
193
+ }
194
+ }
195
+
196
+ setIsLoading(true);
197
+
198
+ try {
199
+ // 언어 변경 (translate 함수에서 이전 언어의 번역을 임시로 반환하므로 깜빡임 방지)
200
+ translator.setLanguage(language);
201
+ setCurrentLanguageState(language);
202
+
203
+ // 새로운 언어의 번역 데이터가 이미 로드되어 있는지 확인
204
+ // 로드되지 않은 네임스페이스는 자동으로 로드됨 (translator 내부에서 처리)
205
+ // 언어 변경 시 리렌더링 트리거 (번역 로드 완료 이벤트가 자동으로 발생)
206
+ await new Promise(resolve => setTimeout(resolve, 0)); // 다음 틱에서 리렌더링
207
+
208
+ if (config.debug) {
209
+ console.log(`✅ [USEI18N] Language changed to ${language}`);
210
+ }
211
+ } catch (error) {
212
+ if (config.debug) {
213
+ console.error(`❌ [USEI18N] Failed to change language to ${language}:`, error);
214
+ }
215
+ } finally {
216
+ setIsLoading(false);
217
+ }
218
+ }, [translator, config.debug]);
219
+
220
+ // parseKey 함수를 메모이제이션하여 성능 최적화
221
+ const parseKey = useCallback((key: string) => {
222
+ const parts = key.split(':');
223
+ if (parts.length >= 2) {
224
+ return { namespace: parts[0], key: parts.slice(1).join(':') };
225
+ }
226
+ return { namespace: 'common', key };
227
+ }, []);
228
+
229
+ // SSR 번역에서 찾기
230
+ const findInSSRTranslations = useCallback((key: string, targetLang: string): string | null => {
231
+ if (!config.initialTranslations) {
232
+ return null;
233
+ }
234
+
235
+ const { namespace, key: actualKey } = parseKey(key);
236
+
237
+ // 현재 언어의 SSR 번역 확인
238
+ const ssrTranslations = config.initialTranslations[targetLang]?.[namespace];
239
+ if (ssrTranslations && ssrTranslations[actualKey]) {
240
+ const value = ssrTranslations[actualKey];
241
+ if (typeof value === 'string') {
242
+ return value;
243
+ }
244
+ }
245
+
246
+ // 폴백 언어의 SSR 번역 확인
247
+ const fallbackLang = config.fallbackLanguage || 'en';
248
+ if (targetLang !== fallbackLang) {
249
+ const fallbackTranslations = config.initialTranslations[fallbackLang]?.[namespace];
250
+ if (fallbackTranslations && fallbackTranslations[actualKey]) {
251
+ const value = fallbackTranslations[actualKey];
252
+ if (typeof value === 'string') {
253
+ return value;
254
+ }
255
+ }
256
+ }
257
+
258
+ return null;
259
+ }, [config.initialTranslations, config.fallbackLanguage, parseKey]);
260
+
261
+ // 기본 번역에서 찾기
262
+ const findInDefaultTranslations = useCallback((key: string, targetLang: string): string | null => {
263
+ const { namespace, key: actualKey } = parseKey(key);
264
+ const defaultTranslations = getDefaultTranslations(targetLang, namespace);
265
+ const fallbackTranslations = getDefaultTranslations(config.fallbackLanguage || 'en', namespace);
266
+
267
+ return defaultTranslations[actualKey] || fallbackTranslations[actualKey] || null;
268
+ }, [config.fallbackLanguage, parseKey]);
269
+
270
+ // hua-api 스타일의 간단한 번역 함수 (메모이제이션)
271
+ // translationVersion과 currentLanguage에 의존하여 번역 로드 및 언어 변경 시 리렌더링 트리거
272
+ const t = useCallback((key: string, language?: string) => {
273
+ // translationVersion과 currentLanguage를 참조하여 번역 로드 및 언어 변경 시 리렌더링 트리거
274
+ // 의존성 배열에 포함되어 있어서 값이 변경되면 함수가 재생성됨
275
+ void translationVersion;
276
+ void currentLanguage;
277
+
278
+ if (!translator) {
279
+ return key;
280
+ }
281
+
282
+ const targetLang = language || currentLanguage;
283
+
284
+ // 1단계: translator.translate() 시도
285
+ try {
286
+ const result = translator.translate(key, language);
287
+ if (result && result !== key && result !== '') {
288
+ return result;
289
+ }
290
+ } catch (error) {
291
+ // translator.translate() 실패 시 다음 단계로 진행
292
+ }
293
+
294
+ // 2단계: SSR 번역 데이터에서 찾기
295
+ const ssrResult = findInSSRTranslations(key, targetLang);
296
+ if (ssrResult) {
297
+ return ssrResult;
298
+ }
299
+
300
+ // 3단계: 기본 번역 데이터에서 찾기
301
+ const defaultResult = findInDefaultTranslations(key, targetLang);
302
+ if (defaultResult) {
303
+ return defaultResult;
304
+ }
305
+
306
+ // 모든 단계에서 번역을 찾지 못한 경우
307
+ if (config.debug) {
308
+ return key; // 개발 환경에서는 키를 표시하여 디버깅 가능
309
+ }
310
+ return ''; // 프로덕션에서는 빈 문자열 반환하여 미싱 키 노출 방지
311
+ }, [translator, config.debug, currentLanguage, config.fallbackLanguage, translationVersion, findInSSRTranslations, findInDefaultTranslations]) as (key: string, language?: string) => string;
312
+
313
+ // 파라미터가 있는 번역 함수 (메모이제이션)
314
+ const tWithParams = useCallback((key: string, params?: TranslationParams, language?: string) => {
315
+ if (!translator || !isInitialized) {
316
+ return key;
317
+ }
318
+ return translator.translateWithParams(key, params, language);
319
+ }, [translator, isInitialized]);
320
+
321
+ // 기존 비동기 번역 함수 (하위 호환성)
322
+ const tAsync = useCallback(async (key: string, params?: TranslationParams) => {
323
+ if (!translator) {
324
+ if (config.debug) {
325
+ console.warn('Translator not initialized');
326
+ }
327
+ return key;
328
+ }
329
+
330
+ setIsLoading(true);
331
+ try {
332
+ const result = await translator.translateAsync(key, params);
333
+ return result;
334
+ } catch (error) {
335
+ if (config.debug) {
336
+ console.error('Translation error:', error);
337
+ }
338
+ return key;
339
+ } finally {
340
+ setIsLoading(false);
341
+ }
342
+ }, [translator, config.debug]);
343
+
344
+ // 기존 동기 번역 함수 (하위 호환성)
345
+ const tSync = useCallback((key: string, namespace?: string, params?: TranslationParams) => {
346
+ if (!translator) {
347
+ if (config.debug) {
348
+ console.warn('Translator not initialized');
349
+ }
350
+ return key;
351
+ }
352
+
353
+ return translator.translateSync(key, params);
354
+ }, [translator, config.debug]);
355
+
356
+ // 원시 값 가져오기 (배열, 객체 포함)
357
+ const getRawValue = useCallback((key: string, language?: string): unknown => {
358
+ if (!translator || !isInitialized) {
359
+ return undefined;
360
+ }
361
+ return translator.getRawValue(key, language);
362
+ }, [translator, isInitialized]);
363
+
364
+ // 개발자 도구 (메모이제이션)
365
+ const debug = useMemo(() => ({
366
+ getCurrentLanguage: () => {
367
+ try {
368
+ return translator?.getCurrentLanguage() || currentLanguage;
369
+ } catch {
370
+ return currentLanguage;
371
+ }
372
+ },
373
+ getSupportedLanguages: () => {
374
+ try {
375
+ return translator?.getSupportedLanguages() || config.supportedLanguages?.map(l => l.code) || [];
376
+ } catch {
377
+ return config.supportedLanguages?.map(l => l.code) || [];
378
+ }
379
+ },
380
+ getLoadedNamespaces: () => {
381
+ try {
382
+ const debugInfo = translator?.debug();
383
+ if (debugInfo && debugInfo.loadedNamespaces) {
384
+ return Array.from(debugInfo.loadedNamespaces);
385
+ }
386
+ // 번역 데이터가 있으면 네임스페이스 추정
387
+ if (debugInfo && debugInfo.allTranslations) {
388
+ const namespaces = new Set<string>();
389
+ Object.values(debugInfo.allTranslations).forEach((langData: unknown) => {
390
+ if (langData && typeof langData === 'object') {
391
+ Object.keys(langData).forEach(namespace => {
392
+ namespaces.add(namespace);
393
+ });
394
+ }
395
+ });
396
+ return Array.from(namespaces);
397
+ }
398
+ return [];
399
+ } catch (error) {
400
+ return [];
401
+ }
402
+ },
403
+ getAllTranslations: () => {
404
+ try {
405
+ return translator?.debug()?.allTranslations || {};
406
+ } catch (error) {
407
+ return {};
408
+ }
409
+ },
410
+ isReady: () => {
411
+ try {
412
+ return translator?.isReady() || isInitialized;
413
+ } catch {
414
+ return isInitialized;
415
+ }
416
+ },
417
+ getInitializationError: () => {
418
+ try {
419
+ return translator?.getInitializationError() || error;
420
+ } catch {
421
+ return error;
422
+ }
423
+ },
424
+ clearCache: () => {
425
+ try {
426
+ translator?.clearCache();
427
+ } catch {
428
+ // 무시
429
+ }
430
+ },
431
+ getCacheStats: () => {
432
+ try {
433
+ const debugInfo = translator?.debug();
434
+ if (debugInfo && debugInfo.cacheStats) {
435
+ return {
436
+ size: debugInfo.cacheSize || 0,
437
+ hits: debugInfo.cacheStats.hits || 0,
438
+ misses: debugInfo.cacheStats.misses || 0
439
+ };
440
+ }
441
+ return { size: 0, hits: 0, misses: 0 };
442
+ } catch (error) {
443
+ return { size: 0, hits: 0, misses: 0 };
444
+ }
445
+ },
446
+ reloadTranslations: async () => {
447
+ if (translator) {
448
+ setIsLoading(true);
449
+ setError(null);
450
+ try {
451
+ await translator.initialize();
452
+ } catch (err) {
453
+ setError(err as TranslationError);
454
+ } finally {
455
+ setIsLoading(false);
456
+ }
457
+ }
458
+ },
459
+ }), [translator, currentLanguage, error, isInitialized, config.supportedLanguages]);
460
+
461
+ const value: I18nContextType = useMemo(() => ({
462
+ currentLanguage,
463
+ setLanguage,
464
+ t,
465
+ tWithParams,
466
+ tAsync,
467
+ tSync,
468
+ getRawValue,
469
+ isLoading,
470
+ error,
471
+ supportedLanguages: config.supportedLanguages,
472
+ debug,
473
+ isInitialized, // 추가: 초기화 상태 직접 노출
474
+ translationVersion, // 번역 로드 완료 시 리렌더링 트리거
475
+ }), [currentLanguage, setLanguage, t, tWithParams, tAsync, tSync, getRawValue, isLoading, error, config.supportedLanguages, debug, isInitialized, translationVersion]);
476
+
477
+ // 의존성 배열은 이미 최적화되어 있음
478
+ // t, tWithParams, tAsync, tSync, getRawValue는 모두 useCallback으로 메모이제이션됨
479
+
480
+ return (
481
+ <I18nContext.Provider value={value}>
482
+ {children}
483
+ </I18nContext.Provider>
484
+ );
485
+ }
486
+
487
+ /**
488
+ * I18n 훅
489
+ */
490
+ export function useI18n(): I18nContextType {
491
+ const context = useContext(I18nContext);
492
+ if (!context) {
493
+ // Provider 밖에서 호출되면 기본값 반환
494
+ return {
495
+ currentLanguage: 'ko',
496
+ setLanguage: () => {},
497
+ t: (key: string) => key,
498
+ tWithParams: (key: string) => key,
499
+ tAsync: async (key: string) => key,
500
+ tSync: (key: string) => key,
501
+ getRawValue: () => undefined,
502
+ isLoading: false,
503
+ error: null,
504
+ supportedLanguages: [
505
+ { code: 'ko', name: 'Korean', nativeName: '한국어' },
506
+ { code: 'en', name: 'English', nativeName: 'English' },
507
+ ],
508
+ isInitialized: false,
509
+ debug: {
510
+ getCurrentLanguage: () => 'ko',
511
+ getSupportedLanguages: () => ['ko', 'en'],
512
+ getLoadedNamespaces: () => [],
513
+ getAllTranslations: () => ({}),
514
+ isReady: () => false,
515
+ getInitializationError: () => null,
516
+ clearCache: () => {},
517
+ getCacheStats: () => ({ size: 0, hits: 0, misses: 0 }),
518
+ reloadTranslations: async () => {},
519
+ },
520
+ };
521
+ }
522
+ return context;
523
+ }
524
+
525
+ /**
526
+ * 간단한 번역 훅 (hua-api 스타일)
527
+ */
528
+ export function useTranslation() {
529
+ const { t, tWithParams, currentLanguage, setLanguage, isLoading, error, supportedLanguages } = useI18n();
530
+
531
+ return {
532
+ t,
533
+ tWithParams,
534
+ currentLanguage,
535
+ setLanguage,
536
+ isLoading,
537
+ error,
538
+ supportedLanguages,
539
+ };
540
+ }
541
+
542
+ /**
543
+ * 언어 변경 훅
544
+ */
545
+ export function useLanguageChange() {
546
+ const context = useContext(I18nContext);
547
+
548
+ // Provider 밖에서 호출되면 기본값 반환
549
+ if (!context) {
550
+ return {
551
+ currentLanguage: 'ko',
552
+ changeLanguage: () => {},
553
+ supportedLanguages: [
554
+ { code: 'ko', name: 'Korean', nativeName: '한국어' },
555
+ { code: 'en', name: 'English', nativeName: 'English' },
556
+ ],
557
+ };
558
+ }
559
+
560
+ const { currentLanguage, setLanguage, supportedLanguages } = context;
561
+
562
+ const changeLanguage = useCallback((language: string) => {
563
+ const supported = supportedLanguages.find(lang => lang.code === language);
564
+ if (supported) {
565
+ setLanguage(language);
566
+ } else {
567
+ console.warn(`Language ${language} is not supported`);
568
+ }
569
+ }, [setLanguage, supportedLanguages]);
570
+
571
+ return {
572
+ currentLanguage,
573
+ changeLanguage,
574
+ supportedLanguages,
575
+ };
576
+ }
577
+
578
+ // 기존 훅들 (하위 호환성을 위해 유지)
579
+ export function usePreloadTranslations() {
580
+ const context = useContext(I18nContext);
581
+
582
+ const preload = useCallback(async (namespaces: string[]) => {
583
+ if (!context) return;
584
+
585
+ // 이미 초기화되어 있으므로 별도 로딩 불필요
586
+ console.warn('usePreloadTranslations is deprecated. Translations are now preloaded automatically.');
587
+ }, [context]);
588
+
589
+ return { preload };
590
+ }
591
+
592
+ export function useAutoLoadNamespace(namespace: string) {
593
+ // 이미 초기화되어 있으므로 별도 로딩 불필요
594
+ console.warn('useAutoLoadNamespace is deprecated. All namespaces are now loaded automatically.');
595
595
  }