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