@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.
@@ -12,18 +12,27 @@ import {
12
12
  defaultErrorLoggingConfig,
13
13
  isRecoverableError,
14
14
  isPluralValue,
15
- PluralCategory
16
- } from '../types';
15
+ PluralCategory,
16
+ } from "../types";
17
17
 
18
18
  export interface TranslatorInterface {
19
- translate(key: string, paramsOrLang?: Record<string, unknown> | string, language?: string): string;
20
- tPlural(key: string, count: number, params?: Record<string, unknown>, language?: string): string;
19
+ translate(
20
+ key: string,
21
+ paramsOrLang?: Record<string, unknown> | string,
22
+ language?: string,
23
+ ): string;
24
+ tPlural(
25
+ key: string,
26
+ count: number,
27
+ params?: Record<string, unknown>,
28
+ language?: string,
29
+ ): string;
21
30
  setLanguage(lang: string): void;
22
31
  getCurrentLanguage(): string;
23
32
  initialize(): Promise<void>;
24
33
  isReady(): boolean;
25
34
  debug(): unknown;
26
- getRawValue(key: string, language?: string): unknown;
35
+ getRawValue<T = unknown>(key: string, language?: string): T | undefined;
27
36
  tArray(key: string, language?: string): string[];
28
37
  }
29
38
 
