@hua-labs/i18n-core 2.0.0 → 2.0.4

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.
Files changed (78) hide show
  1. package/README.md +57 -597
  2. package/dist/chunk-F4PDBJLO.mjs +973 -0
  3. package/dist/chunk-F4PDBJLO.mjs.map +1 -0
  4. package/dist/index.d.mts +249 -0
  5. package/dist/index.d.ts +117 -30
  6. package/dist/index.js +1818 -177
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +845 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/dist/server-4TeBq6hp.d.mts +367 -0
  11. package/dist/server-4TeBq6hp.d.ts +367 -0
  12. package/dist/server.d.mts +1 -0
  13. package/dist/server.d.ts +1 -0
  14. package/dist/server.js +977 -0
  15. package/dist/server.js.map +1 -0
  16. package/dist/server.mjs +3 -0
  17. package/dist/server.mjs.map +1 -0
  18. package/package.json +42 -19
  19. package/src/__tests__/debug-tools.test.ts +359 -0
  20. package/src/__tests__/default-translations.test.ts +179 -0
  21. package/src/__tests__/i18n-resource.test.ts +137 -0
  22. package/src/__tests__/lazy-loader.test.ts +109 -0
  23. package/src/__tests__/missing-key-overlay.test.tsx +339 -0
  24. package/src/__tests__/translator-factory.test.ts +120 -0
  25. package/src/__tests__/translator.test.ts +442 -0
  26. package/src/__tests__/types.test.ts +211 -0
  27. package/src/__tests__/useI18n.test.tsx +181 -0
  28. package/src/__tests__/useTranslation.test.tsx +110 -0
  29. package/src/components/MissingKeyOverlay.tsx +1 -1
  30. package/src/core/lazy-loader.ts +2 -2
  31. package/src/core/translator.tsx +151 -62
  32. package/src/hooks/useI18n.tsx +96 -115
  33. package/src/hooks/useTranslation.tsx +12 -10
  34. package/src/index.ts +102 -5
  35. package/src/server.ts +9 -0
  36. package/src/types/index.ts +67 -12
  37. package/LICENSE +0 -21
  38. package/dist/components/MissingKeyOverlay.d.ts +0 -33
  39. package/dist/components/MissingKeyOverlay.d.ts.map +0 -1
  40. package/dist/components/MissingKeyOverlay.js +0 -138
  41. package/dist/components/MissingKeyOverlay.js.map +0 -1
  42. package/dist/core/debug-tools.d.ts +0 -37
  43. package/dist/core/debug-tools.d.ts.map +0 -1
  44. package/dist/core/debug-tools.js +0 -241
  45. package/dist/core/debug-tools.js.map +0 -1
  46. package/dist/core/i18n-resource.d.ts +0 -59
  47. package/dist/core/i18n-resource.d.ts.map +0 -1
  48. package/dist/core/i18n-resource.js +0 -153
  49. package/dist/core/i18n-resource.js.map +0 -1
  50. package/dist/core/lazy-loader.d.ts +0 -82
  51. package/dist/core/lazy-loader.d.ts.map +0 -1
  52. package/dist/core/lazy-loader.js +0 -193
  53. package/dist/core/lazy-loader.js.map +0 -1
  54. package/dist/core/translator-factory.d.ts +0 -50
  55. package/dist/core/translator-factory.d.ts.map +0 -1
  56. package/dist/core/translator-factory.js +0 -117
  57. package/dist/core/translator-factory.js.map +0 -1
  58. package/dist/core/translator.d.ts +0 -202
  59. package/dist/core/translator.d.ts.map +0 -1
  60. package/dist/core/translator.js +0 -912
  61. package/dist/core/translator.js.map +0 -1
  62. package/dist/hooks/useI18n.d.ts +0 -39
  63. package/dist/hooks/useI18n.d.ts.map +0 -1
  64. package/dist/hooks/useI18n.js +0 -531
  65. package/dist/hooks/useI18n.js.map +0 -1
  66. package/dist/hooks/useTranslation.d.ts +0 -55
  67. package/dist/hooks/useTranslation.d.ts.map +0 -1
  68. package/dist/hooks/useTranslation.js +0 -58
  69. package/dist/hooks/useTranslation.js.map +0 -1
  70. package/dist/index.d.ts.map +0 -1
  71. package/dist/types/index.d.ts +0 -162
  72. package/dist/types/index.d.ts.map +0 -1
  73. package/dist/types/index.js +0 -191
  74. package/dist/types/index.js.map +0 -1
  75. package/dist/utils/default-translations.d.ts +0 -20
  76. package/dist/utils/default-translations.d.ts.map +0 -1
  77. package/dist/utils/default-translations.js +0 -123
  78. package/dist/utils/default-translations.js.map +0 -1
