@hua-labs/i18n-core 2.2.0 → 2.2.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,16 +1,23 @@
1
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';
2
+ import {
3
+ useState,
4
+ useEffect,
5
+ useCallback,
6
+ useContext,
7
+ createContext,
8
+ useMemo,
9
+ } from "react";
10
+ import { Translator } from "../core/translator";
11
+ import { TranslatorFactory } from "../core/translator-factory";
5
12
  import {
6
13
  I18nConfig,
7
14
  I18nContextType,
8
15
  TranslationParams,
9
16
  TranslationError,
10
17
  validateI18nConfig,
11
- webPlatformAdapter
12
- } from '../types';
13
- import { getDefaultTranslations } from '../utils/default-translations';
18
+ webPlatformAdapter,
19
+ } from "../types";
20
+ import { getDefaultTranslations } from "../utils/default-translations";
14
21
 
15
22
  // React Context
16
23
  const I18nContext = createContext<I18nContextType | null>(null);
@@ -21,7 +28,7 @@ const I18nContext = createContext<I18nContextType | null>(null);
21
28
  * config.defaultLanguage가 명시적으로 제공되지 않은 경우에만 디바이스 언어 감지 동작
22
29
  */
23
30
  function resolveInitialLanguage(
24
- config: I18nConfig & { autoLanguageSync?: boolean }
31
+ config: I18nConfig & { autoLanguageSync?: boolean },
25
32
  ): string {
26
33
  // 1. config.defaultLanguage가 명시적으로 제공된 경우 우선 사용
27
34
  if (config.defaultLanguage) {
@@ -32,14 +39,14 @@ function resolveInitialLanguage(
32
39
  const adapter = config.platformAdapter ?? webPlatformAdapter;
33
40
  const deviceLang = adapter.getDeviceLanguage();
34
41
  if (deviceLang) {
35
- const supportedCodes = config.supportedLanguages?.map(l => l.code) ?? [];
42
+ const supportedCodes = config.supportedLanguages?.map((l) => l.code) ?? [];
36
43
  if (supportedCodes.includes(deviceLang)) {
37
44
  return deviceLang;
38
45
  }
39
46
  }
40
47
 
41
48
  // 3. 첫 번째 지원 언어로 폴백
42
- return config.supportedLanguages?.[0]?.code ?? 'ko';
49
+ return config.supportedLanguages?.[0]?.code ?? "ko";
43
50
  }
44
51
 
45
52
  /**
@@ -47,12 +54,14 @@ function resolveInitialLanguage(
47
54
  */
48
55
  export function I18nProvider({
49
56
  config,
50
- children
57
+ children,
51
58
  }: {
52
59
  config: I18nConfig & { autoLanguageSync?: boolean };
53
60
  children: React.ReactNode;
54
61
  }) {
55
- const [currentLanguage, setCurrentLanguageState] = useState(() => resolveInitialLanguage(config));
62
+ const [currentLanguage, setCurrentLanguageState] = useState(() =>
63
+ resolveInitialLanguage(config),
64
+ );
56
65
  const [isLoading, setIsLoading] = useState(true);
57
66
  const [isInitialized, setIsInitialized] = useState(false);
58
67
  const [error, setError] = useState<TranslationError | null>(null);
@@ -70,7 +79,7 @@ export function I18nProvider({
70
79
  // Translator 인스턴스 초기화 (메모이제이션)
71
80
  const translator = useMemo(() => {
72
81
  if (!validateI18nConfig(config)) {
73
- throw new Error('Invalid I18nConfig provided to I18nProvider');
82
+ throw new Error("Invalid I18nConfig provided to I18nProvider");
74
83
  }
75
84
  return TranslatorFactory.create(config);
76
85
  }, [config]);
@@ -85,45 +94,52 @@ export function I18nProvider({
85
94
  // translator의 언어를 currentLanguage로 변경
86
95
  // 이는 외부에서 setLanguage를 호출했을 때 발생하는 정상적인 동기화
87
96
  if (config.debug) {
88
- console.log(`🔄 [USEI18N] Syncing translator language: ${translatorLang} -> ${currentLanguage} (already initialized)`);
97
+ console.log(
98
+ `🔄 [USEI18N] Syncing translator language: ${translatorLang} -> ${currentLanguage} (already initialized)`,
99
+ );
89
100
  }
90
101
  translator.setLanguage(currentLanguage);
91
102
  }
92
103
  return;
93
104
  }
94
-
105
+
95
106
  if (config.debug) {
96
- console.log('🔄 [USEI18N] useEffect triggered:', {
97
- hasTranslator: !!translator,
98
- currentLanguage,
107
+ console.log("🔄 [USEI18N] useEffect triggered:", {
108
+ hasTranslator: !!translator,
109
+ currentLanguage,
99
110
  debug: config.debug,
100
- isInitialized
111
+ isInitialized,
101
112
  });
102
113
  }
103
-
114
+
104
115
  const initializeTranslator = async () => {
105
116
  try {
106
117
  setIsLoading(true);
107
118
  setError(null);
108
-
119
+
109
120
  if (config.debug) {
110
- console.log('🚀 [USEI18N] Starting translator initialization...');
121
+ console.log("🚀 [USEI18N] Starting translator initialization...");
111
122
  }
112
-
123
+
113
124
  translator.setLanguage(currentLanguage);
114
-
125
+
115
126
  // 모든 번역 데이터 미리 로드
116
127
  await translator.initialize();
117
128
  setIsInitialized(true);
118
-
129
+
119
130
  if (config.debug) {
120
- console.log('✅ [USEI18N] Translator initialization completed successfully');
131
+ console.log(
132
+ "✅ [USEI18N] Translator initialization completed successfully",
133
+ );
121
134
  }
122
135
  } catch (err) {
123
136
  const initError = err as TranslationError;
124
137
  setError(initError);
125
138
  if (config.debug) {
126
- console.error('❌ [USEI18N] Failed to initialize translator:', initError);
139
+ console.error(
140
+ "❌ [USEI18N] Failed to initialize translator:",
141
+ initError,
142
+ );
127
143
  }
128
144
  // 에러가 발생해도 초기화 완료로 표시 (기본 번역 사용)
129
145
  setIsInitialized(true);
@@ -143,9 +159,9 @@ export function I18nProvider({
143
159
 
144
160
  const unsubscribe = translator.onTranslationLoaded(() => {
145
161
  // 번역이 로드되면 상태를 업데이트하여 리렌더링 트리거
146
- setTranslationVersion(prev => prev + 1);
162
+ setTranslationVersion((prev) => prev + 1);
147
163
  if (config.debug) {
148
- console.log('🔄 [USEI18N] Translation loaded, triggering re-render');
164
+ console.log("🔄 [USEI18N] Translation loaded, triggering re-render");
149
165
  }
150
166
  });
151
167
 
@@ -162,10 +178,12 @@ export function I18nProvider({
162
178
  const unsubscribe = translator.onLanguageChanged((newLanguage: string) => {
163
179
  if (newLanguage !== currentLanguage) {
164
180
  if (config.debug) {
165
- console.log(`🔄 [USEI18N] Language changed event: ${currentLanguage} -> ${newLanguage}`);
181
+ console.log(
182
+ `🔄 [USEI18N] Language changed event: ${currentLanguage} -> ${newLanguage}`,
183
+ );
166
184
  }
167
185
  setCurrentLanguageState(newLanguage);
168
- setTranslationVersion(prev => prev + 1); // 리렌더링 트리거
186
+ setTranslationVersion((prev) => prev + 1); // 리렌더링 트리거
169
187
  }
170
188
  });
171
189
 
@@ -182,7 +200,7 @@ export function I18nProvider({
182
200
  return adapter.onLanguageChange((newLanguage) => {
183
201
  if (newLanguage !== currentLanguage) {
184
202
  if (config.debug) {
185
- console.log('🌐 Auto language sync:', newLanguage);
203
+ console.log("🌐 Auto language sync:", newLanguage);
186
204
  }
187
205
  setLanguage(newLanguage);
188
206
  }
@@ -190,364 +208,491 @@ export function I18nProvider({
190
208
  }, [config.autoLanguageSync, config.platformAdapter, currentLanguage]);
191
209
 
192
210
  // 언어 변경 함수 (메모이제이션)
193
- const setLanguage = useCallback(async (language: string) => {
194
- if (!translator) {
195
- return;
196
- }
197
-
198
- // 현재 언어와 동일하면 스킵 (무한 루프 방지)
199
- const currentLang = translator.getCurrentLanguage();
200
- if (currentLang === language) {
201
- if (config.debug) {
202
- console.log(`⏭️ [USEI18N] Language unchanged, skipping: ${language}`);
211
+ const setLanguage = useCallback(
212
+ async (language: string) => {
213
+ if (!translator) {
214
+ return;
203
215
  }
204
- return;
205
- }
206
216
 
207
- if (config.debug) {
208
- if (config.debug) {
209
- console.log(`🔄 [USEI18N] setLanguage called: ${currentLang} -> ${language}`);
217
+ // 현재 언어와 동일하면 스킵 (무한 루프 방지)
218
+ const currentLang = translator.getCurrentLanguage();
219
+ if (currentLang === language) {
220
+ if (config.debug) {
221
+ console.log(`⏭️ [USEI18N] Language unchanged, skipping: ${language}`);
222
+ }
223
+ return;
210
224
  }
211
- }
212
-
213
- setIsLoading(true);
214
-
215
- try {
216
- // 언어 변경 (translate 함수에서 이전 언어의 번역을 임시로 반환하므로 깜빡임 방지)
217
- translator.setLanguage(language);
218
- setCurrentLanguageState(language);
219
-
220
- // 새로운 언어의 번역 데이터가 이미 로드되어 있는지 확인
221
- // 로드되지 않은 네임스페이스는 자동으로 로드됨 (translator 내부에서 처리)
222
- // 언어 변경 시 리렌더링 트리거 (번역 로드 완료 이벤트가 자동으로 발생)
223
- await new Promise(resolve => setTimeout(resolve, 0)); // 다음 틱에서 리렌더링
224
-
225
+
225
226
  if (config.debug) {
226
- console.log(`✅ [USEI18N] Language changed to ${language}`);
227
+ if (config.debug) {
228
+ console.log(
229
+ `🔄 [USEI18N] setLanguage called: ${currentLang} -> ${language}`,
230
+ );
231
+ }
227
232
  }
228
- } catch (error) {
229
- if (config.debug) {
230
- console.error(`❌ [USEI18N] Failed to change language to ${language}:`, error);
233
+
234
+ setIsLoading(true);
235
+
236
+ try {
237
+ // 언어 변경 (translate 함수에서 이전 언어의 번역을 임시로 반환하므로 깜빡임 방지)
238
+ translator.setLanguage(language);
239
+ setCurrentLanguageState(language);
240
+
241
+ // 새로운 언어의 번역 데이터가 이미 로드되어 있는지 확인
242
+ // 로드되지 않은 네임스페이스는 자동으로 로드됨 (translator 내부에서 처리)
243
+ // 언어 변경 시 리렌더링 트리거 (번역 로드 완료 이벤트가 자동으로 발생)
244
+ await new Promise((resolve) => setTimeout(resolve, 0)); // 다음 틱에서 리렌더링
245
+
246
+ if (config.debug) {
247
+ console.log(`✅ [USEI18N] Language changed to ${language}`);
248
+ }
249
+ } catch (error) {
250
+ if (config.debug) {
251
+ console.error(
252
+ `❌ [USEI18N] Failed to change language to ${language}:`,
253
+ error,
254
+ );
255
+ }
256
+ } finally {
257
+ setIsLoading(false);
231
258
  }
232
- } finally {
233
- setIsLoading(false);
234
- }
235
- }, [translator, config.debug]);
259
+ },
260
+ [translator, config.debug],
261
+ );
236
262
 
237
263
  // parseKey 함수를 메모이제이션하여 성능 최적화
238
264
  const parseKey = useCallback((key: string) => {
239
- const parts = key.split(':');
265
+ const parts = key.split(":");
240
266
  if (parts.length >= 2) {
241
- return { namespace: parts[0], key: parts.slice(1).join(':') };
267
+ return { namespace: parts[0], key: parts.slice(1).join(":") };
242
268
  }
243
- return { namespace: 'common', key };
269
+ return { namespace: "common", key };
244
270
  }, []);
245
271
 
246
272
  // 네스티드 키 해석 (예: "nav.docs" → obj.nav.docs)
247
- const resolveNestedKey = useCallback((obj: Record<string, unknown>, key: string): string | null => {
248
- // 1차: flat 접근 시도 (키에 점이 없거나 flat 구조인 경우)
249
- if (key in obj && typeof obj[key] === 'string') {
250
- return obj[key] as string;
251
- }
273
+ const resolveNestedKey = useCallback(
274
+ (obj: Record<string, unknown>, key: string): string | null => {
275
+ // 1차: flat 접근 시도 (키에 점이 없거나 flat 구조인 경우)
276
+ if (key in obj && typeof obj[key] === "string") {
277
+ return obj[key] as string;
278
+ }
252
279
 
253
- // 2차: 네스티드 접근 (점 경로 탐색)
254
- const parts = key.split('.');
255
- let current: unknown = obj;
256
- for (const part of parts) {
257
- if (current && typeof current === 'object' && current !== null && part in (current as Record<string, unknown>)) {
258
- current = (current as Record<string, unknown>)[part];
259
- } else {
260
- return null;
280
+ // 2차: 네스티드 접근 (점 경로 탐색)
281
+ const parts = key.split(".");
282
+ let current: unknown = obj;
283
+ for (const part of parts) {
284
+ if (
285
+ current &&
286
+ typeof current === "object" &&
287
+ current !== null &&
288
+ part in (current as Record<string, unknown>)
289
+ ) {
290
+ current = (current as Record<string, unknown>)[part];
291
+ } else {
292
+ return null;
293
+ }
261
294
  }
262
- }
263
- return typeof current === 'string' ? current : null;
264
- }, []);
295
+ return typeof current === "string" ? current : null;
296
+ },
297
+ [],
298
+ );
265
299
 
266
300
  // SSR 번역에서 찾기
267
- const findInSSRTranslations = useCallback((key: string, targetLang: string): string | null => {
268
- if (!config.initialTranslations) {
269
- return null;
270
- }
271
-
272
- const { namespace, key: actualKey } = parseKey(key);
273
-
274
- // 현재 언어의 SSR 번역 확인
275
- const ssrTranslations = config.initialTranslations[targetLang]?.[namespace];
276
- if (ssrTranslations) {
277
- const value = resolveNestedKey(ssrTranslations as Record<string, unknown>, actualKey);
278
- if (value !== null) {
279
- return value;
301
+ const findInSSRTranslations = useCallback(
302
+ (key: string, targetLang: string): string | null => {
303
+ if (!config.initialTranslations) {
304
+ return null;
280
305
  }
281
- }
282
306
 
283
- // 폴백 언어의 SSR 번역 확인
284
- const fallbackLang = config.fallbackLanguage || 'en';
285
- if (targetLang !== fallbackLang) {
286
- const fallbackTranslations = config.initialTranslations[fallbackLang]?.[namespace];
287
- if (fallbackTranslations) {
288
- const value = resolveNestedKey(fallbackTranslations as Record<string, unknown>, actualKey);
307
+ const { namespace, key: actualKey } = parseKey(key);
308
+
309
+ // 현재 언어의 SSR 번역 확인
310
+ const ssrTranslations =
311
+ config.initialTranslations[targetLang]?.[namespace];
312
+ if (ssrTranslations) {
313
+ const value = resolveNestedKey(
314
+ ssrTranslations as Record<string, unknown>,
315
+ actualKey,
316
+ );
289
317
  if (value !== null) {
290
318
  return value;
291
319
  }
292
320
  }
293
- }
294
321
 
295
- return null;
296
- }, [config.initialTranslations, config.fallbackLanguage, parseKey, resolveNestedKey]);
322
+ // 폴백 언어의 SSR 번역 확인
323
+ const fallbackLang = config.fallbackLanguage || "en";
324
+ if (targetLang !== fallbackLang) {
325
+ const fallbackTranslations =
326
+ config.initialTranslations[fallbackLang]?.[namespace];
327
+ if (fallbackTranslations) {
328
+ const value = resolveNestedKey(
329
+ fallbackTranslations as Record<string, unknown>,
330
+ actualKey,
331
+ );
332
+ if (value !== null) {
333
+ return value;
334
+ }
335
+ }
336
+ }
297
337
 
298
- // 기본 번역에서 찾기
299
- const findInDefaultTranslations = useCallback((key: string, targetLang: string): string | null => {
300
- const { namespace, key: actualKey } = parseKey(key);
301
- const defaultTranslations = getDefaultTranslations(targetLang, namespace);
302
- const fallbackTranslations = getDefaultTranslations(config.fallbackLanguage || 'en', namespace);
338
+ return null;
339
+ },
340
+ [
341
+ config.initialTranslations,
342
+ config.fallbackLanguage,
343
+ parseKey,
344
+ resolveNestedKey,
345
+ ],
346
+ );
303
347
 
304
- return resolveNestedKey(defaultTranslations as Record<string, unknown>, actualKey)
305
- || resolveNestedKey(fallbackTranslations as Record<string, unknown>, actualKey)
306
- || null;
307
- }, [config.fallbackLanguage, parseKey, resolveNestedKey]);
348
+ // 기본 번역에서 찾기
349
+ const findInDefaultTranslations = useCallback(
350
+ (key: string, targetLang: string): string | null => {
351
+ const { namespace, key: actualKey } = parseKey(key);
352
+ const defaultTranslations = getDefaultTranslations(targetLang, namespace);
353
+ const fallbackTranslations = getDefaultTranslations(
354
+ config.fallbackLanguage || "en",
355
+ namespace,
356
+ );
357
+
358
+ return (
359
+ resolveNestedKey(
360
+ defaultTranslations as Record<string, unknown>,
361
+ actualKey,
362
+ ) ||
363
+ resolveNestedKey(
364
+ fallbackTranslations as Record<string, unknown>,
365
+ actualKey,
366
+ ) ||
367
+ null
368
+ );
369
+ },
370
+ [config.fallbackLanguage, parseKey, resolveNestedKey],
371
+ );
308
372
 
309
373
  // hua-api 스타일의 간단한 번역 함수 (메모이제이션)
310
374
  // translationVersion과 currentLanguage에 의존하여 번역 로드 및 언어 변경 시 리렌더링 트리거
311
- const t = useCallback((key: string, paramsOrLang?: TranslationParams | string, language?: string) => {
312
- // translationVersion과 currentLanguage를 참조하여 번역 로드 및 언어 변경 시 리렌더링 트리거
313
- void translationVersion;
314
- void currentLanguage;
315
-
316
- if (!translator) {
317
- return key;
318
- }
375
+ const t = useCallback(
376
+ (
377
+ key: string,
378
+ paramsOrLang?: TranslationParams | string,
379
+ language?: string,
380
+ ) => {
381
+ // translationVersion과 currentLanguage를 참조하여 번역 로드 및 언어 변경 시 리렌더링 트리거
382
+ void translationVersion;
383
+ void currentLanguage;
384
+
385
+ if (!translator) {
386
+ return key;
387
+ }
319
388
 
320
- // 두 번째 인자 타입으로 분기
321
- let params: TranslationParams | undefined;
322
- let lang: string | undefined;
323
- if (typeof paramsOrLang === 'string') {
324
- lang = paramsOrLang;
325
- } else if (typeof paramsOrLang === 'object' && paramsOrLang !== null) {
326
- params = paramsOrLang;
327
- lang = language;
328
- }
389
+ // 두 번째 인자 타입으로 분기
390
+ let params: TranslationParams | undefined;
391
+ let lang: string | undefined;
392
+ if (typeof paramsOrLang === "string") {
393
+ lang = paramsOrLang;
394
+ } else if (typeof paramsOrLang === "object" && paramsOrLang !== null) {
395
+ params = paramsOrLang;
396
+ lang = language;
397
+ }
329
398
 
330
- const targetLang = lang || currentLanguage;
399
+ const targetLang = lang || currentLanguage;
331
400
 
332
- // 1단계: translator.translate() 시도 (params가 있으면 translate에 위임)
333
- try {
334
- const result = translator.translate(key, params || lang, params ? lang : undefined);
335
- if (result && result !== key && result !== '') {
336
- return result;
401
+ // 1단계: translator.translate() 시도 (params가 있으면 translate에 위임)
402
+ try {
403
+ const result = translator.translate(
404
+ key,
405
+ params || lang,
406
+ params ? lang : undefined,
407
+ );
408
+ if (result && result !== key && result !== "") {
409
+ return result;
410
+ }
411
+ } catch (error) {
412
+ // translator.translate() 실패 시 다음 단계로 진행
337
413
  }
338
- } catch (error) {
339
- // translator.translate() 실패 시 다음 단계로 진행
340
- }
341
414
 
342
- // interpolate 헬퍼
343
- const interpolate = (text: string) => {
344
- if (!params) return text;
345
- return text.replace(/\{\{(\w+)\}\}/g, (match, k) => {
346
- const value = params![k];
347
- return value !== undefined ? String(value) : match;
348
- });
349
- };
415
+ // interpolate 헬퍼
416
+ const interpolate = (text: string) => {
417
+ if (!params) return text;
418
+ return text.replace(/\{\{(\w+)\}\}/g, (match, k) => {
419
+ const value = params![k];
420
+ return value !== undefined ? String(value) : match;
421
+ });
422
+ };
423
+
424
+ // 2단계: SSR 번역 데이터에서 찾기
425
+ const ssrResult = findInSSRTranslations(key, targetLang);
426
+ if (ssrResult) {
427
+ return interpolate(ssrResult);
428
+ }
350
429
 
351
- // 2단계: SSR 번역 데이터에서 찾기
352
- const ssrResult = findInSSRTranslations(key, targetLang);
353
- if (ssrResult) {
354
- return interpolate(ssrResult);
355
- }
430
+ // 3단계: 기본 번역 데이터에서 찾기
431
+ const defaultResult = findInDefaultTranslations(key, targetLang);
432
+ if (defaultResult) {
433
+ return interpolate(defaultResult);
434
+ }
356
435
 
357
- // 3단계: 기본 번역 데이터에서 찾기
358
- const defaultResult = findInDefaultTranslations(key, targetLang);
359
- if (defaultResult) {
360
- return interpolate(defaultResult);
361
- }
436
+ // 모든 단계에서 번역을 찾지 못한 경우
437
+ // defaultValue가 제공된 경우 반환 (프로덕션/디버그 모두 적용)
438
+ if (
439
+ typeof paramsOrLang === "object" &&
440
+ paramsOrLang !== null &&
441
+ "defaultValue" in paramsOrLang &&
442
+ typeof paramsOrLang.defaultValue === "string"
443
+ ) {
444
+ return interpolate(paramsOrLang.defaultValue);
445
+ }
362
446
 
363
- // 모든 단계에서 번역을 찾지 못한 경우
364
- if (config.debug) {
365
- return interpolate(key); // 개발 환경에서는 키를 표시하여 디버깅 가능
366
- }
367
- return ''; // 프로덕션에서는 빈 문자열 반환하여 미싱 키 노출 방지
368
- }, [translator, config.debug, currentLanguage, config.fallbackLanguage, translationVersion, findInSSRTranslations, findInDefaultTranslations]) as (key: string, paramsOrLang?: TranslationParams | string, language?: string) => string;
447
+ if (config.debug) {
448
+ return interpolate(key); // 개발 환경에서는 키를 표시하여 디버깅 가능
449
+ }
450
+ return ""; // 프로덕션에서는 빈 문자열 반환하여 미싱 키 노출 방지
451
+ },
452
+ [
453
+ translator,
454
+ config.debug,
455
+ currentLanguage,
456
+ config.fallbackLanguage,
457
+ translationVersion,
458
+ findInSSRTranslations,
459
+ findInDefaultTranslations,
460
+ ],
461
+ ) as (
462
+ key: string,
463
+ paramsOrLang?: TranslationParams | string,
464
+ language?: string,
465
+ ) => string;
369
466
 
370
467
  // 기존 비동기 번역 함수 (하위 호환성)
371
- const tAsync = useCallback(async (key: string, params?: TranslationParams) => {
372
- if (!translator) {
373
- if (config.debug) {
374
- console.warn('Translator not initialized');
468
+ const tAsync = useCallback(
469
+ async (key: string, params?: TranslationParams) => {
470
+ if (!translator) {
471
+ if (config.debug) {
472
+ console.warn("Translator not initialized");
473
+ }
474
+ return key;
375
475
  }
376
- return key;
377
- }
378
476
 
379
- setIsLoading(true);
380
- try {
381
- const result = await translator.translateAsync(key, params);
382
- return result;
383
- } catch (error) {
384
- if (config.debug) {
385
- console.error('Translation error:', error);
477
+ setIsLoading(true);
478
+ try {
479
+ const result = await translator.translateAsync(key, params);
480
+ return result;
481
+ } catch (error) {
482
+ if (config.debug) {
483
+ console.error("Translation error:", error);
484
+ }
485
+ return key;
486
+ } finally {
487
+ setIsLoading(false);
386
488
  }
387
- return key;
388
- } finally {
389
- setIsLoading(false);
390
- }
391
- }, [translator, config.debug]);
489
+ },
490
+ [translator, config.debug],
491
+ );
392
492
 
393
493
  // 기존 동기 번역 함수 (하위 호환성)
394
- const tSync = useCallback((key: string, namespace?: string, params?: TranslationParams) => {
395
- if (!translator) {
396
- if (config.debug) {
397
- console.warn('Translator not initialized');
494
+ const tSync = useCallback(
495
+ (key: string, namespace?: string, params?: TranslationParams) => {
496
+ if (!translator) {
497
+ if (config.debug) {
498
+ console.warn("Translator not initialized");
499
+ }
500
+ return key;
398
501
  }
399
- return key;
400
- }
401
502
 
402
- return translator.translateSync(key, params);
403
- }, [translator, config.debug]);
503
+ return translator.translateSync(key, params);
504
+ },
505
+ [translator, config.debug],
506
+ );
404
507
 
405
508
  // 원시 값 가져오기 (배열, 객체 포함) — 제네릭으로 타입 캐스팅 가능
406
- const getRawValue = useCallback(<T = unknown>(key: string, language?: string): T | undefined => {
407
- if (!translator || !isInitialized) {
408
- return undefined;
409
- }
410
- return translator.getRawValue<T>(key, language);
411
- }, [translator, isInitialized]);
509
+ const getRawValue = useCallback(
510
+ <T = unknown,>(key: string, language?: string): T | undefined => {
511
+ if (!translator || !isInitialized) {
512
+ return undefined;
513
+ }
514
+ return translator.getRawValue<T>(key, language);
515
+ },
516
+ [translator, isInitialized],
517
+ );
412
518
 
413
519
  // 배열 번역 값 가져오기 (타입 안전)
414
- const tArray = useCallback((key: string, language?: string): string[] => {
415
- void translationVersion;
416
- void currentLanguage;
417
- if (!translator || !isInitialized) {
418
- return [];
419
- }
420
- return translator.tArray(key, language);
421
- }, [translator, isInitialized, translationVersion, currentLanguage]);
422
-
423
- // 복수형 번역 (ICU / Intl.PluralRules 기반)
424
- const tPlural = useCallback((key: string, count: number, params?: Record<string, unknown>, language?: string): string => {
425
- void translationVersion;
426
- void currentLanguage;
427
- if (!translator || !isInitialized) {
428
- return key;
429
- }
430
- return translator.tPlural(key, count, params, language);
431
- }, [translator, isInitialized, translationVersion, currentLanguage]);
432
-
433
- // 개발자 도구 (메모이제이션)
434
- const debug = useMemo(() => ({
435
- getCurrentLanguage: () => {
436
- try {
437
- return translator?.getCurrentLanguage() || currentLanguage;
438
- } catch {
439
- return currentLanguage;
520
+ const tArray = useCallback(
521
+ (key: string, language?: string): string[] => {
522
+ void translationVersion;
523
+ void currentLanguage;
524
+ if (!translator || !isInitialized) {
525
+ return [];
440
526
  }
527
+ return translator.tArray(key, language);
441
528
  },
442
- getSupportedLanguages: () => {
443
- try {
444
- return translator?.getSupportedLanguages() || config.supportedLanguages?.map(l => l.code) || [];
445
- } catch {
446
- return config.supportedLanguages?.map(l => l.code) || [];
529
+ [translator, isInitialized, translationVersion, currentLanguage],
530
+ );
531
+
532
+ // 복수형 번역 (ICU / Intl.PluralRules 기반)
533
+ const tPlural = useCallback(
534
+ (
535
+ key: string,
536
+ count: number,
537
+ params?: Record<string, unknown>,
538
+ language?: string,
539
+ ): string => {
540
+ void translationVersion;
541
+ void currentLanguage;
542
+ if (!translator || !isInitialized) {
543
+ return key;
447
544
  }
545
+ return translator.tPlural(key, count, params, language);
448
546
  },
449
- getLoadedNamespaces: () => {
450
- try {
451
- const debugInfo = translator?.debug();
452
- if (debugInfo && debugInfo.loadedNamespaces) {
453
- return Array.from(debugInfo.loadedNamespaces);
547
+ [translator, isInitialized, translationVersion, currentLanguage],
548
+ );
549
+
550
+ // 개발자 도구 (메모이제이션)
551
+ const debug = useMemo(
552
+ () => ({
553
+ getCurrentLanguage: () => {
554
+ try {
555
+ return translator?.getCurrentLanguage() || currentLanguage;
556
+ } catch {
557
+ return currentLanguage;
454
558
  }
455
- // 번역 데이터가 있으면 네임스페이스 추정
456
- if (debugInfo && debugInfo.allTranslations) {
457
- const namespaces = new Set<string>();
458
- Object.values(debugInfo.allTranslations).forEach((langData: unknown) => {
459
- if (langData && typeof langData === 'object') {
460
- Object.keys(langData).forEach(namespace => {
461
- namespaces.add(namespace);
462
- });
463
- }
464
- });
465
- return Array.from(namespaces);
559
+ },
560
+ getSupportedLanguages: () => {
561
+ try {
562
+ return (
563
+ translator?.getSupportedLanguages() ||
564
+ config.supportedLanguages?.map((l) => l.code) ||
565
+ []
566
+ );
567
+ } catch {
568
+ return config.supportedLanguages?.map((l) => l.code) || [];
466
569
  }
467
- return [];
468
- } catch (error) {
469
- return [];
470
- }
471
- },
472
- getAllTranslations: () => {
473
- try {
474
- return translator?.debug()?.allTranslations || {};
475
- } catch (error) {
476
- return {};
477
- }
478
- },
479
- isReady: () => {
480
- try {
481
- return translator?.isReady() || isInitialized;
482
- } catch {
483
- return isInitialized;
484
- }
485
- },
486
- getInitializationError: () => {
487
- try {
488
- return translator?.getInitializationError() || error;
489
- } catch {
490
- return error;
491
- }
492
- },
493
- clearCache: () => {
494
- try {
495
- translator?.clearCache();
496
- } catch {
497
- // 무시
498
- }
499
- },
500
- getCacheStats: () => {
501
- try {
502
- const debugInfo = translator?.debug();
503
- if (debugInfo && debugInfo.cacheStats) {
504
- return {
505
- size: debugInfo.cacheSize || 0,
506
- hits: debugInfo.cacheStats.hits || 0,
507
- misses: debugInfo.cacheStats.misses || 0
508
- };
570
+ },
571
+ getLoadedNamespaces: () => {
572
+ try {
573
+ const debugInfo = translator?.debug();
574
+ if (debugInfo && debugInfo.loadedNamespaces) {
575
+ return Array.from(debugInfo.loadedNamespaces);
576
+ }
577
+ // 번역 데이터가 있으면 네임스페이스 추정
578
+ if (debugInfo && debugInfo.allTranslations) {
579
+ const namespaces = new Set<string>();
580
+ Object.values(debugInfo.allTranslations).forEach(
581
+ (langData: unknown) => {
582
+ if (langData && typeof langData === "object") {
583
+ Object.keys(langData).forEach((namespace) => {
584
+ namespaces.add(namespace);
585
+ });
586
+ }
587
+ },
588
+ );
589
+ return Array.from(namespaces);
590
+ }
591
+ return [];
592
+ } catch (error) {
593
+ return [];
509
594
  }
510
- return { size: 0, hits: 0, misses: 0 };
511
- } catch (error) {
512
- return { size: 0, hits: 0, misses: 0 };
513
- }
514
- },
515
- reloadTranslations: async () => {
516
- if (translator) {
517
- setIsLoading(true);
518
- setError(null);
595
+ },
596
+ getAllTranslations: () => {
519
597
  try {
520
- await translator.initialize();
521
- } catch (err) {
522
- setError(err as TranslationError);
523
- } finally {
524
- setIsLoading(false);
598
+ return translator?.debug()?.allTranslations || {};
599
+ } catch (error) {
600
+ return {};
525
601
  }
526
- }
527
- },
528
- }), [translator, currentLanguage, error, isInitialized, config.supportedLanguages]);
529
-
530
- const value: I18nContextType = useMemo(() => ({
531
- currentLanguage,
532
- setLanguage,
533
- t,
534
- tPlural,
535
- tArray,
536
- tAsync,
537
- tSync,
538
- getRawValue,
539
- isLoading,
540
- error,
541
- supportedLanguages: config.supportedLanguages,
542
- debug,
543
- isInitialized,
544
- }), [currentLanguage, setLanguage, t, tPlural, tArray, tAsync, tSync, getRawValue, isLoading, error, config.supportedLanguages, debug, isInitialized]);
545
-
546
- return (
547
- <I18nContext.Provider value={value}>
548
- {children}
549
- </I18nContext.Provider>
602
+ },
603
+ isReady: () => {
604
+ try {
605
+ return translator?.isReady() || isInitialized;
606
+ } catch {
607
+ return isInitialized;
608
+ }
609
+ },
610
+ getInitializationError: () => {
611
+ try {
612
+ return translator?.getInitializationError() || error;
613
+ } catch {
614
+ return error;
615
+ }
616
+ },
617
+ clearCache: () => {
618
+ try {
619
+ translator?.clearCache();
620
+ } catch {
621
+ // 무시
622
+ }
623
+ },
624
+ getCacheStats: () => {
625
+ try {
626
+ const debugInfo = translator?.debug();
627
+ if (debugInfo && debugInfo.cacheStats) {
628
+ return {
629
+ size: debugInfo.cacheSize || 0,
630
+ hits: debugInfo.cacheStats.hits || 0,
631
+ misses: debugInfo.cacheStats.misses || 0,
632
+ };
633
+ }
634
+ return { size: 0, hits: 0, misses: 0 };
635
+ } catch (error) {
636
+ return { size: 0, hits: 0, misses: 0 };
637
+ }
638
+ },
639
+ reloadTranslations: async () => {
640
+ if (translator) {
641
+ setIsLoading(true);
642
+ setError(null);
643
+ try {
644
+ await translator.initialize();
645
+ } catch (err) {
646
+ setError(err as TranslationError);
647
+ } finally {
648
+ setIsLoading(false);
649
+ }
650
+ }
651
+ },
652
+ }),
653
+ [
654
+ translator,
655
+ currentLanguage,
656
+ error,
657
+ isInitialized,
658
+ config.supportedLanguages,
659
+ ],
660
+ );
661
+
662
+ const value: I18nContextType = useMemo(
663
+ () => ({
664
+ currentLanguage,
665
+ setLanguage,
666
+ t,
667
+ tPlural,
668
+ tArray,
669
+ tAsync,
670
+ tSync,
671
+ getRawValue,
672
+ isLoading,
673
+ error,
674
+ supportedLanguages: config.supportedLanguages,
675
+ debug,
676
+ isInitialized,
677
+ }),
678
+ [
679
+ currentLanguage,
680
+ setLanguage,
681
+ t,
682
+ tPlural,
683
+ tArray,
684
+ tAsync,
685
+ tSync,
686
+ getRawValue,
687
+ isLoading,
688
+ error,
689
+ config.supportedLanguages,
690
+ debug,
691
+ isInitialized,
692
+ ],
550
693
  );
694
+
695
+ return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
551
696
  }
552
697
 
553
698
  /**
@@ -558,24 +703,34 @@ export function useI18n(): I18nContextType {
558
703
  if (!context) {
559
704
  // Provider 밖에서 호출되면 기본값 반환
560
705
  return {
561
- currentLanguage: 'ko',
706
+ currentLanguage: "ko",
562
707
  setLanguage: () => {},
563
- t: (key: string) => key,
708
+ t: (key: string, paramsOrLang?: TranslationParams | string) => {
709
+ if (
710
+ typeof paramsOrLang === "object" &&
711
+ paramsOrLang !== null &&
712
+ "defaultValue" in paramsOrLang &&
713
+ typeof paramsOrLang.defaultValue === "string"
714
+ ) {
715
+ return paramsOrLang.defaultValue;
716
+ }
717
+ return key;
718
+ },
564
719
  tPlural: (key: string) => key,
565
720
  tAsync: async (key: string) => key,
566
721
  tSync: (key: string) => key,
567
- getRawValue: <T = unknown>() => undefined as T | undefined,
722
+ getRawValue: <T = unknown,>() => undefined as T | undefined,
568
723
  tArray: () => [],
569
724
  isLoading: false,
570
725
  error: null,
571
726
  supportedLanguages: [
572
- { code: 'ko', name: 'Korean', nativeName: '한국어' },
573
- { code: 'en', name: 'English', nativeName: 'English' },
727
+ { code: "ko", name: "Korean", nativeName: "한국어" },
728
+ { code: "en", name: "English", nativeName: "English" },
574
729
  ],
575
730
  isInitialized: false,
576
731
  debug: {
577
- getCurrentLanguage: () => 'ko',
578
- getSupportedLanguages: () => ['ko', 'en'],
732
+ getCurrentLanguage: () => "ko",
733
+ getSupportedLanguages: () => ["ko", "en"],
579
734
  getLoadedNamespaces: () => [],
580
735
  getAllTranslations: () => ({}),
581
736
  isReady: () => false,
@@ -588,5 +743,3 @@ export function useI18n(): I18nContextType {
588
743
  }
589
744
  return context;
590
745
  }
591
-
592
-