@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/types/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Plural (ICU / Intl.PluralRules)
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
export type PluralCategory =
|
|
4
|
+
export type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other";
|
|
5
5
|
|
|
6
6
|
export interface PluralValue {
|
|
7
7
|
zero?: string;
|
|
@@ -12,20 +12,95 @@ export interface PluralValue {
|
|
|
12
12
|
other: string; // 필수
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const PLURAL_CATEGORIES = new Set<string>([
|
|
15
|
+
const PLURAL_CATEGORIES = new Set<string>([
|
|
16
|
+
"zero",
|
|
17
|
+
"one",
|
|
18
|
+
"two",
|
|
19
|
+
"few",
|
|
20
|
+
"many",
|
|
21
|
+
"other",
|
|
22
|
+
]);
|
|
16
23
|
|
|
17
24
|
export function isPluralValue(value: unknown): value is PluralValue {
|
|
18
|
-
if (typeof value !==
|
|
25
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
26
|
+
return false;
|
|
19
27
|
const obj = value as Record<string, unknown>;
|
|
20
28
|
const keys = Object.keys(obj);
|
|
21
29
|
return (
|
|
22
30
|
keys.length > 0 &&
|
|
23
|
-
keys.every(k => PLURAL_CATEGORIES.has(k)) &&
|
|
24
|
-
Object.values(obj).every(v => typeof v ===
|
|
25
|
-
typeof obj.other ===
|
|
31
|
+
keys.every((k) => PLURAL_CATEGORIES.has(k)) &&
|
|
32
|
+
Object.values(obj).every((v) => typeof v === "string") &&
|
|
33
|
+
typeof obj.other === "string"
|
|
26
34
|
);
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Platform Adapter (cross-platform: Web / React Native / Flutter bridge)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 플랫폼별 i18n 어댑터 인터페이스.
|
|
43
|
+
*
|
|
44
|
+
* Web(기본), React Native, Flutter 브릿지 등 환경에 맞는 구현체를 주입.
|
|
45
|
+
* I18nConfig.platformAdapter로 전달하면 useI18n이 자동으로 사용한다.
|
|
46
|
+
*/
|
|
47
|
+
export interface I18nPlatformAdapter {
|
|
48
|
+
/** 디바이스/브라우저 언어 코드 (e.g. 'ko', 'en'). 감지 불가 시 undefined */
|
|
49
|
+
getDeviceLanguage(): string | undefined;
|
|
50
|
+
|
|
51
|
+
/** 시스템 언어 변경 이벤트 구독. 해제 함수 반환 */
|
|
52
|
+
onLanguageChange(cb: (lang: string) => void): () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Web 기본 어댑터.
|
|
57
|
+
* navigator.language + CustomEvent 기반. SSR에서는 안전하게 no-op.
|
|
58
|
+
*/
|
|
59
|
+
export const webPlatformAdapter: I18nPlatformAdapter = {
|
|
60
|
+
getDeviceLanguage() {
|
|
61
|
+
if (
|
|
62
|
+
typeof globalThis !== "undefined" &&
|
|
63
|
+
typeof navigator !== "undefined" &&
|
|
64
|
+
navigator.language
|
|
65
|
+
) {
|
|
66
|
+
return navigator.language.slice(0, 2).toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
},
|
|
70
|
+
onLanguageChange(cb) {
|
|
71
|
+
if (
|
|
72
|
+
typeof globalThis === "undefined" ||
|
|
73
|
+
typeof window === "undefined" ||
|
|
74
|
+
typeof CustomEvent === "undefined"
|
|
75
|
+
) {
|
|
76
|
+
return () => {};
|
|
77
|
+
}
|
|
78
|
+
const handler = (e: Event) => {
|
|
79
|
+
const lang = (e as CustomEvent).detail;
|
|
80
|
+
if (typeof lang === "string") cb(lang);
|
|
81
|
+
};
|
|
82
|
+
window.addEventListener("huaI18nLanguageChange", handler);
|
|
83
|
+
window.addEventListener("i18nLanguageChanged", handler);
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener("huaI18nLanguageChange", handler);
|
|
86
|
+
window.removeEventListener("i18nLanguageChanged", handler);
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Headless 어댑터 (SSR / 테스트 / Flutter 브릿지용).
|
|
93
|
+
* 언어 감지·이벤트 없이 config.defaultLanguage만 사용.
|
|
94
|
+
*/
|
|
95
|
+
export const headlessPlatformAdapter: I18nPlatformAdapter = {
|
|
96
|
+
getDeviceLanguage() {
|
|
97
|
+
return undefined;
|
|
98
|
+
},
|
|
99
|
+
onLanguageChange() {
|
|
100
|
+
return () => {};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
29
104
|
// ---------------------------------------------------------------------------
|
|
30
105
|
// Translation namespace
|
|
31
106
|
// ---------------------------------------------------------------------------
|
|
@@ -41,8 +116,15 @@ export interface LanguageConfig {
|
|
|
41
116
|
code: string;
|
|
42
117
|
name: string;
|
|
43
118
|
nativeName: string;
|
|
44
|
-
tone?:
|
|
45
|
-
|
|
119
|
+
tone?:
|
|
120
|
+
| "emotional"
|
|
121
|
+
| "encouraging"
|
|
122
|
+
| "calm"
|
|
123
|
+
| "gentle"
|
|
124
|
+
| "formal"
|
|
125
|
+
| "technical"
|
|
126
|
+
| "informal";
|
|
127
|
+
formality?: "informal" | "casual" | "formal" | "polite";
|
|
46
128
|
}
|
|
47
129
|
|
|
48
130
|
// 더 구체적인 설정 타입 정의
|
|
@@ -51,13 +133,20 @@ export interface I18nConfig {
|
|
|
51
133
|
fallbackLanguage?: string;
|
|
52
134
|
supportedLanguages: LanguageConfig[];
|
|
53
135
|
namespaces?: string[];
|
|
54
|
-
loadTranslations: (
|
|
136
|
+
loadTranslations: (
|
|
137
|
+
language: string,
|
|
138
|
+
namespace: string,
|
|
139
|
+
) => Promise<TranslationNamespace>;
|
|
55
140
|
// SSR에서 전달된 초기 번역 데이터 (네트워크 요청 없이 사용)
|
|
56
141
|
initialTranslations?: Record<string, Record<string, TranslationNamespace>>;
|
|
57
142
|
// 개발 모드 설정
|
|
58
143
|
debug?: boolean;
|
|
59
144
|
// 번역 키가 없을 때의 동작
|
|
60
|
-
missingKeyHandler?: (
|
|
145
|
+
missingKeyHandler?: (
|
|
146
|
+
key: string,
|
|
147
|
+
language: string,
|
|
148
|
+
namespace: string,
|
|
149
|
+
) => string;
|
|
61
150
|
// 번역 로딩 실패 시 동작
|
|
62
151
|
errorHandler?: (error: Error, language: string, namespace: string) => void;
|
|
63
152
|
// 캐시 설정
|
|
@@ -79,11 +168,23 @@ export interface I18nConfig {
|
|
|
79
168
|
};
|
|
80
169
|
// 자동 언어 전환 이벤트 처리 (withDefaultConfig용)
|
|
81
170
|
autoLanguageSync?: boolean;
|
|
171
|
+
// 플랫폼 어댑터 (기본: webPlatformAdapter)
|
|
172
|
+
platformAdapter?: I18nPlatformAdapter;
|
|
82
173
|
}
|
|
83
174
|
|
|
84
175
|
// 에러 타입 정의
|
|
85
176
|
export interface TranslationError extends Error {
|
|
86
|
-
code:
|
|
177
|
+
code:
|
|
178
|
+
| "MISSING_KEY"
|
|
179
|
+
| "LOAD_FAILED"
|
|
180
|
+
| "INVALID_KEY"
|
|
181
|
+
| "NETWORK_ERROR"
|
|
182
|
+
| "INITIALIZATION_ERROR"
|
|
183
|
+
| "VALIDATION_ERROR"
|
|
184
|
+
| "CACHE_ERROR"
|
|
185
|
+
| "FALLBACK_LOAD_FAILED"
|
|
186
|
+
| "INITIALIZATION_FAILED"
|
|
187
|
+
| "RETRY_FAILED";
|
|
87
188
|
language?: string;
|
|
88
189
|
namespace?: string;
|
|
89
190
|
key?: string;
|
|
@@ -107,7 +208,7 @@ export interface ErrorRecoveryStrategy {
|
|
|
107
208
|
// 에러 로깅 설정
|
|
108
209
|
export interface ErrorLoggingConfig {
|
|
109
210
|
enabled: boolean;
|
|
110
|
-
level:
|
|
211
|
+
level: "error" | "warn" | "info" | "debug";
|
|
111
212
|
includeStack: boolean;
|
|
112
213
|
includeContext: boolean;
|
|
113
214
|
customLogger?: (error: TranslationError) => void;
|
|
@@ -119,7 +220,7 @@ export interface UserFriendlyError {
|
|
|
119
220
|
message: string;
|
|
120
221
|
suggestion?: string;
|
|
121
222
|
action?: string;
|
|
122
|
-
severity:
|
|
223
|
+
severity: "low" | "medium" | "high" | "critical";
|
|
123
224
|
}
|
|
124
225
|
|
|
125
226
|
// 캐시 엔트리 타입
|
|
@@ -153,15 +254,28 @@ export interface I18nContextType {
|
|
|
153
254
|
currentLanguage: string;
|
|
154
255
|
setLanguage: (language: string) => void | Promise<void>;
|
|
155
256
|
// 통합 번역 함수: t(key), t(key, language), t(key, params), t(key, params, language)
|
|
156
|
-
t: (
|
|
257
|
+
t: (
|
|
258
|
+
key: ResolveStringKey,
|
|
259
|
+
paramsOrLang?: TranslationParams | string,
|
|
260
|
+
language?: string,
|
|
261
|
+
) => string;
|
|
157
262
|
// 복수형 번역 함수: tPlural(key, count), tPlural(key, count, params), tPlural(key, count, params, language)
|
|
158
|
-
tPlural: (
|
|
159
|
-
|
|
263
|
+
tPlural: (
|
|
264
|
+
key: ResolvePluralKey,
|
|
265
|
+
count: number,
|
|
266
|
+
params?: Record<string, unknown>,
|
|
267
|
+
language?: string,
|
|
268
|
+
) => string;
|
|
269
|
+
/** @deprecated Use `t()` instead. Will be removed in a future major version. */
|
|
160
270
|
tAsync: (key: string, params?: TranslationParams) => Promise<string>;
|
|
161
|
-
|
|
162
|
-
tSync: (
|
|
163
|
-
|
|
164
|
-
|
|
271
|
+
/** @deprecated Use `t()` instead. Will be removed in a future major version. */
|
|
272
|
+
tSync: (
|
|
273
|
+
key: string,
|
|
274
|
+
namespace?: string,
|
|
275
|
+
params?: TranslationParams,
|
|
276
|
+
) => string;
|
|
277
|
+
// 원시 값 가져오기 (배열, 객체 포함) — 제네릭으로 타입 캐스팅 가능
|
|
278
|
+
getRawValue: <T = unknown>(key: string, language?: string) => T | undefined;
|
|
165
279
|
// 배열 번역 값 가져오기 (타입 안전)
|
|
166
280
|
tArray: (key: ResolveArrayKey, language?: string) => string[];
|
|
167
281
|
isLoading: boolean;
|
|
@@ -187,6 +301,18 @@ export interface I18nContextType {
|
|
|
187
301
|
};
|
|
188
302
|
}
|
|
189
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Parameters for the `t()` function.
|
|
306
|
+
*
|
|
307
|
+
* `defaultValue` is a reserved key: when translation lookup fails entirely,
|
|
308
|
+
* the string provided here is returned instead of empty string (production)
|
|
309
|
+
* or the raw key (debug). Interpolation variables still work:
|
|
310
|
+
*
|
|
311
|
+
* ```ts
|
|
312
|
+
* t('missing:key', { defaultValue: 'Hello {{name}}', name: 'World' })
|
|
313
|
+
* // → 'Hello World'
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
190
316
|
export interface TranslationParams {
|
|
191
317
|
[key: string]: string | number;
|
|
192
318
|
}
|
|
@@ -206,115 +332,162 @@ export interface TranslationParams {
|
|
|
206
332
|
*
|
|
207
333
|
* augmentation이 없으면 string으로 fallback (breaking 없음).
|
|
208
334
|
*/
|
|
209
|
-
|
|
335
|
+
|
|
210
336
|
export interface TypedTranslationKeys {}
|
|
211
337
|
|
|
212
338
|
/** augmentation 시 좁혀진 타입, 미설정 시 string */
|
|
213
|
-
export type ResolveStringKey = TypedTranslationKeys extends {
|
|
214
|
-
|
|
215
|
-
|
|
339
|
+
export type ResolveStringKey = TypedTranslationKeys extends {
|
|
340
|
+
stringKey: infer K;
|
|
341
|
+
}
|
|
342
|
+
? K & string
|
|
343
|
+
: string;
|
|
344
|
+
export type ResolveArrayKey = TypedTranslationKeys extends { arrayKey: infer K }
|
|
345
|
+
? K & string
|
|
346
|
+
: string;
|
|
347
|
+
export type ResolvePluralKey = TypedTranslationKeys extends {
|
|
348
|
+
pluralKey: infer K;
|
|
349
|
+
}
|
|
350
|
+
? K & string
|
|
351
|
+
: string;
|
|
216
352
|
|
|
217
353
|
// 타입 안전한 번역 키 시스템 (단순화된 버전)
|
|
218
|
-
export type TranslationKey<T> =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
354
|
+
export type TranslationKey<T> =
|
|
355
|
+
T extends Record<string, unknown>
|
|
356
|
+
? {
|
|
357
|
+
[K in keyof T]: T[K] extends string
|
|
358
|
+
? K
|
|
359
|
+
: T[K] extends Record<string, unknown>
|
|
360
|
+
? `${K & string}.${TranslationKey<T[K]> & string}`
|
|
361
|
+
: never;
|
|
362
|
+
}[keyof T]
|
|
363
|
+
: never;
|
|
227
364
|
|
|
228
365
|
// 타입 안전한 번역 함수들
|
|
229
|
-
export interface TypedI18nContextType<T extends TranslationData> extends Omit<
|
|
366
|
+
export interface TypedI18nContextType<T extends TranslationData> extends Omit<
|
|
367
|
+
I18nContextType,
|
|
368
|
+
"t" | "tSync"
|
|
369
|
+
> {
|
|
230
370
|
// 타입 안전한 번역 함수
|
|
231
|
-
t: <K extends TranslationKey<T>>(
|
|
232
|
-
|
|
371
|
+
t: <K extends TranslationKey<T>>(
|
|
372
|
+
key: K,
|
|
373
|
+
paramsOrLang?: TranslationParams | string,
|
|
374
|
+
language?: string,
|
|
375
|
+
) => string;
|
|
376
|
+
tSync: <K extends TranslationKey<T>>(
|
|
377
|
+
key: K,
|
|
378
|
+
namespace?: string,
|
|
379
|
+
params?: TranslationParams,
|
|
380
|
+
) => string;
|
|
233
381
|
}
|
|
234
382
|
|
|
235
383
|
// 간단한 번역 키 타입 (무한 재귀 방지)
|
|
236
384
|
export type SimpleTranslationKey = string;
|
|
237
385
|
|
|
238
386
|
// 고급 번역 키 타입 (제한된 깊이)
|
|
239
|
-
export type TranslationKeyLegacy<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
387
|
+
export type TranslationKeyLegacy<
|
|
388
|
+
T extends Record<string, unknown>,
|
|
389
|
+
D extends number = 3,
|
|
390
|
+
> = [D] extends [never]
|
|
391
|
+
? never
|
|
392
|
+
: T extends Record<string, unknown>
|
|
393
|
+
? {
|
|
394
|
+
[K in keyof T]: T[K] extends string
|
|
395
|
+
? K
|
|
396
|
+
: T[K] extends Record<string, unknown>
|
|
397
|
+
? `${K & string}.${TranslationKeyLegacy<T[K], Prev<D>> & string}`
|
|
398
|
+
: never;
|
|
399
|
+
}[keyof T]
|
|
400
|
+
: never;
|
|
401
|
+
|
|
402
|
+
type Prev<T extends number> = T extends 0
|
|
403
|
+
? never
|
|
404
|
+
: T extends 1
|
|
405
|
+
? 0
|
|
406
|
+
: T extends 2
|
|
407
|
+
? 1
|
|
408
|
+
: T extends 3
|
|
409
|
+
? 2
|
|
410
|
+
: never;
|
|
253
411
|
|
|
254
412
|
// 유틸리티 타입들
|
|
255
|
-
export type ExtractTranslationKeys<T> =
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
413
|
+
export type ExtractTranslationKeys<T> =
|
|
414
|
+
T extends Record<string, unknown>
|
|
415
|
+
? {
|
|
416
|
+
[K in keyof T]: T[K] extends string
|
|
417
|
+
? K
|
|
418
|
+
: T[K] extends Record<string, unknown>
|
|
419
|
+
? `${K & string}.${ExtractTranslationKeys<T[K]> & string}`
|
|
420
|
+
: never;
|
|
421
|
+
}[keyof T]
|
|
422
|
+
: never;
|
|
264
423
|
|
|
265
424
|
// 네임스페이스별 타입 정의를 위한 헬퍼
|
|
266
|
-
export type NamespaceKeys<
|
|
425
|
+
export type NamespaceKeys<
|
|
426
|
+
T extends TranslationData,
|
|
427
|
+
N extends keyof T,
|
|
428
|
+
> = ExtractTranslationKeys<T[N]>;
|
|
267
429
|
|
|
268
430
|
// 타입 안전한 번역 키 생성 헬퍼
|
|
269
|
-
export const createTranslationKey = <
|
|
431
|
+
export const createTranslationKey = <
|
|
432
|
+
T extends TranslationData,
|
|
433
|
+
N extends keyof T,
|
|
434
|
+
K extends NamespaceKeys<T, N>,
|
|
435
|
+
>(
|
|
270
436
|
namespace: N,
|
|
271
|
-
key: K
|
|
272
|
-
): `${N & string}.${K & string}` =>
|
|
437
|
+
key: K,
|
|
438
|
+
): `${N & string}.${K & string}` =>
|
|
439
|
+
`${String(namespace)}.${String(key)}` as `${N & string}.${K & string}`;
|
|
273
440
|
|
|
274
441
|
// 타입 가드 함수들
|
|
275
|
-
export function isTranslationNamespace(
|
|
276
|
-
|
|
442
|
+
export function isTranslationNamespace(
|
|
443
|
+
value: unknown,
|
|
444
|
+
): value is TranslationNamespace {
|
|
445
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
277
446
|
}
|
|
278
447
|
|
|
279
448
|
export function isLanguageConfig(value: unknown): value is LanguageConfig {
|
|
280
449
|
return (
|
|
281
|
-
typeof value ===
|
|
450
|
+
typeof value === "object" &&
|
|
282
451
|
value !== null &&
|
|
283
|
-
typeof (value as LanguageConfig).code ===
|
|
284
|
-
typeof (value as LanguageConfig).name ===
|
|
285
|
-
typeof (value as LanguageConfig).nativeName ===
|
|
452
|
+
typeof (value as LanguageConfig).code === "string" &&
|
|
453
|
+
typeof (value as LanguageConfig).name === "string" &&
|
|
454
|
+
typeof (value as LanguageConfig).nativeName === "string"
|
|
286
455
|
);
|
|
287
456
|
}
|
|
288
457
|
|
|
289
458
|
export function isTranslationError(value: unknown): value is TranslationError {
|
|
290
459
|
return (
|
|
291
460
|
value instanceof Error &&
|
|
292
|
-
typeof (value as TranslationError).code ===
|
|
293
|
-
[
|
|
294
|
-
|
|
295
|
-
|
|
461
|
+
typeof (value as TranslationError).code === "string" &&
|
|
462
|
+
[
|
|
463
|
+
"MISSING_KEY",
|
|
464
|
+
"LOAD_FAILED",
|
|
465
|
+
"INVALID_KEY",
|
|
466
|
+
"NETWORK_ERROR",
|
|
467
|
+
"INITIALIZATION_ERROR",
|
|
468
|
+
].includes((value as TranslationError).code)
|
|
296
469
|
);
|
|
297
470
|
}
|
|
298
471
|
|
|
299
472
|
// 설정 검증 함수
|
|
300
473
|
export function validateI18nConfig(config: unknown): config is I18nConfig {
|
|
301
|
-
if (!config || typeof config !==
|
|
474
|
+
if (!config || typeof config !== "object") {
|
|
302
475
|
return false;
|
|
303
476
|
}
|
|
304
477
|
|
|
305
478
|
const c = config as I18nConfig;
|
|
306
|
-
|
|
479
|
+
|
|
307
480
|
return (
|
|
308
|
-
typeof c.defaultLanguage ===
|
|
481
|
+
typeof c.defaultLanguage === "string" &&
|
|
309
482
|
Array.isArray(c.supportedLanguages) &&
|
|
310
483
|
c.supportedLanguages.every(isLanguageConfig) &&
|
|
311
|
-
typeof c.loadTranslations ===
|
|
484
|
+
typeof c.loadTranslations === "function"
|
|
312
485
|
);
|
|
313
486
|
}
|
|
314
487
|
|
|
315
488
|
// 에러 처리 유틸리티 함수들
|
|
316
489
|
export function createTranslationError(
|
|
317
|
-
code: TranslationError[
|
|
490
|
+
code: TranslationError["code"],
|
|
318
491
|
message: string,
|
|
319
492
|
originalError?: Error,
|
|
320
493
|
context?: {
|
|
@@ -323,7 +496,7 @@ export function createTranslationError(
|
|
|
323
496
|
key?: string;
|
|
324
497
|
retryCount?: number;
|
|
325
498
|
maxRetries?: number;
|
|
326
|
-
}
|
|
499
|
+
},
|
|
327
500
|
): TranslationError {
|
|
328
501
|
const error = new Error(message) as TranslationError;
|
|
329
502
|
error.code = code;
|
|
@@ -334,83 +507,85 @@ export function createTranslationError(
|
|
|
334
507
|
error.retryCount = context?.retryCount || 0;
|
|
335
508
|
error.maxRetries = context?.maxRetries || 3;
|
|
336
509
|
error.timestamp = Date.now();
|
|
337
|
-
error.name =
|
|
510
|
+
error.name = "TranslationError";
|
|
338
511
|
return error;
|
|
339
512
|
}
|
|
340
513
|
|
|
341
514
|
// 사용자 친화적 에러 메시지 생성
|
|
342
|
-
export function createUserFriendlyError(
|
|
343
|
-
|
|
515
|
+
export function createUserFriendlyError(
|
|
516
|
+
error: TranslationError,
|
|
517
|
+
): UserFriendlyError {
|
|
518
|
+
const errorMessages: Record<TranslationError["code"], UserFriendlyError> = {
|
|
344
519
|
MISSING_KEY: {
|
|
345
|
-
code:
|
|
346
|
-
message:
|
|
347
|
-
suggestion:
|
|
348
|
-
action:
|
|
349
|
-
severity:
|
|
520
|
+
code: "MISSING_KEY",
|
|
521
|
+
message: "번역 키를 찾을 수 없습니다",
|
|
522
|
+
suggestion: "번역 파일에 해당 키가 있는지 확인해주세요",
|
|
523
|
+
action: "번역 파일 업데이트",
|
|
524
|
+
severity: "low",
|
|
350
525
|
},
|
|
351
526
|
LOAD_FAILED: {
|
|
352
|
-
code:
|
|
353
|
-
message:
|
|
354
|
-
suggestion:
|
|
355
|
-
action:
|
|
356
|
-
severity:
|
|
527
|
+
code: "LOAD_FAILED",
|
|
528
|
+
message: "번역 파일을 불러오는데 실패했습니다",
|
|
529
|
+
suggestion: "네트워크 연결과 파일 경로를 확인해주세요",
|
|
530
|
+
action: "재시도",
|
|
531
|
+
severity: "medium",
|
|
357
532
|
},
|
|
358
533
|
INVALID_KEY: {
|
|
359
|
-
code:
|
|
360
|
-
message:
|
|
534
|
+
code: "INVALID_KEY",
|
|
535
|
+
message: "잘못된 번역 키 형식입니다",
|
|
361
536
|
suggestion: '키 형식을 "namespace.key" 형태로 입력해주세요',
|
|
362
|
-
action:
|
|
363
|
-
severity:
|
|
537
|
+
action: "키 형식 수정",
|
|
538
|
+
severity: "low",
|
|
364
539
|
},
|
|
365
540
|
NETWORK_ERROR: {
|
|
366
|
-
code:
|
|
367
|
-
message:
|
|
368
|
-
suggestion:
|
|
369
|
-
action:
|
|
370
|
-
severity:
|
|
541
|
+
code: "NETWORK_ERROR",
|
|
542
|
+
message: "네트워크 오류가 발생했습니다",
|
|
543
|
+
suggestion: "인터넷 연결을 확인하고 다시 시도해주세요",
|
|
544
|
+
action: "재시도",
|
|
545
|
+
severity: "high",
|
|
371
546
|
},
|
|
372
547
|
INITIALIZATION_ERROR: {
|
|
373
|
-
code:
|
|
374
|
-
message:
|
|
375
|
-
suggestion:
|
|
376
|
-
action:
|
|
377
|
-
severity:
|
|
548
|
+
code: "INITIALIZATION_ERROR",
|
|
549
|
+
message: "번역 시스템 초기화에 실패했습니다",
|
|
550
|
+
suggestion: "설정을 확인하고 페이지를 새로고침해주세요",
|
|
551
|
+
action: "페이지 새로고침",
|
|
552
|
+
severity: "critical",
|
|
378
553
|
},
|
|
379
554
|
VALIDATION_ERROR: {
|
|
380
|
-
code:
|
|
381
|
-
message:
|
|
382
|
-
suggestion:
|
|
383
|
-
action:
|
|
384
|
-
severity:
|
|
555
|
+
code: "VALIDATION_ERROR",
|
|
556
|
+
message: "설정 검증에 실패했습니다",
|
|
557
|
+
suggestion: "번역 설정을 확인해주세요",
|
|
558
|
+
action: "설정 수정",
|
|
559
|
+
severity: "medium",
|
|
385
560
|
},
|
|
386
561
|
CACHE_ERROR: {
|
|
387
|
-
code:
|
|
388
|
-
message:
|
|
389
|
-
suggestion:
|
|
390
|
-
action:
|
|
391
|
-
severity:
|
|
562
|
+
code: "CACHE_ERROR",
|
|
563
|
+
message: "캐시 처리 중 오류가 발생했습니다",
|
|
564
|
+
suggestion: "캐시를 초기화하고 다시 시도해주세요",
|
|
565
|
+
action: "캐시 초기화",
|
|
566
|
+
severity: "low",
|
|
392
567
|
},
|
|
393
568
|
FALLBACK_LOAD_FAILED: {
|
|
394
|
-
code:
|
|
395
|
-
message:
|
|
396
|
-
suggestion:
|
|
397
|
-
action:
|
|
398
|
-
severity:
|
|
569
|
+
code: "FALLBACK_LOAD_FAILED",
|
|
570
|
+
message: "폴백 언어 로딩에 실패했습니다",
|
|
571
|
+
suggestion: "폴백 언어 파일을 확인해주세요",
|
|
572
|
+
action: "폴백 언어 파일 수정",
|
|
573
|
+
severity: "medium",
|
|
399
574
|
},
|
|
400
575
|
INITIALIZATION_FAILED: {
|
|
401
|
-
code:
|
|
402
|
-
message:
|
|
403
|
-
suggestion:
|
|
404
|
-
action:
|
|
405
|
-
severity:
|
|
576
|
+
code: "INITIALIZATION_FAILED",
|
|
577
|
+
message: "초기화에 실패했습니다",
|
|
578
|
+
suggestion: "설정을 확인하고 다시 시도해주세요",
|
|
579
|
+
action: "설정 확인",
|
|
580
|
+
severity: "critical",
|
|
406
581
|
},
|
|
407
582
|
RETRY_FAILED: {
|
|
408
|
-
code:
|
|
409
|
-
message:
|
|
410
|
-
suggestion:
|
|
411
|
-
action:
|
|
412
|
-
severity:
|
|
413
|
-
}
|
|
583
|
+
code: "RETRY_FAILED",
|
|
584
|
+
message: "재시도에 실패했습니다",
|
|
585
|
+
suggestion: "네트워크 연결을 확인해주세요",
|
|
586
|
+
action: "재시도",
|
|
587
|
+
severity: "high",
|
|
588
|
+
},
|
|
414
589
|
};
|
|
415
590
|
|
|
416
591
|
return errorMessages[error.code];
|
|
@@ -418,14 +593,16 @@ export function createUserFriendlyError(error: TranslationError): UserFriendlyEr
|
|
|
418
593
|
|
|
419
594
|
// 에러 복구 가능 여부 확인
|
|
420
595
|
export function isRecoverableError(error: TranslationError): boolean {
|
|
421
|
-
const recoverableCodes: TranslationError[
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
596
|
+
const recoverableCodes: TranslationError["code"][] = [
|
|
597
|
+
"LOAD_FAILED",
|
|
598
|
+
"NETWORK_ERROR",
|
|
599
|
+
"CACHE_ERROR",
|
|
425
600
|
];
|
|
426
|
-
|
|
427
|
-
return
|
|
428
|
-
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
recoverableCodes.includes(error.code) &&
|
|
604
|
+
(error.retryCount || 0) < (error.maxRetries || 3)
|
|
605
|
+
);
|
|
429
606
|
}
|
|
430
607
|
|
|
431
608
|
// 기본 에러 복구 전략
|
|
@@ -435,26 +612,33 @@ export const defaultErrorRecoveryStrategy: ErrorRecoveryStrategy = {
|
|
|
435
612
|
backoffMultiplier: 2,
|
|
436
613
|
shouldRetry: isRecoverableError,
|
|
437
614
|
onRetry: (error: TranslationError, attempt: number) => {
|
|
438
|
-
if (process.env.NODE_ENV !==
|
|
615
|
+
if (process.env.NODE_ENV !== "production")
|
|
616
|
+
console.warn(
|
|
617
|
+
`Retrying translation operation (attempt ${attempt}/${error.maxRetries}):`,
|
|
618
|
+
error.message,
|
|
619
|
+
);
|
|
439
620
|
},
|
|
440
621
|
onMaxRetriesExceeded: (error: TranslationError) => {
|
|
441
|
-
console.error(
|
|
442
|
-
|
|
622
|
+
console.error(
|
|
623
|
+
"Max retries exceeded for translation operation:",
|
|
624
|
+
error.message,
|
|
625
|
+
);
|
|
626
|
+
},
|
|
443
627
|
};
|
|
444
628
|
|
|
445
629
|
// 기본 에러 로깅 설정
|
|
446
630
|
export const defaultErrorLoggingConfig: ErrorLoggingConfig = {
|
|
447
631
|
enabled: true,
|
|
448
|
-
level:
|
|
632
|
+
level: "error",
|
|
449
633
|
includeStack: true,
|
|
450
634
|
includeContext: true,
|
|
451
|
-
customLogger: undefined
|
|
635
|
+
customLogger: undefined,
|
|
452
636
|
};
|
|
453
637
|
|
|
454
638
|
// 에러 로깅 함수
|
|
455
639
|
export function logTranslationError(
|
|
456
|
-
error: TranslationError,
|
|
457
|
-
config: ErrorLoggingConfig = defaultErrorLoggingConfig
|
|
640
|
+
error: TranslationError,
|
|
641
|
+
config: ErrorLoggingConfig = defaultErrorLoggingConfig,
|
|
458
642
|
): void {
|
|
459
643
|
if (!config.enabled) return;
|
|
460
644
|
|
|
@@ -463,7 +647,7 @@ export function logTranslationError(
|
|
|
463
647
|
message: error.message,
|
|
464
648
|
timestamp: error.timestamp,
|
|
465
649
|
retryCount: error.retryCount,
|
|
466
|
-
maxRetries: error.maxRetries
|
|
650
|
+
maxRetries: error.maxRetries,
|
|
467
651
|
};
|
|
468
652
|
|
|
469
653
|
if (config.includeContext) {
|
|
@@ -481,18 +665,19 @@ export function logTranslationError(
|
|
|
481
665
|
config.customLogger(error);
|
|
482
666
|
} else {
|
|
483
667
|
switch (config.level) {
|
|
484
|
-
case
|
|
485
|
-
console.error(
|
|
668
|
+
case "error":
|
|
669
|
+
console.error("Translation Error:", logData);
|
|
486
670
|
break;
|
|
487
|
-
case
|
|
488
|
-
if (process.env.NODE_ENV !==
|
|
671
|
+
case "warn":
|
|
672
|
+
if (process.env.NODE_ENV !== "production")
|
|
673
|
+
console.warn("Translation Warning:", logData);
|
|
489
674
|
break;
|
|
490
|
-
case
|
|
491
|
-
console.info(
|
|
675
|
+
case "info":
|
|
676
|
+
console.info("Translation Info:", logData);
|
|
492
677
|
break;
|
|
493
|
-
case
|
|
494
|
-
console.debug(
|
|
678
|
+
case "debug":
|
|
679
|
+
console.debug("Translation Debug:", logData);
|
|
495
680
|
break;
|
|
496
681
|
}
|
|
497
682
|
}
|
|
498
|
-
}
|
|
683
|
+
}
|