@@ -0,0 +1,973 @@
1
+ // src/types/index.ts
2
+ var PLURAL_CATEGORIES = /* @__PURE__ */ new Set(["zero", "one", "two", "few", "many", "other"]);
3
+ function isPluralValue(value) {
4
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
5
+ const obj = value;
6
+ const keys = Object.keys(obj);
7
+ return keys.length > 0 && keys.every((k) => PLURAL_CATEGORIES.has(k)) && Object.values(obj).every((v) => typeof v === "string") && typeof obj.other === "string";
8
+ }
9
+ function isTranslationNamespace(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function isLanguageConfig(value) {
13
+ return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.name === "string" && typeof value.nativeName === "string";
14
+ }
15
+ function validateI18nConfig(config) {
16
+ if (!config || typeof config !== "object") {
17
+ return false;
18
+ }
19
+ const c = config;
20
+ return typeof c.defaultLanguage === "string" && Array.isArray(c.supportedLanguages) && c.supportedLanguages.every(isLanguageConfig) && typeof c.loadTranslations === "function";
21
+ }
22
+ function isRecoverableError(error) {
23
+ const recoverableCodes = [
24
+ "LOAD_FAILED",
25
+ "NETWORK_ERROR",
26
+ "CACHE_ERROR"
27
+ ];
28
+ return recoverableCodes.includes(error.code) && (error.retryCount || 0) < (error.maxRetries || 3);
29
+ }
30
+
31
+ // src/core/translator.tsx
32
+ var Translator = class {
33
+ constructor(config) {
34
+ this.cache = /* @__PURE__ */ new Map();
35
+ this.pluralRulesCache = /* @__PURE__ */ new Map();
36
+ this.loadedNamespaces = /* @__PURE__ */ new Set();
37
+ this.loadingPromises = /* @__PURE__ */ new Map();
38
+ this.allTranslations = {};
39
+ this.isInitialized = false;
40
+ this.initializationError = null;
41
+ this.currentLang = "en";
42
+ this.cacheStats = {
43
+ hits: 0,
44
+ misses: 0
45
+ };
46
+ // 번역 로드 완료 시 React 리렌더링을 위한 콜백
47
+ this.onTranslationLoadedCallbacks = /* @__PURE__ */ new Set();
48
+ // 언어 변경 시 React 리렌더링을 위한 콜백
49
+ this.onLanguageChangedCallbacks = /* @__PURE__ */ new Set();
50
+ // 디바운싱을 위한 타이머
51
+ this.notifyTimer = null;
52
+ // 최근 알림한 네임스페이스 (중복 알림 방지)
53
+ this.recentlyNotified = /* @__PURE__ */ new Set();
54
+ if (!validateI18nConfig(config)) {
55
+ throw new Error("Invalid I18nConfig provided");
56
+ }
57
+ this.config = {
58
+ fallbackLanguage: "en",
59
+ namespaces: ["common"],
60
+ debug: false,
61
+ missingKeyHandler: (key) => key,
62
+ errorHandler: (error) => {
63
+ },
64
+ ...config
65
+ };
66
+ this.currentLang = config.defaultLanguage;
67
+ if (config.initialTranslations) {
68
+ this.allTranslations = config.initialTranslations;
69
+ for (const [language, namespaces] of Object.entries(config.initialTranslations)) {
70
+ for (const namespace of Object.keys(namespaces)) {
71
+ this.loadedNamespaces.add(`${language}:${namespace}`);
72
+ }
73
+ }
74
+ this.isInitialized = true;
75
+ }
76
+ }
77
+ /**
78
+ * 번역 로드 완료 콜백 등록
79
+ */
80
+ onTranslationLoaded(callback) {
81
+ this.onTranslationLoadedCallbacks.add(callback);
82
+ return () => {
83
+ this.onTranslationLoadedCallbacks.delete(callback);
84
+ };
85
+ }
86
+ /**
87
+ * 언어 변경 콜백 등록
88
+ */
89
+ onLanguageChanged(callback) {
90
+ this.onLanguageChangedCallbacks.add(callback);
91
+ return () => {
92
+ this.onLanguageChangedCallbacks.delete(callback);
93
+ };
94
+ }
95
+ /**
96
+ * 언어 변경 이벤트 발생
97
+ */
98
+ notifyLanguageChanged(language) {
99
+ this.onLanguageChangedCallbacks.forEach((callback) => {
100
+ try {
101
+ callback(language);
102
+ } catch (error) {
103
+ if (this.config.debug) {
104
+ console.error("Error in language changed callback:", error);
105
+ }
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * 번역 로드 완료 이벤트 발생 (디바운싱 적용)
111
+ */
112
+ notifyTranslationLoaded(language, namespace) {
113
+ const cacheKey = `${language}:${namespace}`;
114
+ if (this.recentlyNotified.has(cacheKey)) {
115
+ return;
116
+ }
117
+ this.recentlyNotified.add(cacheKey);
118
+ if (this.notifyTimer) {
119
+ clearTimeout(this.notifyTimer);
120
+ }
121
+ this.notifyTimer = setTimeout(() => {
122
+ this.onTranslationLoadedCallbacks.forEach((callback) => {
123
+ try {
124
+ callback();
125
+ } catch (error) {
126
+ if (this.config.debug) {
127
+ console.warn("Error in translation loaded callback:", error);
128
+ }
129
+ }
130
+ });
131
+ setTimeout(() => {
132
+ this.recentlyNotified.clear();
133
+ }, 100);
134
+ this.notifyTimer = null;
135
+ }, 50);
136
+ }
137
+ /**
138
+ * 모든 번역 데이터를 미리 로드 (hua-api 스타일)
139
+ */
140
+ async initialize() {
141
+ if (this.isInitialized) {
142
+ if (this.config.debug) {
143
+ console.log("\u{1F6AB} [TRANSLATOR] Already initialized, skipping");
144
+ }
145
+ return;
146
+ }
147
+ if (this.config.debug) {
148
+ console.log("\u{1F680} [TRANSLATOR] Starting initialization...");
149
+ }
150
+ try {
151
+ if (!this.allTranslations) {
152
+ this.allTranslations = {};
153
+ }
154
+ const languages = [this.currentLang];
155
+ if (this.config.fallbackLanguage && this.config.fallbackLanguage !== this.currentLang) {
156
+ languages.push(this.config.fallbackLanguage);
157
+ }
158
+ const skipNamespaces = /* @__PURE__ */ new Set();
159
+ for (const language of languages) {
160
+ if (this.allTranslations[language]) {
161
+ for (const namespace of Object.keys(this.allTranslations[language])) {
162
+ skipNamespaces.add(`${language}:${namespace}`);
163
+ }
164
+ }
165
+ }
166
+ if (this.config.debug) {
167
+ console.log("\u{1F30D} [TRANSLATOR] Initializing translator with languages:", languages);
168
+ console.log("\u{1F4CD} [TRANSLATOR] Current language:", this.currentLang);
169
+ console.log("\u{1F4E6} [TRANSLATOR] Config namespaces:", this.config.namespaces);
170
+ }
171
+ for (const language of languages) {
172
+ if (this.config.debug) {
173
+ console.log("Processing language:", language);
174
+ }
175
+ if (!this.allTranslations[language]) {
176
+ this.allTranslations[language] = {};
177
+ }
178
+ for (const namespace of this.config.namespaces || []) {
179
+ const cacheKey = `${language}:${namespace}`;
180
+ if (skipNamespaces.has(cacheKey)) {
181
+ if (this.config.debug) {
182
+ console.log("\u23ED\uFE0F [TRANSLATOR] Skipping", namespace, "for", language, "(already loaded from SSR)");
183
+ }
184
+ continue;
185
+ }
186
+ if (this.config.debug) {
187
+ console.log("Loading namespace:", namespace, "for language:", language);
188
+ }
189
+ try {
190
+ const data = await this.safeLoadTranslations(language, namespace);
191
+ if (this.config.debug) {
192
+ console.log("Loaded data for", language, namespace, ":", data);
193
+ }
194
+ this.allTranslations[language][namespace] = data;
195
+ this.loadedNamespaces.add(`${language}:${namespace}`);
196
+ } catch (error) {
197
+ const translationError = this.createTranslationError(
198
+ "LOAD_FAILED",
199
+ error,
200
+ language,
201
+ namespace
202
+ );
203
+ this.logError(translationError);
204
+ if (isRecoverableError(translationError)) {
205
+ if (language !== this.config.fallbackLanguage) {
206
+ try {
207
+ const fallbackData = await this.safeLoadTranslations(this.config.fallbackLanguage || "en", namespace);
208
+ this.allTranslations[language][namespace] = fallbackData;
209
+ this.loadedNamespaces.add(`${language}:${namespace}`);
210
+ if (this.config.debug) {
211
+ console.log("Using fallback data for", language, namespace);
212
+ }
213
+ } catch (fallbackError) {
214
+ const fallbackTranslationError = this.createTranslationError(
215
+ "FALLBACK_LOAD_FAILED",
216
+ fallbackError,
217
+ this.config.fallbackLanguage,
218
+ namespace
219
+ );
220
+ this.logError(fallbackTranslationError);
221
+ this.allTranslations[language][namespace] = {};
222
+ }
223
+ } else {
224
+ this.allTranslations[language][namespace] = {};
225
+ }
226
+ } else {
227
+ this.allTranslations[language][namespace] = {};
228
+ }
229
+ }
230
+ }
231
+ }
232
+ this.isInitialized = true;
233
+ if (this.config.debug) {
234
+ console.log("Translator initialized successfully");
235
+ console.log("Loaded translations:", this.allTranslations);
236
+ }
237
+ } catch (error) {
238
+ this.initializationError = this.createTranslationError(
239
+ "INITIALIZATION_FAILED",
240
+ error
241
+ );
242
+ this.logError(this.initializationError);
243
+ this.isInitialized = true;
244
+ if (this.config.debug) {
245
+ console.warn("Translator initialized with errors, using fallback translations");
246
+ }
247
+ }
248
+ }
249
+ /**
250
+ * 초기화되지 않은 상태에서 번역 시도
251
+ */
252
+ translateBeforeInitialized(key, targetLang) {
253
+ if (this.config.debug) {
254
+ console.warn("Translator not initialized. Call initialize() first.");
255
+ }
256
+ const { namespace, key: actualKey } = this.parseKey(key);
257
+ const result = this.findInNamespace(namespace, actualKey, targetLang);
258
+ if (result) {
259
+ if (this.config.debug) {
260
+ console.log(`\u2705 [TRANSLATOR] Found fallback translation from initialTranslations:`, result);
261
+ }
262
+ return result;
263
+ }
264
+ if (this.config.debug) {
265
+ const translations = this.allTranslations[targetLang]?.[namespace];
266
+ console.log(`\u{1F50D} [TRANSLATOR] Not initialized, fallback failed:`, {
267
+ namespace,
268
+ actualKey,
269
+ hasTranslations: !!translations,
270
+ translationsKeys: translations ? Object.keys(translations) : []
271
+ });
272
+ }
273
+ return this.config.missingKeyHandler?.(key, targetLang, "default") || key;
274
+ }
275
+ /**
276
+ * 다른 로드된 언어에서 번역 찾기 (언어 변경 중 깜빡임 방지)
277
+ */
278
+ findInOtherLanguages(namespace, key, targetLang) {
279
+ if (!this.allTranslations || Object.keys(this.allTranslations).length === 0) {
280
+ return null;
281
+ }
282
+ const loadedLanguages = Object.keys(this.allTranslations);
283
+ for (const lang of loadedLanguages) {
284
+ if (lang !== targetLang) {
285
+ const result = this.findInNamespace(namespace, key, lang);
286
+ if (result) {
287
+ return result;
288
+ }
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ /**
294
+ * 폴백 언어에서 번역 찾기
295
+ */
296
+ findInFallbackLanguage(namespace, key, targetLang) {
297
+ const fallbackLang = this.config.fallbackLanguage || "en";
298
+ if (targetLang === fallbackLang) {
299
+ return null;
300
+ }
301
+ const result = this.findInNamespace(namespace, key, fallbackLang);
302
+ if (result) {
303
+ this.cacheStats.hits++;
304
+ return result;
305
+ }
306
+ return null;
307
+ }
308
+ /**
309
+ * 번역 키를 번역된 텍스트로 변환
310
+ */
311
+ translate(key, paramsOrLang, language) {
312
+ let params;
313
+ let targetLang;
314
+ if (typeof paramsOrLang === "string") {
315
+ targetLang = paramsOrLang;
316
+ } else if (typeof paramsOrLang === "object" && paramsOrLang !== null) {
317
+ params = paramsOrLang;
318
+ targetLang = language || this.currentLang;
319
+ } else {
320
+ targetLang = this.currentLang;
321
+ }
322
+ if (!this.isInitialized) {
323
+ const raw = this.translateBeforeInitialized(key, targetLang);
324
+ return params ? this.interpolate(raw, params) : raw;
325
+ }
326
+ const { namespace, key: actualKey } = this.parseKey(key);
327
+ let result = this.findInNamespace(namespace, actualKey, targetLang);
328
+ if (result) {
329
+ this.cacheStats.hits++;
330
+ return params ? this.interpolate(result, params) : result;
331
+ }
332
+ result = this.findInOtherLanguages(namespace, actualKey, targetLang);
333
+ if (result) {
334
+ return params ? this.interpolate(result, params) : result;
335
+ }
336
+ result = this.findInFallbackLanguage(namespace, actualKey, targetLang);
337
+ if (result) {
338
+ return params ? this.interpolate(result, params) : result;
339
+ }
340
+ this.cacheStats.misses++;
341
+ if (this.config.debug) {
342
+ const missing = this.config.missingKeyHandler?.(key, targetLang, namespace) || key;
343
+ return params ? this.interpolate(missing, params) : missing;
344
+ }
345
+ return "";
346
+ }
347
+ /**
348
+ * 네임스페이스에서 키 찾기
349
+ */
350
+ findInNamespace(namespace, key, language) {
351
+ const translations = this.allTranslations[language]?.[namespace];
352
+ if (!translations) {
353
+ const cacheKey = `${language}:${namespace}`;
354
+ if (!this.loadedNamespaces.has(cacheKey) && !this.loadingPromises.has(cacheKey)) {
355
+ this.loadTranslationData(language, namespace).catch((error) => {
356
+ if (this.config.debug) {
357
+ console.warn(`\u26A0\uFE0F [TRANSLATOR] Auto-load failed for ${language}/${namespace}:`, error);
358
+ }
359
+ });
360
+ if (this.config.debug) {
361
+ console.warn(`\u274C [TRANSLATOR] No translations found for ${language}/${namespace}, attempting auto-load...`);
362
+ }
363
+ }
364
+ return "";
365
+ }
366
+ const directValue = translations[key];
367
+ if (this.isStringValue(directValue)) {
368
+ return directValue;
369
+ }
370
+ if (this.isStringArray(directValue)) {
371
+ return directValue[Math.floor(Math.random() * directValue.length)];
372
+ }
373
+ const nestedValue = this.getNestedValue(translations, key);
374
+ if (this.isStringValue(nestedValue)) {
375
+ return nestedValue;
376
+ }
377
+ if (this.isStringArray(nestedValue)) {
378
+ return nestedValue[Math.floor(Math.random() * nestedValue.length)];
379
+ }
380
+ if (this.config.debug) {
381
+ console.warn(`\u274C [TRANSLATOR] No match found for key: ${key} in ${language}/${namespace}`);
382
+ }
383
+ return "";
384
+ }
385
+ /**
386
+ * 중첩된 객체에서 값을 가져오기
387
+ * 배열도 지원: 최종 값이 string[]이면 그대로 반환
388
+ */
389
+ getNestedValue(obj, path) {
390
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
391
+ return void 0;
392
+ }
393
+ return path.split(".").reduce((current, key) => {
394
+ if (current == null) return void 0;
395
+ if (Array.isArray(current)) {
396
+ const idx = Number(key);
397
+ return Number.isInteger(idx) ? current[idx] : void 0;
398
+ }
399
+ if (typeof current === "object" && key in current) {
400
+ return current[key];
401
+ }
402
+ return void 0;
403
+ }, obj);
404
+ }
405
+ /**
406
+ * 문자열 값인지 확인하는 타입 가드
407
+ */
408
+ isStringValue(value) {
409
+ return typeof value === "string" && value.length > 0;
410
+ }
411
+ /**
412
+ * string[] 배열인지 확인하는 타입 가드
413
+ * 배열 값이 t()에 전달되면 랜덤으로 하나를 선택하여 반환
414
+ */
415
+ isStringArray(value) {
416
+ return Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string");
417
+ }
418
+ /**
419
+ * 원시 값 가져오기 (배열, 객체 포함)
420
+ */
421
+ getRawValue(key, language) {
422
+ const targetLang = language || this.currentLang;
423
+ if (!this.isInitialized) {
424
+ if (this.config.debug) {
425
+ console.warn("Translator not initialized. Call initialize() first.");
426
+ }
427
+ return void 0;
428
+ }
429
+ const { namespace, key: actualKey } = this.parseKey(key);
430
+ const translations = this.allTranslations[targetLang]?.[namespace];
431
+ if (!translations) {
432
+ return void 0;
433
+ }
434
+ if (actualKey in translations) {
435
+ return translations[actualKey];
436
+ }
437
+ const nestedValue = this.getNestedValue(translations, actualKey);
438
+ if (nestedValue !== void 0) {
439
+ return nestedValue;
440
+ }
441
+ if (targetLang !== this.config.fallbackLanguage) {
442
+ const fallbackTranslations = this.allTranslations[this.config.fallbackLanguage || "en"]?.[namespace];
443
+ if (fallbackTranslations) {
444
+ if (actualKey in fallbackTranslations) {
445
+ return fallbackTranslations[actualKey];
446
+ }
447
+ const fallbackNestedValue = this.getNestedValue(fallbackTranslations, actualKey);
448
+ if (fallbackNestedValue !== void 0) {
449
+ return fallbackNestedValue;
450
+ }
451
+ }
452
+ }
453
+ return void 0;
454
+ }
455
+ /**
456
+ * 배열 번역 값 가져오기 (타입 안전)
457
+ */
458
+ tArray(key, language) {
459
+ const raw = this.getRawValue(key, language);
460
+ if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) {
461
+ return raw;
462
+ }
463
+ if (process.env.NODE_ENV === "development") {
464
+ console.warn(`tArray: "${key}" is not a string array`);
465
+ }
466
+ return [];
467
+ }
468
+ /**
469
+ * Intl.PluralRules 인스턴스 (언어별 캐시)
470
+ */
471
+ getPluralRules(language) {
472
+ let rules = this.pluralRulesCache.get(language);
473
+ if (!rules) {
474
+ rules = new Intl.PluralRules(language);
475
+ this.pluralRulesCache.set(language, rules);
476
+ }
477
+ return rules;
478
+ }
479
+ /**
480
+ * 복수형 번역 (ICU / Intl.PluralRules 기반)
481
+ *
482
+ * JSON: { "other": "총 {count}개" } (ko)
483
+ * { "one": "{count} item", "other": "{count} items" } (en)
484
+ *
485
+ * tPlural('common:total_count', 1) → en: "1 item" / ko: "총 1개"
486
+ * tPlural('common:total_count', 5) → en: "5 items" / ko: "총 5개"
487
+ */
488
+ tPlural(key, count, params, language) {
489
+ const targetLang = language || this.currentLang;
490
+ const raw = this.getRawValue(key, targetLang);
491
+ const mergedParams = { count, ...params };
492
+ if (isPluralValue(raw)) {
493
+ const category = this.getPluralRules(targetLang).select(count);
494
+ const text = raw[category] ?? raw.other;
495
+ return this.interpolate(text, mergedParams);
496
+ }
497
+ if (typeof raw === "string") {
498
+ return this.interpolate(raw, mergedParams);
499
+ }
500
+ if (this.config.debug) {
501
+ return this.interpolate(key, mergedParams);
502
+ }
503
+ return "";
504
+ }
505
+ /**
506
+ * 매개변수 보간
507
+ *
508
+ * 지원 형식:
509
+ * - {key} - 단일 중괄호 (일반적인 i18n 형식)
510
+ * - {{key}} - 이중 중괄호 (하위 호환성)
511
+ */
512
+ interpolate(text, params) {
513
+ return text.replace(/\{\{?(\w+)\}?\}/g, (match, key) => {
514
+ const value = params[key];
515
+ return value !== void 0 ? String(value) : match;
516
+ });
517
+ }
518
+ /**
519
+ * 언어 설정
520
+ */
521
+ setLanguage(language) {
522
+ if (this.currentLang === language) {
523
+ return;
524
+ }
525
+ const previousLanguage = this.currentLang;
526
+ this.currentLang = language;
527
+ this.notifyLanguageChanged(language);
528
+ if (!this.allTranslations[language]) {
529
+ this.loadLanguageData(language).catch((error) => {
530
+ if (this.config.debug) {
531
+ console.warn("Failed to load language data:", error);
532
+ }
533
+ });
534
+ }
535
+ if (this.config.debug) {
536
+ console.log(`\u{1F310} [TRANSLATOR] Language changed: ${previousLanguage} -> ${language}`);
537
+ }
538
+ }
539
+ /**
540
+ * 언어 데이터 로드
541
+ */
542
+ async loadLanguageData(language) {
543
+ if (!this.allTranslations[language]) {
544
+ this.allTranslations[language] = {};
545
+ }
546
+ for (const namespace of this.config.namespaces || []) {
547
+ try {
548
+ const data = await this.safeLoadTranslations(language, namespace);
549
+ this.allTranslations[language][namespace] = data;
550
+ this.loadedNamespaces.add(`${language}:${namespace}`);
551
+ this.notifyTranslationLoaded(language, namespace);
552
+ } catch (error) {
553
+ const translationError = this.createTranslationError(
554
+ "LOAD_FAILED",
555
+ error,
556
+ language,
557
+ namespace
558
+ );
559
+ this.logError(translationError);
560
+ if (isRecoverableError(translationError)) {
561
+ this.allTranslations[language][namespace] = {};
562
+ } else {
563
+ this.allTranslations[language][namespace] = {};
564
+ }
565
+ }
566
+ }
567
+ }
568
+ /**
569
+ * 현재 언어 가져오기
570
+ */
571
+ getCurrentLanguage() {
572
+ return this.currentLang;
573
+ }
574
+ /**
575
+ * 지원되는 언어 목록 가져오기
576
+ */
577
+ getSupportedLanguages() {
578
+ return this.config.supportedLanguages?.map((lang) => lang.code) || [];
579
+ }
580
+ /**
581
+ * 초기화 완료 여부 확인
582
+ */
583
+ isReady() {
584
+ return this.isInitialized && !this.initializationError;
585
+ }
586
+ /**
587
+ * 초기화 오류 가져오기
588
+ */
589
+ getInitializationError() {
590
+ return this.initializationError;
591
+ }
592
+ /**
593
+ * 캐시 클리어
594
+ */
595
+ clearCache() {
596
+ this.cache.clear();
597
+ this.cacheStats = { hits: 0, misses: 0 };
598
+ if (this.config.debug) {
599
+ console.log("Cache cleared");
600
+ }
601
+ }
602
+ /**
603
+ * 캐시 엔트리 설정
604
+ */
605
+ setCacheEntry(key, data) {
606
+ this.cache.set(key, {
607
+ data,
608
+ timestamp: Date.now(),
609
+ ttl: 5 * 60 * 1e3
610
+ // 5분
611
+ });
612
+ }
613
+ /**
614
+ * 캐시 엔트리 가져오기
615
+ */
616
+ getCacheEntry(key) {
617
+ const entry = this.cache.get(key);
618
+ if (!entry) {
619
+ return null;
620
+ }
621
+ if (Date.now() - entry.timestamp > entry.ttl) {
622
+ this.cache.delete(key);
623
+ return null;
624
+ }
625
+ return entry.data;
626
+ }
627
+ /**
628
+ * 번역 오류 생성
629
+ */
630
+ createTranslationError(code, originalError, language, namespace, key) {
631
+ return {
632
+ name: "TranslationError",
633
+ code,
634
+ message: originalError.message,
635
+ originalError,
636
+ language,
637
+ namespace,
638
+ key,
639
+ timestamp: Date.now(),
640
+ stack: originalError.stack
641
+ };
642
+ }
643
+ /**
644
+ * 오류 로깅
645
+ */
646
+ logError(error) {
647
+ if (this.config.errorHandler) {
648
+ this.config.errorHandler(error, error.language || "", error.namespace || "");
649
+ }
650
+ }
651
+ /**
652
+ * 재시도 작업
653
+ */
654
+ async retryOperation(operation, error, context) {
655
+ const maxRetries = 3;
656
+ let lastError = error;
657
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
658
+ try {
659
+ return await operation();
660
+ } catch (retryError) {
661
+ lastError = this.createTranslationError(
662
+ "RETRY_FAILED",
663
+ retryError,
664
+ context.language,
665
+ context.namespace,
666
+ context.key
667
+ );
668
+ if (attempt === maxRetries) {
669
+ break;
670
+ }
671
+ await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3));
672
+ }
673
+ }
674
+ throw lastError;
675
+ }
676
+ /**
677
+ * 안전한 번역 로드
678
+ */
679
+ async safeLoadTranslations(language, namespace) {
680
+ if (this.config.debug) {
681
+ console.log(`\u{1F4E5} [TRANSLATOR] safeLoadTranslations called:`, { language, namespace });
682
+ }
683
+ const loadOperation = async () => {
684
+ if (!this.config.loadTranslations) {
685
+ throw new Error("No translation loader configured");
686
+ }
687
+ if (this.config.debug) {
688
+ console.log(`\u{1F504} [TRANSLATOR] Calling loadTranslations for:`, { language, namespace });
689
+ }
690
+ const data = await this.config.loadTranslations(language, namespace);
691
+ if (this.config.debug) {
692
+ console.log(`\u{1F4E6} [TRANSLATOR] loadTranslations returned:`, data);
693
+ }
694
+ if (!isTranslationNamespace(data)) {
695
+ throw new Error(`Invalid translation data for ${language}:${namespace}`);
696
+ }
697
+ return data;
698
+ };
699
+ try {
700
+ return await loadOperation();
701
+ } catch (error) {
702
+ const translationError = this.createTranslationError(
703
+ "LOAD_FAILED",
704
+ error,
705
+ language,
706
+ namespace
707
+ );
708
+ return this.retryOperation(loadOperation, translationError, { language, namespace });
709
+ }
710
+ }
711
+ /**
712
+ * 디버그 정보
713
+ */
714
+ debug() {
715
+ return {
716
+ isInitialized: this.isInitialized,
717
+ currentLanguage: this.currentLang,
718
+ loadedNamespaces: Array.from(this.loadedNamespaces),
719
+ cacheStats: this.cacheStats,
720
+ cacheSize: this.cache.size,
721
+ allTranslations: this.allTranslations,
722
+ initializationError: this.initializationError,
723
+ config: this.config
724
+ };
725
+ }
726
+ /**
727
+ * SSR에서 하이드레이션
728
+ */
729
+ hydrateFromSSR(translations) {
730
+ this.allTranslations = translations;
731
+ this.isInitialized = true;
732
+ for (const [language, namespaces] of Object.entries(translations)) {
733
+ for (const namespace of Object.keys(namespaces)) {
734
+ this.loadedNamespaces.add(`${language}:${namespace}`);
735
+ }
736
+ }
737
+ }
738
+ /**
739
+ * 비동기 번역 (고급 기능)
740
+ */
741
+ async translateAsync(key, params) {
742
+ if (!this.isInitialized) {
743
+ await this.initialize();
744
+ }
745
+ const translated = this.translate(key);
746
+ if (!params) {
747
+ return translated;
748
+ }
749
+ return this.interpolate(translated, params);
750
+ }
751
+ /**
752
+ * 동기 번역 (고급 기능)
753
+ */
754
+ translateSync(key, params) {
755
+ if (!this.isInitialized) {
756
+ if (this.config.debug) {
757
+ console.warn("Translator not initialized for sync translation");
758
+ }
759
+ const { namespace } = this.parseKey(key);
760
+ return this.config.missingKeyHandler?.(key, this.currentLang, namespace) || key;
761
+ }
762
+ const translated = this.translate(key);
763
+ if (!params) {
764
+ return translated;
765
+ }
766
+ return this.interpolate(translated, params);
767
+ }
768
+ /**
769
+ * 키 파싱 (네임스페이스:키 형식)
770
+ *
771
+ * - 콜론(:)만 네임스페이스 구분자로 사용
772
+ * - 점(.)은 키 이름의 일부로 취급 (중첩 객체 접근용)
773
+ *
774
+ * @example
775
+ * parseKey("home:hero.badge") → { namespace: "home", key: "hero.badge" }
776
+ * parseKey("hero.badge") → { namespace: "common", key: "hero.badge" }
777
+ * parseKey("save") → { namespace: "common", key: "save" }
778
+ */
779
+ parseKey(key) {
780
+ const colonIndex = key.indexOf(":");
781
+ if (colonIndex !== -1) {
782
+ return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
783
+ }
784
+ return { namespace: "common", key };
785
+ }
786
+ /**
787
+ * 번역 데이터 로드 (고급 기능)
788
+ */
789
+ async loadTranslationData(language, namespace) {
790
+ const cacheKey = `${language}:${namespace}`;
791
+ if (this.loadedNamespaces.has(cacheKey)) {
792
+ const existing = this.allTranslations[language]?.[namespace];
793
+ if (existing) {
794
+ return existing;
795
+ }
796
+ }
797
+ const cached = this.getCacheEntry(cacheKey);
798
+ if (cached) {
799
+ if (!this.allTranslations[language]) {
800
+ this.allTranslations[language] = {};
801
+ }
802
+ this.allTranslations[language][namespace] = cached;
803
+ this.loadedNamespaces.add(cacheKey);
804
+ return cached;
805
+ }
806
+ const loadingPromise = this.loadingPromises.get(cacheKey);
807
+ if (loadingPromise) {
808
+ return loadingPromise;
809
+ }
810
+ const loadPromise = this._loadTranslationData(language, namespace);
811
+ this.loadingPromises.set(cacheKey, loadPromise);
812
+ try {
813
+ const data = await loadPromise;
814
+ if (!this.allTranslations[language]) {
815
+ this.allTranslations[language] = {};
816
+ }
817
+ this.allTranslations[language][namespace] = data;
818
+ this.loadedNamespaces.add(cacheKey);
819
+ this.setCacheEntry(cacheKey, data);
820
+ if (this.config.debug) {
821
+ console.log(`\u2705 [TRANSLATOR] Auto-loaded and saved ${language}/${namespace}`);
822
+ }
823
+ this.notifyTranslationLoaded(language, namespace);
824
+ return data;
825
+ } finally {
826
+ this.loadingPromises.delete(cacheKey);
827
+ }
828
+ }
829
+ /**
830
+ * 실제 번역 데이터 로드
831
+ */
832
+ async _loadTranslationData(language, namespace) {
833
+ if (!this.config.loadTranslations) {
834
+ throw new Error("No translation loader configured");
835
+ }
836
+ try {
837
+ const data = await this.config.loadTranslations(language, namespace);
838
+ if (!isTranslationNamespace(data)) {
839
+ throw new Error(`Invalid translation data for ${language}:${namespace}`);
840
+ }
841
+ return data;
842
+ } catch (error) {
843
+ const translationError = this.createTranslationError(
844
+ "LOAD_FAILED",
845
+ error,
846
+ language,
847
+ namespace
848
+ );
849
+ this.logError(translationError);
850
+ return {};
851
+ }
852
+ }
853
+ };
854
+ function ssrTranslate({
855
+ translations,
856
+ key,
857
+ language = "ko",
858
+ fallbackLanguage = "en",
859
+ missingKeyHandler = (key2) => key2
860
+ }) {
861
+ const { namespace, key: actualKey } = parseKey(key);
862
+ let result = ssrFindInNamespace(translations, namespace, actualKey, language);
863
+ if (result) {
864
+ return result;
865
+ }
866
+ if (language !== fallbackLanguage) {
867
+ result = ssrFindInNamespace(translations, namespace, actualKey, fallbackLanguage);
868
+ if (result) {
869
+ return result;
870
+ }
871
+ }
872
+ return missingKeyHandler(key);
873
+ }
874
+ function ssrFindInNamespace(translations, namespace, key, language, fallbackLanguage, missingKeyHandler) {
875
+ const namespaceData = translations[language]?.[namespace];
876
+ if (!namespaceData) {
877
+ return "";
878
+ }
879
+ const directValue = namespaceData[key];
880
+ if (isStringValue(directValue)) {
881
+ return directValue;
882
+ }
883
+ const nestedValue = getNestedValue(namespaceData, key);
884
+ if (isStringValue(nestedValue)) {
885
+ return nestedValue;
886
+ }
887
+ return "";
888
+ }
889
+ function getNestedValue(obj, path) {
890
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
891
+ return void 0;
892
+ }
893
+ return path.split(".").reduce((current, key) => {
894
+ if (current && typeof current === "object" && !Array.isArray(current) && key in current) {
895
+ return current[key];
896
+ }
897
+ return void 0;
898
+ }, obj);
899
+ }
900
+ function isStringValue(value) {
901
+ return typeof value === "string" && value.length > 0;
902
+ }
903
+ function parseKey(key) {
904
+ const colonIndex = key.indexOf(":");
905
+ if (colonIndex !== -1) {
906
+ return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
907
+ }
908
+ return { namespace: "common", key };
909
+ }
910
+ function serverTranslate({
911
+ translations,
912
+ key,
913
+ language = "ko",
914
+ fallbackLanguage = "en",
915
+ missingKeyHandler = (key2) => key2,
916
+ options = {}
917
+ }) {
918
+ const { cache, metrics, debug } = options;
919
+ if (cache) {
920
+ const cacheKey = `${language}:${key}`;
921
+ const cached = cache.get(cacheKey);
922
+ if (cached) {
923
+ if (metrics) metrics.hits++;
924
+ if (debug) console.log(`[CACHE HIT] ${cacheKey}`);
925
+ return cached;
926
+ }
927
+ }
928
+ const result = findInTranslations(translations, key, language, fallbackLanguage);
929
+ if (cache && result) {
930
+ const cacheKey = `${language}:${key}`;
931
+ cache.set(cacheKey, result);
932
+ }
933
+ if (metrics) metrics.misses++;
934
+ if (debug) console.log(`[TRANSLATE] ${key} -> ${result}`);
935
+ return result;
936
+ }
937
+ function findInTranslations(translations, key, language, fallbackLanguage, missingKeyHandler) {
938
+ const { namespace, key: actualKey } = parseKey(key);
939
+ let result = findInNamespace(translations, namespace, actualKey, language);
940
+ if (result) {
941
+ return result;
942
+ }
943
+ if (language !== fallbackLanguage) {
944
+ result = findInNamespace(translations, namespace, actualKey, fallbackLanguage);
945
+ if (result) {
946
+ return result;
947
+ }
948
+ }
949
+ return "";
950
+ }
951
+ function findInNamespace(translations, namespace, key, language) {
952
+ const languageData = translations[language];
953
+ if (!languageData || typeof languageData !== "object" || Array.isArray(languageData)) {
954
+ return "";
955
+ }
956
+ const namespaceData = languageData[namespace];
957
+ if (!namespaceData || typeof namespaceData !== "object" || Array.isArray(namespaceData)) {
958
+ return "";
959
+ }
960
+ const data = namespaceData;
961
+ if (data[key] && typeof data[key] === "string") {
962
+ return data[key];
963
+ }
964
+ const nestedValue = getNestedValue(namespaceData, key);
965
+ if (typeof nestedValue === "string") {
966
+ return nestedValue;
967
+ }
968
+ return "";
969
+ }
970
+
971
+ export { Translator, serverTranslate, ssrTranslate, validateI18nConfig };
972
+ //# sourceMappingURL=chunk-F4PDBJLO.mjs.map
973
+ //# sourceMappingURL=chunk-F4PDBJLO.mjs.map