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