@@ -32,11 +41,14 @@ export class Translator implements TranslatorInterface {
32
41
  private pluralRulesCache = new Map<string, Intl.PluralRules>();
33
42
  private loadedNamespaces = new Set<string>();
34
43
  private loadingPromises = new Map<string, Promise<TranslationNamespace>>();
35
- private allTranslations: Record<string, Record<string, TranslationNamespace>> = {};
44
+ private allTranslations: Record<
45
+ string,
46
+ Record<string, TranslationNamespace>
47
+ > = {};
36
48
  private isInitialized = false;
37
49
  private initializationError: TranslationError | null = null;
38
50
  private config: I18nConfig;
39
- private currentLang: string = 'en';
51
+ private currentLang: string = "en";
40
52
  private cacheStats = {
41
53
  hits: 0,
42
54
  misses: 0,
@@ -44,12 +56,13 @@ export class Translator implements TranslatorInterface {
44
56
  // 번역 로드 완료 시 React 리렌더링을 위한 콜백
45
57
  private onTranslationLoadedCallbacks: Set<() => void> = new Set();
46
58
  // 언어 변경 시 React 리렌더링을 위한 콜백
47
- private onLanguageChangedCallbacks: Set<(language: string) => void> = new Set();
59
+ private onLanguageChangedCallbacks: Set<(language: string) => void> =
60
+ new Set();
48
61
  // 디바운싱을 위한 타이머
49
- private notifyTimer: NodeJS.Timeout | null = null;
62
+ private notifyTimer: ReturnType<typeof setTimeout> | null = null;
50
63
  // 최근 알림한 네임스페이스 (중복 알림 방지)
51
64
  private recentlyNotified = new Set<string>();
52
-
65
+
53
66
  /**
54
67
  * 번역 로드 완료 콜백 등록
55
68
  */
@@ -74,77 +87,79 @@ export class Translator implements TranslatorInterface {
74
87
  * 언어 변경 이벤트 발생
75
88
  */
76
89
  private notifyLanguageChanged(language: string): void {
77
- this.onLanguageChangedCallbacks.forEach(callback => {
90
+ this.onLanguageChangedCallbacks.forEach((callback) => {
78
91
  try {
79
92
  callback(language);
80
93
  } catch (error) {
81
94
  if (this.config.debug) {
82
- console.error('Error in language changed callback:', error);
95
+ console.error("Error in language changed callback:", error);
83
96
  }
84
97
  }
85
98
  });
86
99
  }
87
-
100
+
88
101
  /**
89
102
  * 번역 로드 완료 이벤트 발생 (디바운싱 적용)
90
103
  */
91
104
  private notifyTranslationLoaded(language: string, namespace: string): void {
92
105
  const cacheKey = `${language}:${namespace}`;
93
-
106
+
94
107
  // 최근에 알림한 네임스페이스는 스킵 (중복 알림 방지)
95
108
  if (this.recentlyNotified.has(cacheKey)) {
96
109
  return;
97
110
  }
98
-
111
+
99
112
  this.recentlyNotified.add(cacheKey);
100
-
113
+
101
114
  // 디바운싱: 짧은 시간 내 여러 번역이 로드되면 한 번만 알림
102
115
  if (this.notifyTimer) {
103
116
  clearTimeout(this.notifyTimer);
104
117
  }
105
-
118
+
106
119
  this.notifyTimer = setTimeout(() => {
107
- this.onTranslationLoadedCallbacks.forEach(callback => {
120
+ this.onTranslationLoadedCallbacks.forEach((callback) => {
108
121
  try {
109
122
  callback();
110
123
  } catch (error) {
111
124
  if (this.config.debug) {
112
- console.warn('Error in translation loaded callback:', error);
125
+ console.warn("Error in translation loaded callback:", error);
113
126
  }
114
127
  }
115
128
  });
116
-
129
+
117
130
  // 100ms 후 recentlyNotified 초기화 (같은 네임스페이스도 다시 알림 가능하도록)
118
131
  setTimeout(() => {
119
132
  this.recentlyNotified.clear();
120
133
  }, 100);
121
-
134
+
122
135
  this.notifyTimer = null;
123
136
  }, 50); // 50ms 디바운싱
124
137
  }
125
138
 
126
139
  constructor(config: I18nConfig) {
127
140
  if (!validateI18nConfig(config)) {
128
- throw new Error('Invalid I18nConfig provided');
141
+ throw new Error("Invalid I18nConfig provided");
129
142
  }
130
143
 
131
144
  this.config = {
132
- fallbackLanguage: 'en',
133
- namespaces: ['common'],
145
+ fallbackLanguage: "en",
146
+ namespaces: ["common"],
134
147
  debug: false,
135
148
  missingKeyHandler: (key: string) => key,
136
149
  errorHandler: (error: Error) => {
137
150
  // Silent by default, user can override
138
151
  },
139
- ...config
152
+ ...config,
140
153
  };
141
154
  this.currentLang = config.defaultLanguage;
142
-
155
+
143
156
  // SSR에서 전달된 초기 번역 데이터가 있으면 즉시 설정 (네트워크 요청 없음)
144
157
  if (config.initialTranslations) {
145
158
  this.allTranslations = config.initialTranslations;
146
159
  // 로드된 네임스페이스 마킹
147
- for (const [language, namespaces] of Object.entries(config.initialTranslations)) {
160
+ for (const [language, namespaces] of Object.entries(
161
+ config.initialTranslations,
162
+ )) {
148
163
  for (const namespace of Object.keys(namespaces)) {
149
164
  this.loadedNamespaces.add(`${language}:${namespace}`);
150
165
  }
@@ -161,13 +176,13 @@ export class Translator implements TranslatorInterface {
161
176
  async initialize(): Promise<void> {
162
177
  if (this.isInitialized) {
163
178
  if (this.config.debug) {
164
- console.log('🚫 [TRANSLATOR] Already initialized, skipping');
179
+ console.log("🚫 [TRANSLATOR] Already initialized, skipping");
165
180
  }
166
181
  return;
167
182
  }
168
183
 
169
184
  if (this.config.debug) {
170
- console.log('🚀 [TRANSLATOR] Starting initialization...');
185
+ console.log("🚀 [TRANSLATOR] Starting initialization...");
171
186
  }
172
187
 
173
188
  try {
@@ -177,10 +192,13 @@ export class Translator implements TranslatorInterface {
177
192
  }
178
193
 
179
194
  const languages = [this.currentLang];
180
- if (this.config.fallbackLanguage && this.config.fallbackLanguage !== this.currentLang) {
195
+ if (
196
+ this.config.fallbackLanguage &&
197
+ this.config.fallbackLanguage !== this.currentLang
198
+ ) {
181
199
  languages.push(this.config.fallbackLanguage);
182
200
  }
183
-
201
+
184
202
  // 초기 번역 데이터가 이미 있으면 해당 네임스페이스는 스킵
185
203
  const skipNamespaces = new Set<string>();
186
204
  for (const language of languages) {
@@ -192,14 +210,20 @@ export class Translator implements TranslatorInterface {
192
210
  }
193
211
 
194
212
  if (this.config.debug) {
195
- console.log('🌍 [TRANSLATOR] Initializing translator with languages:', languages);
196
- console.log('📍 [TRANSLATOR] Current language:', this.currentLang);
197
- console.log('📦 [TRANSLATOR] Config namespaces:', this.config.namespaces);
213
+ console.log(
214
+ "🌍 [TRANSLATOR] Initializing translator with languages:",
215
+ languages,
216
+ );
217
+ console.log("📍 [TRANSLATOR] Current language:", this.currentLang);
218
+ console.log(
219
+ "📦 [TRANSLATOR] Config namespaces:",
220
+ this.config.namespaces,
221
+ );
198
222
  }
199
223
 
200
224
  for (const language of languages) {
201
225
  if (this.config.debug) {
202
- console.log('Processing language:', language);
226
+ console.log("Processing language:", language);
203
227
  }
204
228
 
205
229
  if (!this.allTranslations[language]) {
@@ -208,35 +232,45 @@ export class Translator implements TranslatorInterface {
208
232
 
209
233
  for (const namespace of this.config.namespaces || []) {
210
234
  const cacheKey = `${language}:${namespace}`;
211
-
235
+
212
236
  // 이미 초기 번역 데이터가 있으면 스킵 (네트워크 요청 없음)
213
237
  if (skipNamespaces.has(cacheKey)) {
214
238
  if (this.config.debug) {
215
- console.log('⏭️ [TRANSLATOR] Skipping', namespace, 'for', language, '(already loaded from SSR)');
239
+ console.log(
240
+ "⏭️ [TRANSLATOR] Skipping",
241
+ namespace,
242
+ "for",
243
+ language,
244
+ "(already loaded from SSR)",
245
+ );
216
246
  }
217
247
  continue;
218
248
  }
219
-
249
+
220
250
  if (this.config.debug) {
221
- console.log('Loading namespace:', namespace, 'for language:', language);
251
+ console.log(
252
+ "Loading namespace:",
253
+ namespace,
254
+ "for language:",
255
+ language,
256
+ );
222
257
  }
223
258
 
224
259
  try {
225
260
  const data = await this.safeLoadTranslations(language, namespace);
226
261
 
227
262
  if (this.config.debug) {
228
- console.log('Loaded data for', language, namespace, ':', data);
263
+ console.log("Loaded data for", language, namespace, ":", data);
229
264
  }
230
265
 
231
266
  this.allTranslations[language][namespace] = data;
232
267
  this.loadedNamespaces.add(`${language}:${namespace}`);
233
-
234
268
  } catch (error) {
235
269
  const translationError = this.createTranslationError(
236
- 'LOAD_FAILED',
270
+ "LOAD_FAILED",
237
271
  error as Error,
238
272
  language,
239
- namespace
273
+ namespace,
240
274
  );
241
275
 
242
276
  this.logError(translationError);
@@ -246,19 +280,22 @@ export class Translator implements TranslatorInterface {
246
280
  // 폴백 언어로 시도
247
281
  if (language !== this.config.fallbackLanguage) {
248
282
  try {
249
- const fallbackData = await this.safeLoadTranslations(this.config.fallbackLanguage || 'en', namespace);
283
+ const fallbackData = await this.safeLoadTranslations(
284
+ this.config.fallbackLanguage || "en",
285
+ namespace,
286
+ );
250
287
  this.allTranslations[language][namespace] = fallbackData;
251
288
  this.loadedNamespaces.add(`${language}:${namespace}`);
252
289
 
253
290
  if (this.config.debug) {
254
- console.log('Using fallback data for', language, namespace);
291
+ console.log("Using fallback data for", language, namespace);
255
292
  }
256
293
  } catch (fallbackError) {
257
294
  const fallbackTranslationError = this.createTranslationError(
258
- 'FALLBACK_LOAD_FAILED',
295
+ "FALLBACK_LOAD_FAILED",
259
296
  fallbackError as Error,
260
297
  this.config.fallbackLanguage,
261
- namespace
298
+ namespace,
262
299
  );
263
300
 
264
301
  this.logError(fallbackTranslationError);
@@ -281,14 +318,13 @@ export class Translator implements TranslatorInterface {
281
318
  this.isInitialized = true;
282
319
 
283
320
  if (this.config.debug) {
284
- console.log('Translator initialized successfully');
285
- console.log('Loaded translations:', this.allTranslations);
321
+ console.log("Translator initialized successfully");
322
+ console.log("Loaded translations:", this.allTranslations);
286
323
  }
287
-
288
324
  } catch (error) {
289
325
  this.initializationError = this.createTranslationError(
290
- 'INITIALIZATION_FAILED',
291
- error as Error
326
+ "INITIALIZATION_FAILED",
327
+ error as Error,
292
328
  );
293
329
 
294
330
  this.logError(this.initializationError);
@@ -297,7 +333,9 @@ export class Translator implements TranslatorInterface {
297
333
  this.isInitialized = true;
298
334
 
299
335
  if (this.config.debug) {
300
- console.warn('Translator initialized with errors, using fallback translations');
336
+ console.warn(
337
+ "Translator initialized with errors, using fallback translations",
338
+ );
301
339
  }
302
340
  }
303
341
  }
@@ -307,38 +345,48 @@ export class Translator implements TranslatorInterface {
307
345
  */
308
346
  private translateBeforeInitialized(key: string, targetLang: string): string {
309
347
  if (this.config.debug) {
310
- console.warn('Translator not initialized. Call initialize() first.');
348
+ console.warn("Translator not initialized. Call initialize() first.");
311
349
  }
312
-
350
+
313
351
  // 초기화되지 않았을 때도 기본 번역 시도 (initialTranslations 사용)
314
352
  const { namespace, key: actualKey } = this.parseKey(key);
315
-
353
+
316
354
  // findInNamespace를 사용하여 중첩 키도 처리
317
355
  const result = this.findInNamespace(namespace, actualKey, targetLang);
318
356
  if (result) {
319
357
  if (this.config.debug) {
320
- console.log(`✅ [TRANSLATOR] Found fallback translation from initialTranslations:`, result);
358
+ console.log(
359
+ `✅ [TRANSLATOR] Found fallback translation from initialTranslations:`,
360
+ result,
361
+ );
321
362
  }
322
363
  return result;
323
364
  }
324
-
365
+
325
366
  if (this.config.debug) {
326
367
  const translations = this.allTranslations[targetLang]?.[namespace];
327
368
  console.log(`🔍 [TRANSLATOR] Not initialized, fallback failed:`, {
328
369
  namespace,
329
370
  actualKey,
330
371
  hasTranslations: !!translations,
331
- translationsKeys: translations ? Object.keys(translations) : []
372
+ translationsKeys: translations ? Object.keys(translations) : [],
332
373
  });
333
374
  }
334
- return this.config.missingKeyHandler?.(key, targetLang, 'default') || key;
375
+ return this.config.missingKeyHandler?.(key, targetLang, "default") || key;
335
376
  }
336
377
 
337
378
  /**
338
379
  * 다른 로드된 언어에서 번역 찾기 (언어 변경 중 깜빡임 방지)
339
380
  */
340
- private findInOtherLanguages(namespace: string, key: string, targetLang: string): string | null {
341
- if (!this.allTranslations || Object.keys(this.allTranslations).length === 0) {
381
+ private findInOtherLanguages(
382
+ namespace: string,
383
+ key: string,
384
+ targetLang: string,
385
+ ): string | null {
386
+ if (
387
+ !this.allTranslations ||
388
+ Object.keys(this.allTranslations).length === 0
389
+ ) {
342
390
  return null;
343
391
  }
344
392
 
@@ -351,15 +399,19 @@ export class Translator implements TranslatorInterface {
351
399
  }
352
400
  }
353
401
  }
354
-
402
+
355
403
  return null;
356
404
  }
357
405
 
358
406
  /**
359
407
  * 폴백 언어에서 번역 찾기
360
408
  */
361
- private findInFallbackLanguage(namespace: string, key: string, targetLang: string): string | null {
362
- const fallbackLang = this.config.fallbackLanguage || 'en';
409
+ private findInFallbackLanguage(
410
+ namespace: string,
411
+ key: string,
412
+ targetLang: string,
413
+ ): string | null {
414
+ const fallbackLang = this.config.fallbackLanguage || "en";
363
415
  if (targetLang === fallbackLang) {
364
416
  return null;
365
417
  }
@@ -369,20 +421,24 @@ export class Translator implements TranslatorInterface {
369
421
  this.cacheStats.hits++;
370
422
  return result;
371
423
  }
372
-
424
+
373
425
  return null;
374
426
  }
375
427
 
376
428
  /**
377
429
  * 번역 키를 번역된 텍스트로 변환
378
430
  */
379
- translate(key: string, paramsOrLang?: Record<string, unknown> | string, language?: string): string {
431
+ translate(
432
+ key: string,
433
+ paramsOrLang?: Record<string, unknown> | string,
434
+ language?: string,
435
+ ): string {
380
436
  // 두 번째 인자 타입으로 분기
381
437
  let params: Record<string, unknown> | undefined;
382
438
  let targetLang: string;
383
- if (typeof paramsOrLang === 'string') {
439
+ if (typeof paramsOrLang === "string") {
384
440
  targetLang = paramsOrLang;
385
- } else if (typeof paramsOrLang === 'object' && paramsOrLang !== null) {
441
+ } else if (typeof paramsOrLang === "object" && paramsOrLang !== null) {
386
442
  params = paramsOrLang;
387
443
  targetLang = language || this.currentLang;
388
444
  } else {
@@ -392,13 +448,28 @@ export class Translator implements TranslatorInterface {
392
448
  // 초기화되지 않은 경우 처리
393
449
  if (!this.isInitialized) {
394
450
  const raw = this.translateBeforeInitialized(key, targetLang);
451
+ // translateBeforeInitialized returns key or missingKeyHandler result on miss
452
+ // Check defaultValue before returning the raw key
453
+ if (
454
+ (!raw || raw === key) &&
455
+ params &&
456
+ typeof params === "object" &&
457
+ "defaultValue" in params &&
458
+ typeof params.defaultValue === "string"
459
+ ) {
460
+ return this.interpolate(params.defaultValue, params);
461
+ }
395
462
  return params ? this.interpolate(raw, params) : raw;
396
463
  }
397
464
 
398
465
  const { namespace, key: actualKey } = this.parseKey(key);
399
466
 
400
467
  // 1단계: 현재 언어에서 찾기
401
- let result: string | null = this.findInNamespace(namespace, actualKey, targetLang);
468
+ let result: string | null = this.findInNamespace(
469
+ namespace,
470
+ actualKey,
471
+ targetLang,
472
+ );
402
473
  if (result) {
403
474
  this.cacheStats.hits++;
404
475
  return params ? this.interpolate(result, params) : result;
@@ -419,38 +490,61 @@ export class Translator implements TranslatorInterface {
419
490
  // 모든 단계에서 찾지 못한 경우
420
491
  this.cacheStats.misses++;
421
492
 
493
+ // defaultValue가 제공된 경우 반환 (프로덕션/디버그 모두 적용)
494
+ if (
495
+ params &&
496
+ typeof params === "object" &&
497
+ "defaultValue" in params &&
498
+ typeof params.defaultValue === "string"
499
+ ) {
500
+ return this.interpolate(params.defaultValue, params);
501
+ }
502
+
422
503
  if (this.config.debug) {
423
- const missing = this.config.missingKeyHandler?.(key, targetLang, namespace) || key;
504
+ const missing =
505
+ this.config.missingKeyHandler?.(key, targetLang, namespace) || key;
424
506
  return params ? this.interpolate(missing, params) : missing;
425
507
  }
426
508
 
427
509
  // 프로덕션에서는 빈 문자열 반환 (미싱 키 노출 방지)
428
- return '';
510
+ return "";
429
511
  }
430
512
 
431
513
  /**
432
514
  * 네임스페이스에서 키 찾기
433
515
  */
434
- private findInNamespace(namespace: string, key: string, language: string): string {
516
+ private findInNamespace(
517
+ namespace: string,
518
+ key: string,
519
+ language: string,
520
+ ): string {
435
521
  const translations = this.allTranslations[language]?.[namespace];
436
522
 
437
523
  if (!translations) {
438
524
  // 네임스페이스가 없으면 자동으로 로드 시도 (비동기, 백그라운드)
439
525
  const cacheKey = `${language}:${namespace}`;
440
- if (!this.loadedNamespaces.has(cacheKey) && !this.loadingPromises.has(cacheKey)) {
526
+ if (
527
+ !this.loadedNamespaces.has(cacheKey) &&
528
+ !this.loadingPromises.has(cacheKey)
529
+ ) {
441
530
  // 로딩 시작 (비동기, 즉시 반환하지 않음)
442
- this.loadTranslationData(language, namespace).catch(error => {
531
+ this.loadTranslationData(language, namespace).catch((error) => {
443
532
  if (this.config.debug) {
444
- console.warn(`⚠️ [TRANSLATOR] Auto-load failed for ${language}/${namespace}:`, error);
533
+ console.warn(
534
+ `⚠️ [TRANSLATOR] Auto-load failed for ${language}/${namespace}:`,
535
+ error,
536
+ );
445
537
  }
446
538
  });
447
-
539
+
448
540
  // 디버그 모드에서만 첫 시도 시에만 경고 출력 (중복 방지)
449
541
  if (this.config.debug) {
450
- console.warn(`❌ [TRANSLATOR] No translations found for ${language}/${namespace}, attempting auto-load...`);
542
+ console.warn(
543
+ `❌ [TRANSLATOR] No translations found for ${language}/${namespace}, attempting auto-load...`,
544
+ );
451
545
  }
452
546
  }
453
- return '';
547
+ return "";
454
548
  }
455
549
 
456
550
  // 직접 키 매칭
@@ -472,9 +566,11 @@ export class Translator implements TranslatorInterface {
472
566
  }
473
567
 
474
568
  if (this.config.debug) {
475
- console.warn(`❌ [TRANSLATOR] No match found for key: ${key} in ${language}/${namespace}`);
569
+ console.warn(
570
+ `❌ [TRANSLATOR] No match found for key: ${key} in ${language}/${namespace}`,
571
+ );
476
572
  }
477
- return '';
573
+ return "";
478
574
  }
479
575
 
480
576
  /**
@@ -482,28 +578,31 @@ export class Translator implements TranslatorInterface {
482
578
  * 배열도 지원: 최종 값이 string[]이면 그대로 반환
483
579
  */
484
580
  private getNestedValue(obj: unknown, path: string): unknown {
485
- if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
581
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
486
582
  return undefined;
487
583
  }
488
584
 
489
- return path.split('.').reduce((current: unknown, key: string) => {
585
+ return path.split(".").reduce((current: unknown, key: string) => {
490
586
  if (current == null) return undefined;
491
587
  if (Array.isArray(current)) {
492
588
  const idx = Number(key);
493
589
  return Number.isInteger(idx) ? current[idx] : undefined;
494
590
  }
495
- if (typeof current === 'object' && key in (current as Record<string, unknown>)) {
591
+ if (
592
+ typeof current === "object" &&
593
+ key in (current as Record<string, unknown>)
594
+ ) {
496
595
  return (current as Record<string, unknown>)[key];
497
596
  }
498
597
  return undefined;
499
598
  }, obj);
500
599
  }
501
-
600
+
502
601
  /**
503
602
  * 문자열 값인지 확인하는 타입 가드
504
603
  */
505
604
  private isStringValue(value: unknown): value is string {
506
- return typeof value === 'string' && value.length > 0;
605
+ return typeof value === "string" && value.length > 0;
507
606
  }
508
607
 
509
608
  /**
@@ -511,18 +610,22 @@ export class Translator implements TranslatorInterface {
511
610
  * 배열 값이 t()에 전달되면 랜덤으로 하나를 선택하여 반환
512
611
  */
513
612
  private isStringArray(value: unknown): value is string[] {
514
- return Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string');
613
+ return (
614
+ Array.isArray(value) &&
615
+ value.length > 0 &&
616
+ value.every((v) => typeof v === "string")
617
+ );
515
618
  }
516
619
 
517
620
  /**
518
621
  * 원시 값 가져오기 (배열, 객체 포함)
519
622
  */
520
- getRawValue(key: string, language?: string): unknown {
623
+ getRawValue<T = unknown>(key: string, language?: string): T | undefined {
521
624
  const targetLang = language || this.currentLang;
522
625
 
523
626
  if (!this.isInitialized) {
524
627
  if (this.config.debug) {
525
- console.warn('Translator not initialized. Call initialize() first.');
628
+ console.warn("Translator not initialized. Call initialize() first.");
526
629
  }
527
630
  return undefined;
528
631
  }
@@ -536,25 +639,29 @@ export class Translator implements TranslatorInterface {
536
639
 
537
640
  // 직접 키 매칭
538
641
  if (actualKey in translations) {
539
- return translations[actualKey];
642
+ return translations[actualKey] as T;
540
643
  }
541
644
 
542
645
  // 중첩 키 매칭
543
646
  const nestedValue = this.getNestedValue(translations, actualKey);
544
647
  if (nestedValue !== undefined) {
545
- return nestedValue;
648
+ return nestedValue as T;
546
649
  }
547
650
 
548
651
  // 폴백 언어에서 찾기
549
652
  if (targetLang !== this.config.fallbackLanguage) {
550
- const fallbackTranslations = this.allTranslations[this.config.fallbackLanguage || 'en']?.[namespace];
653
+ const fallbackTranslations =
654
+ this.allTranslations[this.config.fallbackLanguage || "en"]?.[namespace];
551
655
  if (fallbackTranslations) {
552
656
  if (actualKey in fallbackTranslations) {
553
- return fallbackTranslations[actualKey];
657
+ return fallbackTranslations[actualKey] as T;
554
658
  }
555
- const fallbackNestedValue = this.getNestedValue(fallbackTranslations, actualKey);
659
+ const fallbackNestedValue = this.getNestedValue(
660
+ fallbackTranslations,
661
+ actualKey,
662
+ );
556
663
  if (fallbackNestedValue !== undefined) {
557
- return fallbackNestedValue;
664
+ return fallbackNestedValue as T;
558
665
  }
559
666
  }
560
667
  }
@@ -567,10 +674,13 @@ export class Translator implements TranslatorInterface {
567
674
  */
568
675
  tArray(key: string, language?: string): string[] {
569
676
  const raw = this.getRawValue(key, language);
570
- if (Array.isArray(raw) && raw.every((v: unknown) => typeof v === 'string')) {
677
+ if (
678
+ Array.isArray(raw) &&
679
+ raw.every((v: unknown) => typeof v === "string")
680
+ ) {
571
681
  return raw as string[];
572
682
  }
573
- if (process.env.NODE_ENV === 'development') {
683
+ if (process.env.NODE_ENV === "development") {
574
684
  console.warn(`tArray: "${key}" is not a string array`);
575
685
  }
576
686
  return [];
@@ -597,20 +707,27 @@ export class Translator implements TranslatorInterface {
597
707
  * tPlural('common:total_count', 1) → en: "1 item" / ko: "총 1개"
598
708
  * tPlural('common:total_count', 5) → en: "5 items" / ko: "총 5개"
599
709
  */
600
- tPlural(key: string, count: number, params?: Record<string, unknown>, language?: string): string {
710
+ tPlural(
711
+ key: string,
712
+ count: number,
713
+ params?: Record<string, unknown>,
714
+ language?: string,
715
+ ): string {
601
716
  const targetLang = language || this.currentLang;
602
717
  const raw = this.getRawValue(key, targetLang);
603
718
  const mergedParams: Record<string, unknown> = { count, ...params };
604
719
 
605
720
  // PluralValue 객체인 경우: Intl.PluralRules로 카테고리 결정
606
721
  if (isPluralValue(raw)) {
607
- const category = this.getPluralRules(targetLang).select(count) as PluralCategory;
722
+ const category = this.getPluralRules(targetLang).select(
723
+ count,
724
+ ) as PluralCategory;
608
725
  const text = raw[category] ?? raw.other;
609
726
  return this.interpolate(text, mergedParams);
610
727
  }
611
728
 
612
729
  // fallback: plain string이면 interpolate만
613
- if (typeof raw === 'string') {
730
+ if (typeof raw === "string") {
614
731
  return this.interpolate(raw, mergedParams);
615
732
  }
616
733
 
@@ -618,7 +735,7 @@ export class Translator implements TranslatorInterface {
618
735
  if (this.config.debug) {
619
736
  return this.interpolate(key, mergedParams);
620
737
  }
621
- return '';
738
+ return "";
622
739
  }
623
740
 
624
741
  /**
@@ -652,15 +769,17 @@ export class Translator implements TranslatorInterface {
652
769
 
653
770
  // 새로운 언어의 데이터가 로드되지 않았다면 로드
654
771
  if (!this.allTranslations[language]) {
655
- this.loadLanguageData(language).catch(error => {
772
+ this.loadLanguageData(language).catch((error) => {
656
773
  if (this.config.debug) {
657
- console.warn('Failed to load language data:', error);
774
+ console.warn("Failed to load language data:", error);
658
775
  }
659
776
  });
660
777
  }
661
778
 
662
779
  if (this.config.debug) {
663
- console.log(`🌐 [TRANSLATOR] Language changed: ${previousLanguage} -> ${language}`);
780
+ console.log(
781
+ `🌐 [TRANSLATOR] Language changed: ${previousLanguage} -> ${language}`,
782
+ );
664
783
  }
665
784
  }
666
785
 
@@ -677,15 +796,15 @@ export class Translator implements TranslatorInterface {
677
796
  const data = await this.safeLoadTranslations(language, namespace);
678
797
  this.allTranslations[language][namespace] = data;
679
798
  this.loadedNamespaces.add(`${language}:${namespace}`);
680
-
799
+
681
800
  // 언어 변경 시 번역 로드 완료 알림
682
801
  this.notifyTranslationLoaded(language, namespace);
683
802
  } catch (error) {
684
803
  const translationError = this.createTranslationError(
685
- 'LOAD_FAILED',
804
+ "LOAD_FAILED",
686
805
  error as Error,
687
806
  language,
688
- namespace
807
+ namespace,
689
808
  );
690
809
 
691
810
  this.logError(translationError);
@@ -713,7 +832,7 @@ export class Translator implements TranslatorInterface {
713
832
  * 지원되는 언어 목록 가져오기
714
833
  */
715
834
  getSupportedLanguages(): string[] {
716
- return this.config.supportedLanguages?.map(lang => lang.code) || [];
835
+ return this.config.supportedLanguages?.map((lang) => lang.code) || [];
717
836
  }
718
837
 
719
838
  /**
@@ -738,7 +857,7 @@ export class Translator implements TranslatorInterface {
738
857
  this.cacheStats = { hits: 0, misses: 0 };
739
858
 
740
859
  if (this.config.debug) {
741
- console.log('Cache cleared');
860
+ console.log("Cache cleared");
742
861
  }
743
862
  }
744
863
 
@@ -749,7 +868,7 @@ export class Translator implements TranslatorInterface {
749
868
  this.cache.set(key, {
750
869
  data,
751
870
  timestamp: Date.now(),
752
- ttl: 5 * 60 * 1000 // 5분
871
+ ttl: 5 * 60 * 1000, // 5분
753
872
  });
754
873
  }
755
874
 
@@ -776,14 +895,14 @@ export class Translator implements TranslatorInterface {
776
895
  * 번역 오류 생성
777
896
  */
778
897
  private createTranslationError(
779
- code: TranslationError['code'],
898
+ code: TranslationError["code"],
780
899
  originalError: Error,
781
900
  language?: string,
782
901
  namespace?: string,
783
- key?: string
902
+ key?: string,
784
903
  ): TranslationError {
785
904
  return {
786
- name: 'TranslationError',
905
+ name: "TranslationError",
787
906
  code,
788
907
  message: originalError.message,
789
908
  originalError,
@@ -791,7 +910,7 @@ export class Translator implements TranslatorInterface {
791
910
  namespace,
792
911
  key,
793
912
  timestamp: Date.now(),
794
- stack: originalError.stack
913
+ stack: originalError.stack,
795
914
  };
796
915
  }
797
916
 
@@ -800,7 +919,11 @@ export class Translator implements TranslatorInterface {
800
919
  */
801
920
  private logError(error: TranslationError): void {
802
921
  if (this.config.errorHandler) {
803
- this.config.errorHandler(error, error.language || '', error.namespace || '');
922
+ this.config.errorHandler(
923
+ error,
924
+ error.language || "",
925
+ error.namespace || "",
926
+ );
804
927
  }
805
928
  }
806
929
 
@@ -810,7 +933,7 @@ export class Translator implements TranslatorInterface {
810
933
  private async retryOperation<T>(
811
934
  operation: () => Promise<T>,
812
935
  error: TranslationError,
813
- context: { language?: string; namespace?: string; key?: string }
936
+ context: { language?: string; namespace?: string; key?: string },
814
937
  ): Promise<T> {
815
938
  const maxRetries = 3;
816
939
  let lastError = error;
@@ -820,11 +943,11 @@ export class Translator implements TranslatorInterface {
820
943
  return await operation();
821
944
  } catch (retryError) {
822
945
  lastError = this.createTranslationError(
823
- 'RETRY_FAILED',
946
+ "RETRY_FAILED",
824
947
  retryError as Error,
825
948
  context.language,
826
949
  context.namespace,
827
- context.key
950
+ context.key,
828
951
  );
829
952
 
830
953
  if (attempt === maxRetries) {
@@ -832,7 +955,9 @@ export class Translator implements TranslatorInterface {
832
955
  }
833
956
 
834
957
  // 지수 백오프
835
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
958
+ await new Promise((resolve) =>
959
+ setTimeout(resolve, Math.pow(2, attempt) * 1000),
960
+ );
836
961
  }
837
962
  }
838
963
 
@@ -842,18 +967,27 @@ export class Translator implements TranslatorInterface {
842
967
  /**
843
968
  * 안전한 번역 로드
844
969
  */
845
- private async safeLoadTranslations(language: string, namespace: string): Promise<TranslationNamespace> {
970
+ private async safeLoadTranslations(
971
+ language: string,
972
+ namespace: string,
973
+ ): Promise<TranslationNamespace> {
846
974
  if (this.config.debug) {
847
- console.log(`📥 [TRANSLATOR] safeLoadTranslations called:`, { language, namespace });
975
+ console.log(`📥 [TRANSLATOR] safeLoadTranslations called:`, {
976
+ language,
977
+ namespace,
978
+ });
848
979
  }
849
980
 
850
981
  const loadOperation = async (): Promise<TranslationNamespace> => {
851
982
  if (!this.config.loadTranslations) {
852
- throw new Error('No translation loader configured');
983
+ throw new Error("No translation loader configured");
853
984
  }
854
985
 
855
986
  if (this.config.debug) {
856
- console.log(`🔄 [TRANSLATOR] Calling loadTranslations for:`, { language, namespace });
987
+ console.log(`🔄 [TRANSLATOR] Calling loadTranslations for:`, {
988
+ language,
989
+ namespace,
990
+ });
857
991
  }
858
992
 
859
993
  const data = await this.config.loadTranslations(language, namespace);
@@ -863,7 +997,9 @@ export class Translator implements TranslatorInterface {
863
997
  }
864
998
 
865
999
  if (!isTranslationNamespace(data)) {
866
- throw new Error(`Invalid translation data for ${language}:${namespace}`);
1000
+ throw new Error(
1001
+ `Invalid translation data for ${language}:${namespace}`,
1002
+ );
867
1003
  }
868
1004
 
869
1005
  return data;
@@ -873,13 +1009,16 @@ export class Translator implements TranslatorInterface {
873
1009
  return await loadOperation();
874
1010
  } catch (error) {
875
1011
  const translationError = this.createTranslationError(
876
- 'LOAD_FAILED',
1012
+ "LOAD_FAILED",
877
1013
  error as Error,
878
1014
  language,
879
- namespace
1015
+ namespace,
880
1016
  );
881
1017
 
882
- return this.retryOperation(loadOperation, translationError, { language, namespace });
1018
+ return this.retryOperation(loadOperation, translationError, {
1019
+ language,
1020
+ namespace,
1021
+ });
883
1022
  }
884
1023
  }
885
1024
 
@@ -895,14 +1034,16 @@ export class Translator implements TranslatorInterface {
895
1034
  cacheSize: this.cache.size,
896
1035
  allTranslations: this.allTranslations,
897
1036
  initializationError: this.initializationError,
898
- config: this.config
1037
+ config: this.config,
899
1038
  };
900
1039
  }
901
1040
 
902
1041
  /**
903
1042
  * SSR에서 하이드레이션
904
1043
  */
905
- hydrateFromSSR(translations: Record<string, Record<string, TranslationNamespace>>): void {
1044
+ hydrateFromSSR(
1045
+ translations: Record<string, Record<string, TranslationNamespace>>,
1046
+ ): void {
906
1047
  this.allTranslations = translations;
907
1048
  this.isInitialized = true;
908
1049
 
@@ -917,18 +1058,15 @@ export class Translator implements TranslatorInterface {
917
1058
  /**
918
1059
  * 비동기 번역 (고급 기능)
919
1060
  */
920
- async translateAsync(key: string, params?: Record<string, unknown>): Promise<string> {
1061
+ async translateAsync(
1062
+ key: string,
1063
+ params?: Record<string, unknown>,
1064
+ ): Promise<string> {
921
1065
  if (!this.isInitialized) {
922
1066
  await this.initialize();
923
1067
  }
924
1068
 
925
- const translated = this.translate(key);
926
-
927
- if (!params) {
928
- return translated;
929
- }
930
-
931
- return this.interpolate(translated, params);
1069
+ return this.translate(key, params);
932
1070
  }
933
1071
 
934
1072
  /**
@@ -936,28 +1074,33 @@ export class Translator implements TranslatorInterface {
936
1074
  */
937
1075
  translateSync(key: string, params?: Record<string, unknown>): string {
938
1076
  if (!this.isInitialized) {
1077
+ // defaultValue support even before initialization
1078
+ if (
1079
+ params &&
1080
+ typeof params === "object" &&
1081
+ "defaultValue" in params &&
1082
+ typeof params.defaultValue === "string"
1083
+ ) {
1084
+ return this.interpolate(params.defaultValue, params);
1085
+ }
939
1086
  if (this.config.debug) {
940
- console.warn('Translator not initialized for sync translation');
1087
+ console.warn("Translator not initialized for sync translation");
941
1088
  }
942
1089
  const { namespace } = this.parseKey(key);
943
- return this.config.missingKeyHandler?.(key, this.currentLang, namespace) || key;
944
- }
945
-
946
- const translated = this.translate(key);
947
-
948
- if (!params) {
949
- return translated;
1090
+ return (
1091
+ this.config.missingKeyHandler?.(key, this.currentLang, namespace) || key
1092
+ );
950
1093
  }
951
1094
 
952
- return this.interpolate(translated, params);
1095
+ return this.translate(key, params);
953
1096
  }
954
1097
 
955
1098
  /**
956
1099
  * 키 파싱 (네임스페이스:키 형식)
957
- *
1100
+ *
958
1101
  * - 콜론(:)만 네임스페이스 구분자로 사용
959
1102
  * - 점(.)은 키 이름의 일부로 취급 (중첩 객체 접근용)
960
- *
1103
+ *
961
1104
  * @example
962
1105
  * parseKey("home:hero.badge") → { namespace: "home", key: "hero.badge" }
963
1106
  * parseKey("hero.badge") → { namespace: "common", key: "hero.badge" }
@@ -965,20 +1108,26 @@ export class Translator implements TranslatorInterface {
965
1108
  */
966
1109
  private parseKey(key: string): { namespace: string; key: string } {
967
1110
  // 콜론(:)만 네임스페이스 구분자로 사용
968
- const colonIndex = key.indexOf(':');
1111
+ const colonIndex = key.indexOf(":");
969
1112
  if (colonIndex !== -1) {
970
- return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
1113
+ return {
1114
+ namespace: key.substring(0, colonIndex),
1115
+ key: key.substring(colonIndex + 1),
1116
+ };
971
1117
  }
972
1118
 
973
1119
  // 콜론이 없으면 common 네임스페이스로 간주
974
1120
  // 점(.)은 키 이름의 일부 (중첩 객체 접근은 getNestedValue에서 처리)
975
- return { namespace: 'common', key };
1121
+ return { namespace: "common", key };
976
1122
  }
977
1123
 
978
1124
  /**
979
1125
  * 번역 데이터 로드 (고급 기능)
980
1126
  */
981
- private async loadTranslationData(language: string, namespace: string): Promise<TranslationNamespace> {
1127
+ private async loadTranslationData(
1128
+ language: string,
1129
+ namespace: string,
1130
+ ): Promise<TranslationNamespace> {
982
1131
  const cacheKey = `${language}:${namespace}`;
983
1132
 
984
1133
  // 이미 로드된 네임스페이스인지 확인
@@ -1013,24 +1162,26 @@ export class Translator implements TranslatorInterface {
1013
1162
 
1014
1163
  try {
1015
1164
  const data = await loadPromise;
1016
-
1165
+
1017
1166
  // allTranslations에 저장 (중요: 이렇게 해야 findInNamespace에서 찾을 수 있음)
1018
1167
  if (!this.allTranslations[language]) {
1019
1168
  this.allTranslations[language] = {};
1020
1169
  }
1021
1170
  this.allTranslations[language][namespace] = data;
1022
1171
  this.loadedNamespaces.add(cacheKey);
1023
-
1172
+
1024
1173
  // 캐시에도 저장
1025
1174
  this.setCacheEntry(cacheKey, data);
1026
-
1175
+
1027
1176
  if (this.config.debug) {
1028
- console.log(`✅ [TRANSLATOR] Auto-loaded and saved ${language}/${namespace}`);
1177
+ console.log(
1178
+ `✅ [TRANSLATOR] Auto-loaded and saved ${language}/${namespace}`,
1179
+ );
1029
1180
  }
1030
-
1181
+
1031
1182
  // React 리렌더링 트리거 (디바운싱 적용)
1032
1183
  this.notifyTranslationLoaded(language, namespace);
1033
-
1184
+
1034
1185
  return data;
1035
1186
  } finally {
1036
1187
  this.loadingPromises.delete(cacheKey);
@@ -1040,25 +1191,30 @@ export class Translator implements TranslatorInterface {
1040
1191
  /**
1041
1192
  * 실제 번역 데이터 로드
1042
1193
  */
1043
- private async _loadTranslationData(language: string, namespace: string): Promise<TranslationNamespace> {
1194
+ private async _loadTranslationData(
1195
+ language: string,
1196
+ namespace: string,
1197
+ ): Promise<TranslationNamespace> {
1044
1198
  if (!this.config.loadTranslations) {
1045
- throw new Error('No translation loader configured');
1199
+ throw new Error("No translation loader configured");
1046
1200
  }
1047
1201
 
1048
1202
  try {
1049
1203
  const data = await this.config.loadTranslations(language, namespace);
1050
1204
 
1051
1205
  if (!isTranslationNamespace(data)) {
1052
- throw new Error(`Invalid translation data for ${language}:${namespace}`);
1206
+ throw new Error(
1207
+ `Invalid translation data for ${language}:${namespace}`,
1208
+ );
1053
1209
  }
1054
1210
 
1055
1211
  return data;
1056
1212
  } catch (error) {
1057
1213
  const translationError = this.createTranslationError(
1058
- 'LOAD_FAILED',
1214
+ "LOAD_FAILED",
1059
1215
  error as Error,
1060
1216
  language,
1061
- namespace
1217
+ namespace,
1062
1218
  );
1063
1219
 
1064
1220
  this.logError(translationError);
@@ -1073,9 +1229,9 @@ export class Translator implements TranslatorInterface {
1073
1229
  export function ssrTranslate({
1074
1230
  translations,
1075
1231
  key,
1076
- language = 'ko',
1077
- fallbackLanguage = 'en',
1078
- missingKeyHandler = (key: string) => key
1232
+ language = "ko",
1233
+ fallbackLanguage = "en",
1234
+ missingKeyHandler = (key: string) => key,
1079
1235
  }: {
1080
1236
  translations: Record<string, Record<string, TranslationNamespace>>;
1081
1237
  key: string;
@@ -1086,7 +1242,14 @@ export function ssrTranslate({
1086
1242
  const { namespace, key: actualKey } = parseKey(key);
1087
1243
 
1088
1244
  // 현재 언어에서 찾기
1089
- let result = ssrFindInNamespace(translations, namespace, actualKey, language, fallbackLanguage, missingKeyHandler);
1245
+ let result = ssrFindInNamespace(
1246
+ translations,
1247
+ namespace,
1248
+ actualKey,
1249
+ language,
1250
+ fallbackLanguage,
1251
+ missingKeyHandler,
1252
+ );
1090
1253
 
1091
1254
  if (result) {
1092
1255
  return result;
@@ -1094,7 +1257,14 @@ export function ssrTranslate({
1094
1257
 
1095
1258
  // 폴백 언어에서 찾기
1096
1259
  if (language !== fallbackLanguage) {
1097
- result = ssrFindInNamespace(translations, namespace, actualKey, fallbackLanguage, fallbackLanguage, missingKeyHandler);
1260
+ result = ssrFindInNamespace(
1261
+ translations,
1262
+ namespace,
1263
+ actualKey,
1264
+ fallbackLanguage,
1265
+ fallbackLanguage,
1266
+ missingKeyHandler,
1267
+ );
1098
1268
  if (result) {
1099
1269
  return result;
1100
1270
  }
@@ -1109,12 +1279,12 @@ function ssrFindInNamespace(
1109
1279
  key: string,
1110
1280
  language: string,
1111
1281
  fallbackLanguage: string,
1112
- missingKeyHandler: (key: string) => string
1282
+ missingKeyHandler: (key: string) => string,
1113
1283
  ): string {
1114
1284
  const namespaceData = translations[language]?.[namespace];
1115
1285
 
1116
1286
  if (!namespaceData) {
1117
- return '';
1287
+ return "";
1118
1288
  }
1119
1289
 
1120
1290
  // 직접 키 매칭
@@ -1129,16 +1299,21 @@ function ssrFindInNamespace(
1129
1299
  return nestedValue;
1130
1300
  }
1131
1301
 
1132
- return '';
1302
+ return "";
1133
1303
  }
1134
1304
 
1135
1305
  function getNestedValue(obj: unknown, path: string): unknown {
1136
- if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
1306
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
1137
1307
  return undefined;
1138
1308
  }
1139
1309
 
1140
- return path.split('.').reduce((current: unknown, key: string) => {
1141
- if (current && typeof current === 'object' && !Array.isArray(current) && key in current) {
1310
+ return path.split(".").reduce((current: unknown, key: string) => {
1311
+ if (
1312
+ current &&
1313
+ typeof current === "object" &&
1314
+ !Array.isArray(current) &&
1315
+ key in current
1316
+ ) {
1142
1317
  return (current as Record<string, unknown>)[key];
1143
1318
  }
1144
1319
  return undefined;
@@ -1149,34 +1324,37 @@ function getNestedValue(obj: unknown, path: string): unknown {
1149
1324
  * 문자열 값인지 확인하는 타입 가드
1150
1325
  */
1151
1326
  function isStringValue(value: unknown): value is string {
1152
- return typeof value === 'string' && value.length > 0;
1327
+ return typeof value === "string" && value.length > 0;
1153
1328
  }
1154
1329
 
1155
1330
  /**
1156
1331
  * 키 파싱 (네임스페이스:키 형식) - SSR용 standalone 함수
1157
- *
1332
+ *
1158
1333
  * - 콜론(:)만 네임스페이스 구분자로 사용
1159
1334
  * - 점(.)은 키 이름의 일부로 취급 (중첩 객체 접근용)
1160
1335
  */
1161
1336
  function parseKey(key: string): { namespace: string; key: string } {
1162
1337
  // 콜론(:)만 네임스페이스 구분자로 사용
1163
- const colonIndex = key.indexOf(':');
1338
+ const colonIndex = key.indexOf(":");
1164
1339
  if (colonIndex !== -1) {
1165
- return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
1340
+ return {
1341
+ namespace: key.substring(0, colonIndex),
1342
+ key: key.substring(colonIndex + 1),
1343
+ };
1166
1344
  }
1167
1345
 
1168
1346
  // 콜론이 없으면 common 네임스페이스로 간주
1169
- return { namespace: 'common', key };
1347
+ return { namespace: "common", key };
1170
1348
  }
1171
1349
 
1172
1350
  // 서버 번역 함수 (고급 기능 포함)
1173
1351
  export function serverTranslate({
1174
1352
  translations,
1175
1353
  key,
1176
- language = 'ko',
1177
- fallbackLanguage = 'en',
1354
+ language = "ko",
1355
+ fallbackLanguage = "en",
1178
1356
  missingKeyHandler = (key: string) => key,
1179
- options = {}
1357
+ options = {},
1180
1358
  }: {
1181
1359
  translations: Record<string, unknown>; // 번역 데이터
1182
1360
  key: string; // 번역 키
@@ -1203,7 +1381,13 @@ export function serverTranslate({
1203
1381
  }
1204
1382
 
1205
1383
  // 번역 찾기
1206
- const result = findInTranslations(translations, key, language, fallbackLanguage, missingKeyHandler);
1384
+ const result = findInTranslations(
1385
+ translations,
1386
+ key,
1387
+ language,
1388
+ fallbackLanguage,
1389
+ missingKeyHandler,
1390
+ );
1207
1391
 
1208
1392
  // 캐시에 저장
1209
1393
  if (cache && result) {
@@ -1222,7 +1406,7 @@ function findInTranslations(
1222
1406
  key: string,
1223
1407
  language: string,
1224
1408
  fallbackLanguage: string,
1225
- missingKeyHandler: (key: string) => string
1409
+ missingKeyHandler: (key: string) => string,
1226
1410
  ): string {
1227
1411
  const { namespace, key: actualKey } = parseKey(key);
1228
1412
 
@@ -1235,49 +1419,62 @@ function findInTranslations(
1235
1419
 
1236
1420
  // 폴백 언어에서 찾기
1237
1421
  if (language !== fallbackLanguage) {
1238
- result = findInNamespace(translations, namespace, actualKey, fallbackLanguage);
1422
+ result = findInNamespace(
1423
+ translations,
1424
+ namespace,
1425
+ actualKey,
1426
+ fallbackLanguage,
1427
+ );
1239
1428
  if (result) {
1240
1429
  return result;
1241
1430
  }
1242
1431
  }
1243
1432
 
1244
- return '';
1433
+ return "";
1245
1434
  }
1246
1435
 
1247
1436
  function findInNamespace(
1248
1437
  translations: Record<string, unknown>,
1249
1438
  namespace: string,
1250
1439
  key: string,
1251
- language: string
1440
+ language: string,
1252
1441
  ): string {
1253
1442
  // 언어 데이터 가져오기
1254
1443
  const languageData = translations[language];
1255
1444
 
1256
1445
  // 언어 데이터가 객체인지 확인
1257
- if (!languageData || typeof languageData !== 'object' || Array.isArray(languageData)) {
1258
- return '';
1446
+ if (
1447
+ !languageData ||
1448
+ typeof languageData !== "object" ||
1449
+ Array.isArray(languageData)
1450
+ ) {
1451
+ return "";
1259
1452
  }
1260
1453
 
1261
1454
  // 네임스페이스 데이터 가져오기
1262
1455
  const namespaceData = (languageData as Record<string, unknown>)[namespace];
1263
1456
 
1264
- if (!namespaceData || typeof namespaceData !== 'object' || Array.isArray(namespaceData)) {
1265
- return '';
1457
+ if (
1458
+ !namespaceData ||
1459
+ typeof namespaceData !== "object" ||
1460
+ Array.isArray(namespaceData)
1461
+ ) {
1462
+ return "";
1266
1463
  }
1267
1464
 
1268
1465
  // 타입 단언: namespaceData는 객체임을 확인했으므로 Record로 단언
1269
1466
  const data = namespaceData as Record<string, unknown>;
1270
1467
 
1271
1468
  // 직접 키 매칭
1272
- if (data[key] && typeof data[key] === 'string') {
1469
+ if (data[key] && typeof data[key] === "string") {
1273
1470
  return data[key] as string;
1274
1471
  }
1275
1472
 
1276
1473
  // 중첩 키 매칭
1277
1474
  const nestedValue = getNestedValue(namespaceData, key);
1278
- if (typeof nestedValue === 'string') {
1475
+ if (typeof nestedValue === "string") {
1279
1476
  return nestedValue;
1280
1477
  }
1281
1478
 
1282
- return '';
1283
- }
1479
+ return "";
1480
+ }