@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.
- package/README.md +57 -597
- package/dist/chunk-F4PDBJLO.mjs +973 -0
- package/dist/chunk-F4PDBJLO.mjs.map +1 -0
- package/dist/index.d.mts +249 -0
- package/dist/index.d.ts +117 -30
- package/dist/index.js +1818 -177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +845 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server-4TeBq6hp.d.mts +367 -0
- package/dist/server-4TeBq6hp.d.ts +367 -0
- package/dist/server.d.mts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +977 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +3 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +42 -19
- package/src/__tests__/debug-tools.test.ts +359 -0
- package/src/__tests__/default-translations.test.ts +179 -0
- package/src/__tests__/i18n-resource.test.ts +137 -0
- package/src/__tests__/lazy-loader.test.ts +109 -0
- package/src/__tests__/missing-key-overlay.test.tsx +339 -0
- package/src/__tests__/translator-factory.test.ts +120 -0
- package/src/__tests__/translator.test.ts +442 -0
- package/src/__tests__/types.test.ts +211 -0
- package/src/__tests__/useI18n.test.tsx +181 -0
- package/src/__tests__/useTranslation.test.tsx +110 -0
- package/src/components/MissingKeyOverlay.tsx +1 -1
- package/src/core/lazy-loader.ts +2 -2
- package/src/core/translator.tsx +151 -62
- package/src/hooks/useI18n.tsx +96 -115
- package/src/hooks/useTranslation.tsx +12 -10
- package/src/index.ts +102 -5
- package/src/server.ts +9 -0
- package/src/types/index.ts +67 -12
- package/LICENSE +0 -21
- package/dist/components/MissingKeyOverlay.d.ts +0 -33
- package/dist/components/MissingKeyOverlay.d.ts.map +0 -1
- package/dist/components/MissingKeyOverlay.js +0 -138
- package/dist/components/MissingKeyOverlay.js.map +0 -1
- package/dist/core/debug-tools.d.ts +0 -37
- package/dist/core/debug-tools.d.ts.map +0 -1
- package/dist/core/debug-tools.js +0 -241
- package/dist/core/debug-tools.js.map +0 -1
- package/dist/core/i18n-resource.d.ts +0 -59
- package/dist/core/i18n-resource.d.ts.map +0 -1
- package/dist/core/i18n-resource.js +0 -153
- package/dist/core/i18n-resource.js.map +0 -1
- package/dist/core/lazy-loader.d.ts +0 -82
- package/dist/core/lazy-loader.d.ts.map +0 -1
- package/dist/core/lazy-loader.js +0 -193
- package/dist/core/lazy-loader.js.map +0 -1
- package/dist/core/translator-factory.d.ts +0 -50
- package/dist/core/translator-factory.d.ts.map +0 -1
- package/dist/core/translator-factory.js +0 -117
- package/dist/core/translator-factory.js.map +0 -1
- package/dist/core/translator.d.ts +0 -202
- package/dist/core/translator.d.ts.map +0 -1
- package/dist/core/translator.js +0 -912
- package/dist/core/translator.js.map +0 -1
- package/dist/hooks/useI18n.d.ts +0 -39
- package/dist/hooks/useI18n.d.ts.map +0 -1
- package/dist/hooks/useI18n.js +0 -531
- package/dist/hooks/useI18n.js.map +0 -1
- package/dist/hooks/useTranslation.d.ts +0 -55
- package/dist/hooks/useTranslation.d.ts.map +0 -1
- package/dist/hooks/useTranslation.js +0 -58
- package/dist/hooks/useTranslation.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -162
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -191
- package/dist/types/index.js.map +0 -1
- package/dist/utils/default-translations.d.ts +0 -20
- package/dist/utils/default-translations.d.ts.map +0 -1
- package/dist/utils/default-translations.js +0 -123
- package/dist/utils/default-translations.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,191 +1,1832 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
|
|
12
|
+
// src/types/index.ts
|
|
13
|
+
var PLURAL_CATEGORIES = /* @__PURE__ */ new Set(["zero", "one", "two", "few", "many", "other"]);
|
|
14
|
+
function isPluralValue(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
16
|
+
const obj = value;
|
|
17
|
+
const keys = Object.keys(obj);
|
|
18
|
+
return keys.length > 0 && keys.every((k) => PLURAL_CATEGORIES.has(k)) && Object.values(obj).every((v) => typeof v === "string") && typeof obj.other === "string";
|
|
19
|
+
}
|
|
20
|
+
function isTranslationNamespace(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function isLanguageConfig(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.name === "string" && typeof value.nativeName === "string";
|
|
25
|
+
}
|
|
26
|
+
function validateI18nConfig(config) {
|
|
27
|
+
if (!config || typeof config !== "object") {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const c = config;
|
|
31
|
+
return typeof c.defaultLanguage === "string" && Array.isArray(c.supportedLanguages) && c.supportedLanguages.every(isLanguageConfig) && typeof c.loadTranslations === "function";
|
|
32
|
+
}
|
|
33
|
+
function isRecoverableError(error) {
|
|
34
|
+
const recoverableCodes = [
|
|
35
|
+
"LOAD_FAILED",
|
|
36
|
+
"NETWORK_ERROR",
|
|
37
|
+
"CACHE_ERROR"
|
|
38
|
+
];
|
|
39
|
+
return recoverableCodes.includes(error.code) && (error.retryCount || 0) < (error.maxRetries || 3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/core/translator.tsx
|
|
43
|
+
var Translator = class {
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
46
|
+
this.pluralRulesCache = /* @__PURE__ */ new Map();
|
|
47
|
+
this.loadedNamespaces = /* @__PURE__ */ new Set();
|
|
48
|
+
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
49
|
+
this.allTranslations = {};
|
|
50
|
+
this.isInitialized = false;
|
|
51
|
+
this.initializationError = null;
|
|
52
|
+
this.currentLang = "en";
|
|
53
|
+
this.cacheStats = {
|
|
54
|
+
hits: 0,
|
|
55
|
+
misses: 0
|
|
56
|
+
};
|
|
57
|
+
// 번역 로드 완료 시 React 리렌더링을 위한 콜백
|
|
58
|
+
this.onTranslationLoadedCallbacks = /* @__PURE__ */ new Set();
|
|
59
|
+
// 언어 변경 시 React 리렌더링을 위한 콜백
|
|
60
|
+
this.onLanguageChangedCallbacks = /* @__PURE__ */ new Set();
|
|
61
|
+
// 디바운싱을 위한 타이머
|
|
62
|
+
this.notifyTimer = null;
|
|
63
|
+
// 최근 알림한 네임스페이스 (중복 알림 방지)
|
|
64
|
+
this.recentlyNotified = /* @__PURE__ */ new Set();
|
|
65
|
+
if (!validateI18nConfig(config)) {
|
|
66
|
+
throw new Error("Invalid I18nConfig provided");
|
|
67
|
+
}
|
|
68
|
+
this.config = {
|
|
69
|
+
fallbackLanguage: "en",
|
|
70
|
+
namespaces: ["common"],
|
|
71
|
+
debug: false,
|
|
72
|
+
missingKeyHandler: (key) => key,
|
|
73
|
+
errorHandler: (error) => {
|
|
74
|
+
},
|
|
75
|
+
...config
|
|
76
|
+
};
|
|
77
|
+
this.currentLang = config.defaultLanguage;
|
|
78
|
+
if (config.initialTranslations) {
|
|
79
|
+
this.allTranslations = config.initialTranslations;
|
|
80
|
+
for (const [language, namespaces] of Object.entries(config.initialTranslations)) {
|
|
81
|
+
for (const namespace of Object.keys(namespaces)) {
|
|
82
|
+
this.loadedNamespaces.add(`${language}:${namespace}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.isInitialized = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 번역 로드 완료 콜백 등록
|
|
90
|
+
*/
|
|
91
|
+
onTranslationLoaded(callback) {
|
|
92
|
+
this.onTranslationLoadedCallbacks.add(callback);
|
|
93
|
+
return () => {
|
|
94
|
+
this.onTranslationLoadedCallbacks.delete(callback);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 언어 변경 콜백 등록
|
|
99
|
+
*/
|
|
100
|
+
onLanguageChanged(callback) {
|
|
101
|
+
this.onLanguageChangedCallbacks.add(callback);
|
|
102
|
+
return () => {
|
|
103
|
+
this.onLanguageChangedCallbacks.delete(callback);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 언어 변경 이벤트 발생
|
|
108
|
+
*/
|
|
109
|
+
notifyLanguageChanged(language) {
|
|
110
|
+
this.onLanguageChangedCallbacks.forEach((callback) => {
|
|
111
|
+
try {
|
|
112
|
+
callback(language);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (this.config.debug) {
|
|
115
|
+
console.error("Error in language changed callback:", error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 번역 로드 완료 이벤트 발생 (디바운싱 적용)
|
|
122
|
+
*/
|
|
123
|
+
notifyTranslationLoaded(language, namespace) {
|
|
124
|
+
const cacheKey = `${language}:${namespace}`;
|
|
125
|
+
if (this.recentlyNotified.has(cacheKey)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.recentlyNotified.add(cacheKey);
|
|
129
|
+
if (this.notifyTimer) {
|
|
130
|
+
clearTimeout(this.notifyTimer);
|
|
131
|
+
}
|
|
132
|
+
this.notifyTimer = setTimeout(() => {
|
|
133
|
+
this.onTranslationLoadedCallbacks.forEach((callback) => {
|
|
44
134
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
135
|
+
callback();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (this.config.debug) {
|
|
138
|
+
console.warn("Error in translation loaded callback:", error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
this.recentlyNotified.clear();
|
|
144
|
+
}, 100);
|
|
145
|
+
this.notifyTimer = null;
|
|
146
|
+
}, 50);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 모든 번역 데이터를 미리 로드 (hua-api 스타일)
|
|
150
|
+
*/
|
|
151
|
+
async initialize() {
|
|
152
|
+
if (this.isInitialized) {
|
|
153
|
+
if (this.config.debug) {
|
|
154
|
+
console.log("\u{1F6AB} [TRANSLATOR] Already initialized, skipping");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (this.config.debug) {
|
|
159
|
+
console.log("\u{1F680} [TRANSLATOR] Starting initialization...");
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
if (!this.allTranslations) {
|
|
163
|
+
this.allTranslations = {};
|
|
164
|
+
}
|
|
165
|
+
const languages = [this.currentLang];
|
|
166
|
+
if (this.config.fallbackLanguage && this.config.fallbackLanguage !== this.currentLang) {
|
|
167
|
+
languages.push(this.config.fallbackLanguage);
|
|
168
|
+
}
|
|
169
|
+
const skipNamespaces = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const language of languages) {
|
|
171
|
+
if (this.allTranslations[language]) {
|
|
172
|
+
for (const namespace of Object.keys(this.allTranslations[language])) {
|
|
173
|
+
skipNamespaces.add(`${language}:${namespace}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (this.config.debug) {
|
|
178
|
+
console.log("\u{1F30D} [TRANSLATOR] Initializing translator with languages:", languages);
|
|
179
|
+
console.log("\u{1F4CD} [TRANSLATOR] Current language:", this.currentLang);
|
|
180
|
+
console.log("\u{1F4E6} [TRANSLATOR] Config namespaces:", this.config.namespaces);
|
|
181
|
+
}
|
|
182
|
+
for (const language of languages) {
|
|
183
|
+
if (this.config.debug) {
|
|
184
|
+
console.log("Processing language:", language);
|
|
185
|
+
}
|
|
186
|
+
if (!this.allTranslations[language]) {
|
|
187
|
+
this.allTranslations[language] = {};
|
|
188
|
+
}
|
|
189
|
+
for (const namespace of this.config.namespaces || []) {
|
|
190
|
+
const cacheKey = `${language}:${namespace}`;
|
|
191
|
+
if (skipNamespaces.has(cacheKey)) {
|
|
192
|
+
if (this.config.debug) {
|
|
193
|
+
console.log("\u23ED\uFE0F [TRANSLATOR] Skipping", namespace, "for", language, "(already loaded from SSR)");
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (this.config.debug) {
|
|
198
|
+
console.log("Loading namespace:", namespace, "for language:", language);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const data = await this.safeLoadTranslations(language, namespace);
|
|
202
|
+
if (this.config.debug) {
|
|
203
|
+
console.log("Loaded data for", language, namespace, ":", data);
|
|
204
|
+
}
|
|
205
|
+
this.allTranslations[language][namespace] = data;
|
|
206
|
+
this.loadedNamespaces.add(`${language}:${namespace}`);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const translationError = this.createTranslationError(
|
|
209
|
+
"LOAD_FAILED",
|
|
210
|
+
error,
|
|
211
|
+
language,
|
|
212
|
+
namespace
|
|
213
|
+
);
|
|
214
|
+
this.logError(translationError);
|
|
215
|
+
if (isRecoverableError(translationError)) {
|
|
216
|
+
if (language !== this.config.fallbackLanguage) {
|
|
217
|
+
try {
|
|
218
|
+
const fallbackData = await this.safeLoadTranslations(this.config.fallbackLanguage || "en", namespace);
|
|
219
|
+
this.allTranslations[language][namespace] = fallbackData;
|
|
220
|
+
this.loadedNamespaces.add(`${language}:${namespace}`);
|
|
221
|
+
if (this.config.debug) {
|
|
222
|
+
console.log("Using fallback data for", language, namespace);
|
|
223
|
+
}
|
|
224
|
+
} catch (fallbackError) {
|
|
225
|
+
const fallbackTranslationError = this.createTranslationError(
|
|
226
|
+
"FALLBACK_LOAD_FAILED",
|
|
227
|
+
fallbackError,
|
|
228
|
+
this.config.fallbackLanguage,
|
|
229
|
+
namespace
|
|
230
|
+
);
|
|
231
|
+
this.logError(fallbackTranslationError);
|
|
232
|
+
this.allTranslations[language][namespace] = {};
|
|
60
233
|
}
|
|
234
|
+
} else {
|
|
235
|
+
this.allTranslations[language][namespace] = {};
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
this.allTranslations[language][namespace] = {};
|
|
61
239
|
}
|
|
62
|
-
|
|
63
|
-
return getDefaultTranslations(language, namespace);
|
|
240
|
+
}
|
|
64
241
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
242
|
+
}
|
|
243
|
+
this.isInitialized = true;
|
|
244
|
+
if (this.config.debug) {
|
|
245
|
+
console.log("Translator initialized successfully");
|
|
246
|
+
console.log("Loaded translations:", this.allTranslations);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.initializationError = this.createTranslationError(
|
|
250
|
+
"INITIALIZATION_FAILED",
|
|
251
|
+
error
|
|
252
|
+
);
|
|
253
|
+
this.logError(this.initializationError);
|
|
254
|
+
this.isInitialized = true;
|
|
255
|
+
if (this.config.debug) {
|
|
256
|
+
console.warn("Translator initialized with errors, using fallback translations");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 초기화되지 않은 상태에서 번역 시도
|
|
262
|
+
*/
|
|
263
|
+
translateBeforeInitialized(key, targetLang) {
|
|
264
|
+
if (this.config.debug) {
|
|
265
|
+
console.warn("Translator not initialized. Call initialize() first.");
|
|
266
|
+
}
|
|
267
|
+
const { namespace, key: actualKey } = this.parseKey(key);
|
|
268
|
+
const result = this.findInNamespace(namespace, actualKey, targetLang);
|
|
269
|
+
if (result) {
|
|
270
|
+
if (this.config.debug) {
|
|
271
|
+
console.log(`\u2705 [TRANSLATOR] Found fallback translation from initialTranslations:`, result);
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
if (this.config.debug) {
|
|
276
|
+
const translations = this.allTranslations[targetLang]?.[namespace];
|
|
277
|
+
console.log(`\u{1F50D} [TRANSLATOR] Not initialized, fallback failed:`, {
|
|
278
|
+
namespace,
|
|
279
|
+
actualKey,
|
|
280
|
+
hasTranslations: !!translations,
|
|
281
|
+
translationsKeys: translations ? Object.keys(translations) : []
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return this.config.missingKeyHandler?.(key, targetLang, "default") || key;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 다른 로드된 언어에서 번역 찾기 (언어 변경 중 깜빡임 방지)
|
|
288
|
+
*/
|
|
289
|
+
findInOtherLanguages(namespace, key, targetLang) {
|
|
290
|
+
if (!this.allTranslations || Object.keys(this.allTranslations).length === 0) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const loadedLanguages = Object.keys(this.allTranslations);
|
|
294
|
+
for (const lang of loadedLanguages) {
|
|
295
|
+
if (lang !== targetLang) {
|
|
296
|
+
const result = this.findInNamespace(namespace, key, lang);
|
|
297
|
+
if (result) {
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 폴백 언어에서 번역 찾기
|
|
306
|
+
*/
|
|
307
|
+
findInFallbackLanguage(namespace, key, targetLang) {
|
|
308
|
+
const fallbackLang = this.config.fallbackLanguage || "en";
|
|
309
|
+
if (targetLang === fallbackLang) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const result = this.findInNamespace(namespace, key, fallbackLang);
|
|
313
|
+
if (result) {
|
|
314
|
+
this.cacheStats.hits++;
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 번역 키를 번역된 텍스트로 변환
|
|
321
|
+
*/
|
|
322
|
+
translate(key, paramsOrLang, language) {
|
|
323
|
+
let params;
|
|
324
|
+
let targetLang;
|
|
325
|
+
if (typeof paramsOrLang === "string") {
|
|
326
|
+
targetLang = paramsOrLang;
|
|
327
|
+
} else if (typeof paramsOrLang === "object" && paramsOrLang !== null) {
|
|
328
|
+
params = paramsOrLang;
|
|
329
|
+
targetLang = language || this.currentLang;
|
|
330
|
+
} else {
|
|
331
|
+
targetLang = this.currentLang;
|
|
332
|
+
}
|
|
333
|
+
if (!this.isInitialized) {
|
|
334
|
+
const raw = this.translateBeforeInitialized(key, targetLang);
|
|
335
|
+
return params ? this.interpolate(raw, params) : raw;
|
|
336
|
+
}
|
|
337
|
+
const { namespace, key: actualKey } = this.parseKey(key);
|
|
338
|
+
let result = this.findInNamespace(namespace, actualKey, targetLang);
|
|
339
|
+
if (result) {
|
|
340
|
+
this.cacheStats.hits++;
|
|
341
|
+
return params ? this.interpolate(result, params) : result;
|
|
342
|
+
}
|
|
343
|
+
result = this.findInOtherLanguages(namespace, actualKey, targetLang);
|
|
344
|
+
if (result) {
|
|
345
|
+
return params ? this.interpolate(result, params) : result;
|
|
346
|
+
}
|
|
347
|
+
result = this.findInFallbackLanguage(namespace, actualKey, targetLang);
|
|
348
|
+
if (result) {
|
|
349
|
+
return params ? this.interpolate(result, params) : result;
|
|
350
|
+
}
|
|
351
|
+
this.cacheStats.misses++;
|
|
352
|
+
if (this.config.debug) {
|
|
353
|
+
const missing = this.config.missingKeyHandler?.(key, targetLang, namespace) || key;
|
|
354
|
+
return params ? this.interpolate(missing, params) : missing;
|
|
355
|
+
}
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 네임스페이스에서 키 찾기
|
|
360
|
+
*/
|
|
361
|
+
findInNamespace(namespace, key, language) {
|
|
362
|
+
const translations = this.allTranslations[language]?.[namespace];
|
|
363
|
+
if (!translations) {
|
|
364
|
+
const cacheKey = `${language}:${namespace}`;
|
|
365
|
+
if (!this.loadedNamespaces.has(cacheKey) && !this.loadingPromises.has(cacheKey)) {
|
|
366
|
+
this.loadTranslationData(language, namespace).catch((error) => {
|
|
367
|
+
if (this.config.debug) {
|
|
368
|
+
console.warn(`\u26A0\uFE0F [TRANSLATOR] Auto-load failed for ${language}/${namespace}:`, error);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
if (this.config.debug) {
|
|
372
|
+
console.warn(`\u274C [TRANSLATOR] No translations found for ${language}/${namespace}, attempting auto-load...`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
const directValue = translations[key];
|
|
378
|
+
if (this.isStringValue(directValue)) {
|
|
379
|
+
return directValue;
|
|
380
|
+
}
|
|
381
|
+
if (this.isStringArray(directValue)) {
|
|
382
|
+
return directValue[Math.floor(Math.random() * directValue.length)];
|
|
383
|
+
}
|
|
384
|
+
const nestedValue = this.getNestedValue(translations, key);
|
|
385
|
+
if (this.isStringValue(nestedValue)) {
|
|
386
|
+
return nestedValue;
|
|
387
|
+
}
|
|
388
|
+
if (this.isStringArray(nestedValue)) {
|
|
389
|
+
return nestedValue[Math.floor(Math.random() * nestedValue.length)];
|
|
390
|
+
}
|
|
391
|
+
if (this.config.debug) {
|
|
392
|
+
console.warn(`\u274C [TRANSLATOR] No match found for key: ${key} in ${language}/${namespace}`);
|
|
393
|
+
}
|
|
394
|
+
return "";
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* 중첩된 객체에서 값을 가져오기
|
|
398
|
+
* 배열도 지원: 최종 값이 string[]이면 그대로 반환
|
|
399
|
+
*/
|
|
400
|
+
getNestedValue(obj, path) {
|
|
401
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
return path.split(".").reduce((current, key) => {
|
|
405
|
+
if (current == null) return void 0;
|
|
406
|
+
if (Array.isArray(current)) {
|
|
407
|
+
const idx = Number(key);
|
|
408
|
+
return Number.isInteger(idx) ? current[idx] : void 0;
|
|
409
|
+
}
|
|
410
|
+
if (typeof current === "object" && key in current) {
|
|
411
|
+
return current[key];
|
|
412
|
+
}
|
|
413
|
+
return void 0;
|
|
414
|
+
}, obj);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 문자열 값인지 확인하는 타입 가드
|
|
418
|
+
*/
|
|
419
|
+
isStringValue(value) {
|
|
420
|
+
return typeof value === "string" && value.length > 0;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* string[] 배열인지 확인하는 타입 가드
|
|
424
|
+
* 배열 값이 t()에 전달되면 랜덤으로 하나를 선택하여 반환
|
|
425
|
+
*/
|
|
426
|
+
isStringArray(value) {
|
|
427
|
+
return Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "string");
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* 원시 값 가져오기 (배열, 객체 포함)
|
|
431
|
+
*/
|
|
432
|
+
getRawValue(key, language) {
|
|
433
|
+
const targetLang = language || this.currentLang;
|
|
434
|
+
if (!this.isInitialized) {
|
|
435
|
+
if (this.config.debug) {
|
|
436
|
+
console.warn("Translator not initialized. Call initialize() first.");
|
|
437
|
+
}
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
const { namespace, key: actualKey } = this.parseKey(key);
|
|
441
|
+
const translations = this.allTranslations[targetLang]?.[namespace];
|
|
442
|
+
if (!translations) {
|
|
443
|
+
return void 0;
|
|
444
|
+
}
|
|
445
|
+
if (actualKey in translations) {
|
|
446
|
+
return translations[actualKey];
|
|
447
|
+
}
|
|
448
|
+
const nestedValue = this.getNestedValue(translations, actualKey);
|
|
449
|
+
if (nestedValue !== void 0) {
|
|
450
|
+
return nestedValue;
|
|
451
|
+
}
|
|
452
|
+
if (targetLang !== this.config.fallbackLanguage) {
|
|
453
|
+
const fallbackTranslations = this.allTranslations[this.config.fallbackLanguage || "en"]?.[namespace];
|
|
454
|
+
if (fallbackTranslations) {
|
|
455
|
+
if (actualKey in fallbackTranslations) {
|
|
456
|
+
return fallbackTranslations[actualKey];
|
|
457
|
+
}
|
|
458
|
+
const fallbackNestedValue = this.getNestedValue(fallbackTranslations, actualKey);
|
|
459
|
+
if (fallbackNestedValue !== void 0) {
|
|
460
|
+
return fallbackNestedValue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return void 0;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* 배열 번역 값 가져오기 (타입 안전)
|
|
468
|
+
*/
|
|
469
|
+
tArray(key, language) {
|
|
470
|
+
const raw = this.getRawValue(key, language);
|
|
471
|
+
if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) {
|
|
472
|
+
return raw;
|
|
473
|
+
}
|
|
474
|
+
if (process.env.NODE_ENV === "development") {
|
|
475
|
+
console.warn(`tArray: "${key}" is not a string array`);
|
|
476
|
+
}
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Intl.PluralRules 인스턴스 (언어별 캐시)
|
|
481
|
+
*/
|
|
482
|
+
getPluralRules(language) {
|
|
483
|
+
let rules = this.pluralRulesCache.get(language);
|
|
484
|
+
if (!rules) {
|
|
485
|
+
rules = new Intl.PluralRules(language);
|
|
486
|
+
this.pluralRulesCache.set(language, rules);
|
|
487
|
+
}
|
|
488
|
+
return rules;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 복수형 번역 (ICU / Intl.PluralRules 기반)
|
|
492
|
+
*
|
|
493
|
+
* JSON: { "other": "총 {count}개" } (ko)
|
|
494
|
+
* { "one": "{count} item", "other": "{count} items" } (en)
|
|
495
|
+
*
|
|
496
|
+
* tPlural('common:total_count', 1) → en: "1 item" / ko: "총 1개"
|
|
497
|
+
* tPlural('common:total_count', 5) → en: "5 items" / ko: "총 5개"
|
|
498
|
+
*/
|
|
499
|
+
tPlural(key, count, params, language) {
|
|
500
|
+
const targetLang = language || this.currentLang;
|
|
501
|
+
const raw = this.getRawValue(key, targetLang);
|
|
502
|
+
const mergedParams = { count, ...params };
|
|
503
|
+
if (isPluralValue(raw)) {
|
|
504
|
+
const category = this.getPluralRules(targetLang).select(count);
|
|
505
|
+
const text = raw[category] ?? raw.other;
|
|
506
|
+
return this.interpolate(text, mergedParams);
|
|
507
|
+
}
|
|
508
|
+
if (typeof raw === "string") {
|
|
509
|
+
return this.interpolate(raw, mergedParams);
|
|
510
|
+
}
|
|
511
|
+
if (this.config.debug) {
|
|
512
|
+
return this.interpolate(key, mergedParams);
|
|
513
|
+
}
|
|
514
|
+
return "";
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* 매개변수 보간
|
|
518
|
+
*
|
|
519
|
+
* 지원 형식:
|
|
520
|
+
* - {key} - 단일 중괄호 (일반적인 i18n 형식)
|
|
521
|
+
* - {{key}} - 이중 중괄호 (하위 호환성)
|
|
522
|
+
*/
|
|
523
|
+
interpolate(text, params) {
|
|
524
|
+
return text.replace(/\{\{?(\w+)\}?\}/g, (match, key) => {
|
|
525
|
+
const value = params[key];
|
|
526
|
+
return value !== void 0 ? String(value) : match;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 언어 설정
|
|
531
|
+
*/
|
|
532
|
+
setLanguage(language) {
|
|
533
|
+
if (this.currentLang === language) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const previousLanguage = this.currentLang;
|
|
537
|
+
this.currentLang = language;
|
|
538
|
+
this.notifyLanguageChanged(language);
|
|
539
|
+
if (!this.allTranslations[language]) {
|
|
540
|
+
this.loadLanguageData(language).catch((error) => {
|
|
541
|
+
if (this.config.debug) {
|
|
542
|
+
console.warn("Failed to load language data:", error);
|
|
70
543
|
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (this.config.debug) {
|
|
547
|
+
console.log(`\u{1F310} [TRANSLATOR] Language changed: ${previousLanguage} -> ${language}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* 언어 데이터 로드
|
|
552
|
+
*/
|
|
553
|
+
async loadLanguageData(language) {
|
|
554
|
+
if (!this.allTranslations[language]) {
|
|
555
|
+
this.allTranslations[language] = {};
|
|
556
|
+
}
|
|
557
|
+
for (const namespace of this.config.namespaces || []) {
|
|
558
|
+
try {
|
|
559
|
+
const data = await this.safeLoadTranslations(language, namespace);
|
|
560
|
+
this.allTranslations[language][namespace] = data;
|
|
561
|
+
this.loadedNamespaces.add(`${language}:${namespace}`);
|
|
562
|
+
this.notifyTranslationLoaded(language, namespace);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
const translationError = this.createTranslationError(
|
|
565
|
+
"LOAD_FAILED",
|
|
566
|
+
error,
|
|
567
|
+
language,
|
|
568
|
+
namespace
|
|
569
|
+
);
|
|
570
|
+
this.logError(translationError);
|
|
571
|
+
if (isRecoverableError(translationError)) {
|
|
572
|
+
this.allTranslations[language][namespace] = {};
|
|
573
|
+
} else {
|
|
574
|
+
this.allTranslations[language][namespace] = {};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* 현재 언어 가져오기
|
|
581
|
+
*/
|
|
582
|
+
getCurrentLanguage() {
|
|
583
|
+
return this.currentLang;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* 지원되는 언어 목록 가져오기
|
|
587
|
+
*/
|
|
588
|
+
getSupportedLanguages() {
|
|
589
|
+
return this.config.supportedLanguages?.map((lang) => lang.code) || [];
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* 초기화 완료 여부 확인
|
|
593
|
+
*/
|
|
594
|
+
isReady() {
|
|
595
|
+
return this.isInitialized && !this.initializationError;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 초기화 오류 가져오기
|
|
599
|
+
*/
|
|
600
|
+
getInitializationError() {
|
|
601
|
+
return this.initializationError;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* 캐시 클리어
|
|
605
|
+
*/
|
|
606
|
+
clearCache() {
|
|
607
|
+
this.cache.clear();
|
|
608
|
+
this.cacheStats = { hits: 0, misses: 0 };
|
|
609
|
+
if (this.config.debug) {
|
|
610
|
+
console.log("Cache cleared");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* 캐시 엔트리 설정
|
|
615
|
+
*/
|
|
616
|
+
setCacheEntry(key, data) {
|
|
617
|
+
this.cache.set(key, {
|
|
618
|
+
data,
|
|
619
|
+
timestamp: Date.now(),
|
|
620
|
+
ttl: 5 * 60 * 1e3
|
|
621
|
+
// 5분
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* 캐시 엔트리 가져오기
|
|
626
|
+
*/
|
|
627
|
+
getCacheEntry(key) {
|
|
628
|
+
const entry = this.cache.get(key);
|
|
629
|
+
if (!entry) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
633
|
+
this.cache.delete(key);
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return entry.data;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* 번역 오류 생성
|
|
640
|
+
*/
|
|
641
|
+
createTranslationError(code, originalError, language, namespace, key) {
|
|
642
|
+
return {
|
|
643
|
+
name: "TranslationError",
|
|
644
|
+
code,
|
|
645
|
+
message: originalError.message,
|
|
646
|
+
originalError,
|
|
647
|
+
language,
|
|
648
|
+
namespace,
|
|
649
|
+
key,
|
|
650
|
+
timestamp: Date.now(),
|
|
651
|
+
stack: originalError.stack
|
|
71
652
|
};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (data) {
|
|
102
|
-
return data;
|
|
103
|
-
}
|
|
104
|
-
return getDefaultTranslations(language, namespace);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* 오류 로깅
|
|
656
|
+
*/
|
|
657
|
+
logError(error) {
|
|
658
|
+
if (this.config.errorHandler) {
|
|
659
|
+
this.config.errorHandler(error, error.language || "", error.namespace || "");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 재시도 작업
|
|
664
|
+
*/
|
|
665
|
+
async retryOperation(operation, error, context) {
|
|
666
|
+
const maxRetries = 3;
|
|
667
|
+
let lastError = error;
|
|
668
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
669
|
+
try {
|
|
670
|
+
return await operation();
|
|
671
|
+
} catch (retryError) {
|
|
672
|
+
lastError = this.createTranslationError(
|
|
673
|
+
"RETRY_FAILED",
|
|
674
|
+
retryError,
|
|
675
|
+
context.language,
|
|
676
|
+
context.namespace,
|
|
677
|
+
context.key
|
|
678
|
+
);
|
|
679
|
+
if (attempt === maxRetries) {
|
|
680
|
+
break;
|
|
105
681
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
682
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
throw lastError;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* 안전한 번역 로드
|
|
689
|
+
*/
|
|
690
|
+
async safeLoadTranslations(language, namespace) {
|
|
691
|
+
if (this.config.debug) {
|
|
692
|
+
console.log(`\u{1F4E5} [TRANSLATOR] safeLoadTranslations called:`, { language, namespace });
|
|
693
|
+
}
|
|
694
|
+
const loadOperation = async () => {
|
|
695
|
+
if (!this.config.loadTranslations) {
|
|
696
|
+
throw new Error("No translation loader configured");
|
|
697
|
+
}
|
|
698
|
+
if (this.config.debug) {
|
|
699
|
+
console.log(`\u{1F504} [TRANSLATOR] Calling loadTranslations for:`, { language, namespace });
|
|
700
|
+
}
|
|
701
|
+
const data = await this.config.loadTranslations(language, namespace);
|
|
702
|
+
if (this.config.debug) {
|
|
703
|
+
console.log(`\u{1F4E6} [TRANSLATOR] loadTranslations returned:`, data);
|
|
704
|
+
}
|
|
705
|
+
if (!isTranslationNamespace(data)) {
|
|
706
|
+
throw new Error(`Invalid translation data for ${language}:${namespace}`);
|
|
707
|
+
}
|
|
708
|
+
return data;
|
|
709
|
+
};
|
|
710
|
+
try {
|
|
711
|
+
return await loadOperation();
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const translationError = this.createTranslationError(
|
|
714
|
+
"LOAD_FAILED",
|
|
715
|
+
error,
|
|
716
|
+
language,
|
|
717
|
+
namespace
|
|
718
|
+
);
|
|
719
|
+
return this.retryOperation(loadOperation, translationError, { language, namespace });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* 디버그 정보
|
|
724
|
+
*/
|
|
725
|
+
debug() {
|
|
726
|
+
return {
|
|
727
|
+
isInitialized: this.isInitialized,
|
|
728
|
+
currentLanguage: this.currentLang,
|
|
729
|
+
loadedNamespaces: Array.from(this.loadedNamespaces),
|
|
730
|
+
cacheStats: this.cacheStats,
|
|
731
|
+
cacheSize: this.cache.size,
|
|
732
|
+
allTranslations: this.allTranslations,
|
|
733
|
+
initializationError: this.initializationError,
|
|
734
|
+
config: this.config
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* SSR에서 하이드레이션
|
|
739
|
+
*/
|
|
740
|
+
hydrateFromSSR(translations) {
|
|
741
|
+
this.allTranslations = translations;
|
|
742
|
+
this.isInitialized = true;
|
|
743
|
+
for (const [language, namespaces] of Object.entries(translations)) {
|
|
744
|
+
for (const namespace of Object.keys(namespaces)) {
|
|
745
|
+
this.loadedNamespaces.add(`${language}:${namespace}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 비동기 번역 (고급 기능)
|
|
751
|
+
*/
|
|
752
|
+
async translateAsync(key, params) {
|
|
753
|
+
if (!this.isInitialized) {
|
|
754
|
+
await this.initialize();
|
|
755
|
+
}
|
|
756
|
+
const translated = this.translate(key);
|
|
757
|
+
if (!params) {
|
|
758
|
+
return translated;
|
|
759
|
+
}
|
|
760
|
+
return this.interpolate(translated, params);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 동기 번역 (고급 기능)
|
|
764
|
+
*/
|
|
765
|
+
translateSync(key, params) {
|
|
766
|
+
if (!this.isInitialized) {
|
|
767
|
+
if (this.config.debug) {
|
|
768
|
+
console.warn("Translator not initialized for sync translation");
|
|
769
|
+
}
|
|
770
|
+
const { namespace } = this.parseKey(key);
|
|
771
|
+
return this.config.missingKeyHandler?.(key, this.currentLang, namespace) || key;
|
|
772
|
+
}
|
|
773
|
+
const translated = this.translate(key);
|
|
774
|
+
if (!params) {
|
|
775
|
+
return translated;
|
|
776
|
+
}
|
|
777
|
+
return this.interpolate(translated, params);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* 키 파싱 (네임스페이스:키 형식)
|
|
781
|
+
*
|
|
782
|
+
* - 콜론(:)만 네임스페이스 구분자로 사용
|
|
783
|
+
* - 점(.)은 키 이름의 일부로 취급 (중첩 객체 접근용)
|
|
784
|
+
*
|
|
785
|
+
* @example
|
|
786
|
+
* parseKey("home:hero.badge") → { namespace: "home", key: "hero.badge" }
|
|
787
|
+
* parseKey("hero.badge") → { namespace: "common", key: "hero.badge" }
|
|
788
|
+
* parseKey("save") → { namespace: "common", key: "save" }
|
|
789
|
+
*/
|
|
790
|
+
parseKey(key) {
|
|
791
|
+
const colonIndex = key.indexOf(":");
|
|
792
|
+
if (colonIndex !== -1) {
|
|
793
|
+
return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
|
|
794
|
+
}
|
|
795
|
+
return { namespace: "common", key };
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* 번역 데이터 로드 (고급 기능)
|
|
799
|
+
*/
|
|
800
|
+
async loadTranslationData(language, namespace) {
|
|
801
|
+
const cacheKey = `${language}:${namespace}`;
|
|
802
|
+
if (this.loadedNamespaces.has(cacheKey)) {
|
|
803
|
+
const existing = this.allTranslations[language]?.[namespace];
|
|
804
|
+
if (existing) {
|
|
805
|
+
return existing;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const cached = this.getCacheEntry(cacheKey);
|
|
809
|
+
if (cached) {
|
|
810
|
+
if (!this.allTranslations[language]) {
|
|
811
|
+
this.allTranslations[language] = {};
|
|
812
|
+
}
|
|
813
|
+
this.allTranslations[language][namespace] = cached;
|
|
814
|
+
this.loadedNamespaces.add(cacheKey);
|
|
815
|
+
return cached;
|
|
816
|
+
}
|
|
817
|
+
const loadingPromise = this.loadingPromises.get(cacheKey);
|
|
818
|
+
if (loadingPromise) {
|
|
819
|
+
return loadingPromise;
|
|
820
|
+
}
|
|
821
|
+
const loadPromise = this._loadTranslationData(language, namespace);
|
|
822
|
+
this.loadingPromises.set(cacheKey, loadPromise);
|
|
823
|
+
try {
|
|
824
|
+
const data = await loadPromise;
|
|
825
|
+
if (!this.allTranslations[language]) {
|
|
826
|
+
this.allTranslations[language] = {};
|
|
827
|
+
}
|
|
828
|
+
this.allTranslations[language][namespace] = data;
|
|
829
|
+
this.loadedNamespaces.add(cacheKey);
|
|
830
|
+
this.setCacheEntry(cacheKey, data);
|
|
831
|
+
if (this.config.debug) {
|
|
832
|
+
console.log(`\u2705 [TRANSLATOR] Auto-loaded and saved ${language}/${namespace}`);
|
|
833
|
+
}
|
|
834
|
+
this.notifyTranslationLoaded(language, namespace);
|
|
835
|
+
return data;
|
|
836
|
+
} finally {
|
|
837
|
+
this.loadingPromises.delete(cacheKey);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* 실제 번역 데이터 로드
|
|
842
|
+
*/
|
|
843
|
+
async _loadTranslationData(language, namespace) {
|
|
844
|
+
if (!this.config.loadTranslations) {
|
|
845
|
+
throw new Error("No translation loader configured");
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const data = await this.config.loadTranslations(language, namespace);
|
|
849
|
+
if (!isTranslationNamespace(data)) {
|
|
850
|
+
throw new Error(`Invalid translation data for ${language}:${namespace}`);
|
|
851
|
+
}
|
|
852
|
+
return data;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
const translationError = this.createTranslationError(
|
|
855
|
+
"LOAD_FAILED",
|
|
856
|
+
error,
|
|
857
|
+
language,
|
|
858
|
+
namespace
|
|
859
|
+
);
|
|
860
|
+
this.logError(translationError);
|
|
861
|
+
return {};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
function ssrTranslate({
|
|
866
|
+
translations,
|
|
867
|
+
key,
|
|
868
|
+
language = "ko",
|
|
869
|
+
fallbackLanguage = "en",
|
|
870
|
+
missingKeyHandler = (key2) => key2
|
|
871
|
+
}) {
|
|
872
|
+
const { namespace, key: actualKey } = parseKey(key);
|
|
873
|
+
let result = ssrFindInNamespace(translations, namespace, actualKey, language);
|
|
874
|
+
if (result) {
|
|
875
|
+
return result;
|
|
876
|
+
}
|
|
877
|
+
if (language !== fallbackLanguage) {
|
|
878
|
+
result = ssrFindInNamespace(translations, namespace, actualKey, fallbackLanguage);
|
|
879
|
+
if (result) {
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return missingKeyHandler(key);
|
|
884
|
+
}
|
|
885
|
+
function ssrFindInNamespace(translations, namespace, key, language, fallbackLanguage, missingKeyHandler) {
|
|
886
|
+
const namespaceData = translations[language]?.[namespace];
|
|
887
|
+
if (!namespaceData) {
|
|
888
|
+
return "";
|
|
889
|
+
}
|
|
890
|
+
const directValue = namespaceData[key];
|
|
891
|
+
if (isStringValue(directValue)) {
|
|
892
|
+
return directValue;
|
|
893
|
+
}
|
|
894
|
+
const nestedValue = getNestedValue(namespaceData, key);
|
|
895
|
+
if (isStringValue(nestedValue)) {
|
|
896
|
+
return nestedValue;
|
|
897
|
+
}
|
|
898
|
+
return "";
|
|
899
|
+
}
|
|
900
|
+
function getNestedValue(obj, path) {
|
|
901
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
902
|
+
return void 0;
|
|
903
|
+
}
|
|
904
|
+
return path.split(".").reduce((current, key) => {
|
|
905
|
+
if (current && typeof current === "object" && !Array.isArray(current) && key in current) {
|
|
906
|
+
return current[key];
|
|
907
|
+
}
|
|
908
|
+
return void 0;
|
|
909
|
+
}, obj);
|
|
910
|
+
}
|
|
911
|
+
function isStringValue(value) {
|
|
912
|
+
return typeof value === "string" && value.length > 0;
|
|
913
|
+
}
|
|
914
|
+
function parseKey(key) {
|
|
915
|
+
const colonIndex = key.indexOf(":");
|
|
916
|
+
if (colonIndex !== -1) {
|
|
917
|
+
return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
|
|
918
|
+
}
|
|
919
|
+
return { namespace: "common", key };
|
|
920
|
+
}
|
|
921
|
+
function serverTranslate({
|
|
922
|
+
translations,
|
|
923
|
+
key,
|
|
924
|
+
language = "ko",
|
|
925
|
+
fallbackLanguage = "en",
|
|
926
|
+
missingKeyHandler = (key2) => key2,
|
|
927
|
+
options = {}
|
|
928
|
+
}) {
|
|
929
|
+
const { cache, metrics, debug } = options;
|
|
930
|
+
if (cache) {
|
|
931
|
+
const cacheKey = `${language}:${key}`;
|
|
932
|
+
const cached = cache.get(cacheKey);
|
|
933
|
+
if (cached) {
|
|
934
|
+
if (metrics) metrics.hits++;
|
|
935
|
+
if (debug) console.log(`[CACHE HIT] ${cacheKey}`);
|
|
936
|
+
return cached;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const result = findInTranslations(translations, key, language, fallbackLanguage);
|
|
940
|
+
if (cache && result) {
|
|
941
|
+
const cacheKey = `${language}:${key}`;
|
|
942
|
+
cache.set(cacheKey, result);
|
|
943
|
+
}
|
|
944
|
+
if (metrics) metrics.misses++;
|
|
945
|
+
if (debug) console.log(`[TRANSLATE] ${key} -> ${result}`);
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
function findInTranslations(translations, key, language, fallbackLanguage, missingKeyHandler) {
|
|
949
|
+
const { namespace, key: actualKey } = parseKey(key);
|
|
950
|
+
let result = findInNamespace(translations, namespace, actualKey, language);
|
|
951
|
+
if (result) {
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
if (language !== fallbackLanguage) {
|
|
955
|
+
result = findInNamespace(translations, namespace, actualKey, fallbackLanguage);
|
|
956
|
+
if (result) {
|
|
957
|
+
return result;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return "";
|
|
961
|
+
}
|
|
962
|
+
function findInNamespace(translations, namespace, key, language) {
|
|
963
|
+
const languageData = translations[language];
|
|
964
|
+
if (!languageData || typeof languageData !== "object" || Array.isArray(languageData)) {
|
|
965
|
+
return "";
|
|
966
|
+
}
|
|
967
|
+
const namespaceData = languageData[namespace];
|
|
968
|
+
if (!namespaceData || typeof namespaceData !== "object" || Array.isArray(namespaceData)) {
|
|
969
|
+
return "";
|
|
970
|
+
}
|
|
971
|
+
const data = namespaceData;
|
|
972
|
+
if (data[key] && typeof data[key] === "string") {
|
|
973
|
+
return data[key];
|
|
974
|
+
}
|
|
975
|
+
const nestedValue = getNestedValue(namespaceData, key);
|
|
976
|
+
if (typeof nestedValue === "string") {
|
|
977
|
+
return nestedValue;
|
|
978
|
+
}
|
|
979
|
+
return "";
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/core/translator-factory.ts
|
|
983
|
+
var TranslatorFactory = class {
|
|
984
|
+
// 최대 인스턴스 수 제한
|
|
985
|
+
/**
|
|
986
|
+
* Config를 기반으로 고유 키 생성
|
|
987
|
+
*/
|
|
988
|
+
static generateConfigKey(config) {
|
|
989
|
+
const keyParts = [
|
|
990
|
+
config.defaultLanguage,
|
|
991
|
+
config.fallbackLanguage || "en",
|
|
992
|
+
config.namespaces?.join(",") || "common",
|
|
993
|
+
config.debug ? "debug" : "prod"
|
|
994
|
+
];
|
|
995
|
+
return keyParts.join("|");
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Config가 변경되었는지 확인
|
|
999
|
+
*/
|
|
1000
|
+
static isConfigChanged(configKey, newConfig) {
|
|
1001
|
+
const cachedConfig = this.configCache.get(configKey);
|
|
1002
|
+
if (!cachedConfig) return true;
|
|
1003
|
+
return cachedConfig.defaultLanguage !== newConfig.defaultLanguage || cachedConfig.fallbackLanguage !== newConfig.fallbackLanguage || JSON.stringify(cachedConfig.namespaces) !== JSON.stringify(newConfig.namespaces) || cachedConfig.debug !== newConfig.debug;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Translator 인스턴스 생성 또는 반환
|
|
1007
|
+
*/
|
|
1008
|
+
static create(config) {
|
|
1009
|
+
const configKey = this.generateConfigKey(config);
|
|
1010
|
+
if (!this.instances.has(configKey) || this.isConfigChanged(configKey, config)) {
|
|
1011
|
+
if (this.instances.size >= this.MAX_INSTANCES && !this.instances.has(configKey)) {
|
|
1012
|
+
const oldestKey = this.instances.keys().next().value;
|
|
1013
|
+
if (oldestKey) {
|
|
1014
|
+
const oldInstance = this.instances.get(oldestKey);
|
|
1015
|
+
if (oldInstance) {
|
|
1016
|
+
oldInstance.clearCache();
|
|
1017
|
+
}
|
|
1018
|
+
this.instances.delete(oldestKey);
|
|
1019
|
+
this.configCache.delete(oldestKey);
|
|
111
1020
|
}
|
|
1021
|
+
}
|
|
1022
|
+
if (this.instances.has(configKey)) {
|
|
1023
|
+
const oldInstance = this.instances.get(configKey);
|
|
1024
|
+
oldInstance.clearCache();
|
|
1025
|
+
}
|
|
1026
|
+
const newInstance = new Translator(config);
|
|
1027
|
+
this.instances.set(configKey, newInstance);
|
|
1028
|
+
this.configCache.set(configKey, { ...config });
|
|
1029
|
+
}
|
|
1030
|
+
return this.instances.get(configKey);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* 특정 Config 키의 Translator 인스턴스 반환
|
|
1034
|
+
*/
|
|
1035
|
+
static get(config) {
|
|
1036
|
+
const configKey = this.generateConfigKey(config);
|
|
1037
|
+
return this.instances.get(configKey) || null;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* 모든 Translator 인스턴스 정리 (테스트용)
|
|
1041
|
+
*/
|
|
1042
|
+
static clear() {
|
|
1043
|
+
for (const instance of this.instances.values()) {
|
|
1044
|
+
instance.clearCache();
|
|
1045
|
+
}
|
|
1046
|
+
this.instances.clear();
|
|
1047
|
+
this.configCache.clear();
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* 특정 Config 키의 인스턴스만 정리
|
|
1051
|
+
*/
|
|
1052
|
+
static clearConfig(config) {
|
|
1053
|
+
const configKey = this.generateConfigKey(config);
|
|
1054
|
+
const instance = this.instances.get(configKey);
|
|
1055
|
+
if (instance) {
|
|
1056
|
+
instance.clearCache();
|
|
1057
|
+
this.instances.delete(configKey);
|
|
1058
|
+
this.configCache.delete(configKey);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* 현재 관리 중인 인스턴스 수 반환
|
|
1063
|
+
*/
|
|
1064
|
+
static getInstanceCount() {
|
|
1065
|
+
return this.instances.size;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* 디버깅용: 모든 인스턴스 정보 반환
|
|
1069
|
+
*/
|
|
1070
|
+
static debug() {
|
|
1071
|
+
return {
|
|
1072
|
+
instanceCount: this.instances.size,
|
|
1073
|
+
configKeys: Array.from(this.instances.keys()),
|
|
1074
|
+
instances: this.instances
|
|
112
1075
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
TranslatorFactory.instances = /* @__PURE__ */ new Map();
|
|
1079
|
+
TranslatorFactory.configCache = /* @__PURE__ */ new Map();
|
|
1080
|
+
TranslatorFactory.MAX_INSTANCES = 10;
|
|
1081
|
+
|
|
1082
|
+
// src/utils/default-translations.ts
|
|
1083
|
+
var DEFAULT_TRANSLATIONS = {
|
|
1084
|
+
ko: {
|
|
1085
|
+
common: {
|
|
1086
|
+
welcome: "\uD658\uC601\uD569\uB2C8\uB2E4",
|
|
1087
|
+
greeting: "\uC548\uB155\uD558\uC138\uC694",
|
|
1088
|
+
goodbye: "\uC548\uB155\uD788 \uAC00\uC138\uC694",
|
|
1089
|
+
loading: "\uB85C\uB529 \uC911...",
|
|
1090
|
+
error: "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
|
|
1091
|
+
success: "\uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4",
|
|
1092
|
+
cancel: "\uCDE8\uC18C",
|
|
1093
|
+
confirm: "\uD655\uC778",
|
|
1094
|
+
save: "\uC800\uC7A5",
|
|
1095
|
+
delete: "\uC0AD\uC81C",
|
|
1096
|
+
edit: "\uD3B8\uC9D1",
|
|
1097
|
+
add: "\uCD94\uAC00",
|
|
1098
|
+
search: "\uAC80\uC0C9",
|
|
1099
|
+
filter: "\uD544\uD130",
|
|
1100
|
+
sort: "\uC815\uB82C",
|
|
1101
|
+
refresh: "\uC0C8\uB85C\uACE0\uCE68",
|
|
1102
|
+
back: "\uB4A4\uB85C",
|
|
1103
|
+
next: "\uB2E4\uC74C",
|
|
1104
|
+
previous: "\uC774\uC804",
|
|
1105
|
+
home: "\uD648",
|
|
1106
|
+
about: "\uC18C\uAC1C",
|
|
1107
|
+
contact: "\uC5F0\uB77D\uCC98",
|
|
1108
|
+
settings: "\uC124\uC815",
|
|
1109
|
+
profile: "\uD504\uB85C\uD544",
|
|
1110
|
+
logout: "\uB85C\uADF8\uC544\uC6C3",
|
|
1111
|
+
login: "\uB85C\uADF8\uC778",
|
|
1112
|
+
register: "\uD68C\uC6D0\uAC00\uC785"
|
|
1113
|
+
},
|
|
1114
|
+
auth: {
|
|
1115
|
+
login: "\uB85C\uADF8\uC778",
|
|
1116
|
+
logout: "\uB85C\uADF8\uC544\uC6C3",
|
|
1117
|
+
register: "\uD68C\uC6D0\uAC00\uC785",
|
|
1118
|
+
email: "\uC774\uBA54\uC77C",
|
|
1119
|
+
password: "\uBE44\uBC00\uBC88\uD638",
|
|
1120
|
+
forgot_password: "\uBE44\uBC00\uBC88\uD638 \uCC3E\uAE30",
|
|
1121
|
+
remember_me: "\uB85C\uADF8\uC778 \uC0C1\uD0DC \uC720\uC9C0"
|
|
1122
|
+
},
|
|
1123
|
+
errors: {
|
|
1124
|
+
not_found: "\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
1125
|
+
server_error: "\uC11C\uBC84 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
|
|
1126
|
+
network_error: "\uB124\uD2B8\uC6CC\uD06C \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
|
|
1127
|
+
unauthorized: "\uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4",
|
|
1128
|
+
forbidden: "\uC811\uADFC\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4"
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
en: {
|
|
1132
|
+
common: {
|
|
1133
|
+
welcome: "Welcome",
|
|
1134
|
+
greeting: "Hello",
|
|
1135
|
+
goodbye: "Goodbye",
|
|
1136
|
+
loading: "Loading...",
|
|
1137
|
+
error: "An error occurred",
|
|
1138
|
+
success: "Success",
|
|
1139
|
+
cancel: "Cancel",
|
|
1140
|
+
confirm: "Confirm",
|
|
1141
|
+
save: "Save",
|
|
1142
|
+
delete: "Delete",
|
|
1143
|
+
edit: "Edit",
|
|
1144
|
+
add: "Add",
|
|
1145
|
+
search: "Search",
|
|
1146
|
+
filter: "Filter",
|
|
1147
|
+
sort: "Sort",
|
|
1148
|
+
refresh: "Refresh",
|
|
1149
|
+
back: "Back",
|
|
1150
|
+
next: "Next",
|
|
1151
|
+
previous: "Previous",
|
|
1152
|
+
home: "Home",
|
|
1153
|
+
about: "About",
|
|
1154
|
+
contact: "Contact",
|
|
1155
|
+
settings: "Settings",
|
|
1156
|
+
profile: "Profile",
|
|
1157
|
+
logout: "Logout",
|
|
1158
|
+
login: "Login",
|
|
1159
|
+
register: "Register"
|
|
1160
|
+
},
|
|
1161
|
+
auth: {
|
|
1162
|
+
login: "Login",
|
|
1163
|
+
logout: "Logout",
|
|
1164
|
+
register: "Register",
|
|
1165
|
+
email: "Email",
|
|
1166
|
+
password: "Password",
|
|
1167
|
+
forgot_password: "Forgot Password",
|
|
1168
|
+
remember_me: "Remember Me"
|
|
1169
|
+
},
|
|
1170
|
+
errors: {
|
|
1171
|
+
not_found: "Page not found",
|
|
1172
|
+
server_error: "Server error occurred",
|
|
1173
|
+
network_error: "Network error occurred",
|
|
1174
|
+
unauthorized: "Authentication required",
|
|
1175
|
+
forbidden: "Access denied"
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
function getDefaultTranslations(language, namespace) {
|
|
1180
|
+
return DEFAULT_TRANSLATIONS[language]?.[namespace] || {};
|
|
1181
|
+
}
|
|
1182
|
+
var I18nContext = React.createContext(null);
|
|
1183
|
+
function I18nProvider({
|
|
1184
|
+
config,
|
|
1185
|
+
children
|
|
1186
|
+
}) {
|
|
1187
|
+
const [currentLanguage, setCurrentLanguageState] = React.useState(config.defaultLanguage);
|
|
1188
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
1189
|
+
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
1190
|
+
const [error, setError] = React.useState(null);
|
|
1191
|
+
const [translationVersion, setTranslationVersion] = React.useState(0);
|
|
1192
|
+
React.useEffect(() => {
|
|
1193
|
+
if (!isInitialized && config.defaultLanguage !== currentLanguage) {
|
|
1194
|
+
setCurrentLanguageState(config.defaultLanguage);
|
|
1195
|
+
}
|
|
1196
|
+
}, [config.defaultLanguage, currentLanguage, isInitialized]);
|
|
1197
|
+
const translator = React.useMemo(() => {
|
|
1198
|
+
if (!validateI18nConfig(config)) {
|
|
1199
|
+
throw new Error("Invalid I18nConfig provided to I18nProvider");
|
|
1200
|
+
}
|
|
1201
|
+
return TranslatorFactory.create(config);
|
|
1202
|
+
}, [config]);
|
|
1203
|
+
React.useEffect(() => {
|
|
1204
|
+
if (isInitialized) {
|
|
1205
|
+
const translatorLang = translator.getCurrentLanguage();
|
|
1206
|
+
if (translatorLang !== currentLanguage) {
|
|
1207
|
+
if (config.debug) {
|
|
1208
|
+
console.log(`\u{1F504} [USEI18N] Syncing translator language: ${translatorLang} -> ${currentLanguage} (already initialized)`);
|
|
1209
|
+
}
|
|
1210
|
+
translator.setLanguage(currentLanguage);
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (config.debug) {
|
|
1215
|
+
console.log("\u{1F504} [USEI18N] useEffect triggered:", {
|
|
1216
|
+
hasTranslator: !!translator,
|
|
1217
|
+
currentLanguage,
|
|
1218
|
+
debug: config.debug,
|
|
1219
|
+
isInitialized
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
const initializeTranslator = async () => {
|
|
1223
|
+
try {
|
|
1224
|
+
setIsLoading(true);
|
|
1225
|
+
setError(null);
|
|
1226
|
+
if (config.debug) {
|
|
1227
|
+
console.log("\u{1F680} [USEI18N] Starting translator initialization...");
|
|
1228
|
+
}
|
|
1229
|
+
translator.setLanguage(currentLanguage);
|
|
1230
|
+
await translator.initialize();
|
|
1231
|
+
setIsInitialized(true);
|
|
1232
|
+
if (config.debug) {
|
|
1233
|
+
console.log("\u2705 [USEI18N] Translator initialization completed successfully");
|
|
1234
|
+
}
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
const initError = err;
|
|
1237
|
+
setError(initError);
|
|
1238
|
+
if (config.debug) {
|
|
1239
|
+
console.error("\u274C [USEI18N] Failed to initialize translator:", initError);
|
|
1240
|
+
}
|
|
1241
|
+
setIsInitialized(true);
|
|
1242
|
+
} finally {
|
|
1243
|
+
setIsLoading(false);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
initializeTranslator();
|
|
1247
|
+
}, [translator, currentLanguage, config.debug, isInitialized]);
|
|
1248
|
+
React.useEffect(() => {
|
|
1249
|
+
if (!translator || !isInitialized) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const unsubscribe = translator.onTranslationLoaded(() => {
|
|
1253
|
+
setTranslationVersion((prev) => prev + 1);
|
|
1254
|
+
if (config.debug) {
|
|
1255
|
+
console.log("\u{1F504} [USEI18N] Translation loaded, triggering re-render");
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
return unsubscribe;
|
|
1259
|
+
}, [translator, isInitialized, config.debug]);
|
|
1260
|
+
React.useEffect(() => {
|
|
1261
|
+
if (!translator || !isInitialized) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const unsubscribe = translator.onLanguageChanged((newLanguage) => {
|
|
1265
|
+
if (newLanguage !== currentLanguage) {
|
|
1266
|
+
if (config.debug) {
|
|
1267
|
+
console.log(`\u{1F504} [USEI18N] Language changed event: ${currentLanguage} -> ${newLanguage}`);
|
|
1268
|
+
}
|
|
1269
|
+
setCurrentLanguageState(newLanguage);
|
|
1270
|
+
setTranslationVersion((prev) => prev + 1);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
return unsubscribe;
|
|
1274
|
+
}, [translator, isInitialized, currentLanguage, config.debug]);
|
|
1275
|
+
React.useEffect(() => {
|
|
1276
|
+
if (!config.autoLanguageSync || typeof window === "undefined") {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const handleLanguageChange = (event) => {
|
|
1280
|
+
const newLanguage = event.detail;
|
|
1281
|
+
if (typeof newLanguage === "string" && newLanguage !== currentLanguage) {
|
|
1282
|
+
if (config.debug) {
|
|
1283
|
+
console.log("\u{1F310} Auto language sync:", newLanguage);
|
|
1284
|
+
}
|
|
1285
|
+
setLanguage(newLanguage);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
window.addEventListener("huaI18nLanguageChange", handleLanguageChange);
|
|
1289
|
+
window.addEventListener("i18nLanguageChanged", handleLanguageChange);
|
|
1290
|
+
return () => {
|
|
1291
|
+
window.removeEventListener("huaI18nLanguageChange", handleLanguageChange);
|
|
1292
|
+
window.removeEventListener("i18nLanguageChanged", handleLanguageChange);
|
|
1293
|
+
};
|
|
1294
|
+
}, [config.autoLanguageSync, currentLanguage]);
|
|
1295
|
+
const setLanguage = React.useCallback(async (language) => {
|
|
1296
|
+
if (!translator) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const currentLang = translator.getCurrentLanguage();
|
|
1300
|
+
if (currentLang === language) {
|
|
1301
|
+
if (config.debug) {
|
|
1302
|
+
console.log(`\u23ED\uFE0F [USEI18N] Language unchanged, skipping: ${language}`);
|
|
1303
|
+
}
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (config.debug) {
|
|
1307
|
+
if (config.debug) {
|
|
1308
|
+
console.log(`\u{1F504} [USEI18N] setLanguage called: ${currentLang} -> ${language}`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
setIsLoading(true);
|
|
1312
|
+
try {
|
|
1313
|
+
translator.setLanguage(language);
|
|
1314
|
+
setCurrentLanguageState(language);
|
|
1315
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1316
|
+
if (config.debug) {
|
|
1317
|
+
console.log(`\u2705 [USEI18N] Language changed to ${language}`);
|
|
1318
|
+
}
|
|
1319
|
+
} catch (error2) {
|
|
1320
|
+
if (config.debug) {
|
|
1321
|
+
console.error(`\u274C [USEI18N] Failed to change language to ${language}:`, error2);
|
|
1322
|
+
}
|
|
1323
|
+
} finally {
|
|
1324
|
+
setIsLoading(false);
|
|
1325
|
+
}
|
|
1326
|
+
}, [translator, config.debug]);
|
|
1327
|
+
const parseKey2 = React.useCallback((key) => {
|
|
1328
|
+
const parts = key.split(":");
|
|
1329
|
+
if (parts.length >= 2) {
|
|
1330
|
+
return { namespace: parts[0], key: parts.slice(1).join(":") };
|
|
1331
|
+
}
|
|
1332
|
+
return { namespace: "common", key };
|
|
1333
|
+
}, []);
|
|
1334
|
+
const resolveNestedKey = React.useCallback((obj, key) => {
|
|
1335
|
+
if (key in obj && typeof obj[key] === "string") {
|
|
1336
|
+
return obj[key];
|
|
1337
|
+
}
|
|
1338
|
+
const parts = key.split(".");
|
|
1339
|
+
let current = obj;
|
|
1340
|
+
for (const part of parts) {
|
|
1341
|
+
if (current && typeof current === "object" && current !== null && part in current) {
|
|
1342
|
+
current = current[part];
|
|
1343
|
+
} else {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return typeof current === "string" ? current : null;
|
|
1348
|
+
}, []);
|
|
1349
|
+
const findInSSRTranslations = React.useCallback((key, targetLang) => {
|
|
1350
|
+
if (!config.initialTranslations) {
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
const { namespace, key: actualKey } = parseKey2(key);
|
|
1354
|
+
const ssrTranslations = config.initialTranslations[targetLang]?.[namespace];
|
|
1355
|
+
if (ssrTranslations) {
|
|
1356
|
+
const value2 = resolveNestedKey(ssrTranslations, actualKey);
|
|
1357
|
+
if (value2 !== null) {
|
|
1358
|
+
return value2;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const fallbackLang = config.fallbackLanguage || "en";
|
|
1362
|
+
if (targetLang !== fallbackLang) {
|
|
1363
|
+
const fallbackTranslations = config.initialTranslations[fallbackLang]?.[namespace];
|
|
1364
|
+
if (fallbackTranslations) {
|
|
1365
|
+
const value2 = resolveNestedKey(fallbackTranslations, actualKey);
|
|
1366
|
+
if (value2 !== null) {
|
|
1367
|
+
return value2;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
}, [config.initialTranslations, config.fallbackLanguage, parseKey2, resolveNestedKey]);
|
|
1373
|
+
const findInDefaultTranslations = React.useCallback((key, targetLang) => {
|
|
1374
|
+
const { namespace, key: actualKey } = parseKey2(key);
|
|
1375
|
+
const defaultTranslations = getDefaultTranslations(targetLang, namespace);
|
|
1376
|
+
const fallbackTranslations = getDefaultTranslations(config.fallbackLanguage || "en", namespace);
|
|
1377
|
+
return resolveNestedKey(defaultTranslations, actualKey) || resolveNestedKey(fallbackTranslations, actualKey) || null;
|
|
1378
|
+
}, [config.fallbackLanguage, parseKey2, resolveNestedKey]);
|
|
1379
|
+
const t = React.useCallback((key, paramsOrLang, language) => {
|
|
1380
|
+
if (!translator) {
|
|
1381
|
+
return key;
|
|
1382
|
+
}
|
|
1383
|
+
let params;
|
|
1384
|
+
let lang;
|
|
1385
|
+
if (typeof paramsOrLang === "string") {
|
|
1386
|
+
lang = paramsOrLang;
|
|
1387
|
+
} else if (typeof paramsOrLang === "object" && paramsOrLang !== null) {
|
|
1388
|
+
params = paramsOrLang;
|
|
1389
|
+
lang = language;
|
|
1390
|
+
}
|
|
1391
|
+
const targetLang = lang || currentLanguage;
|
|
1392
|
+
try {
|
|
1393
|
+
const result = translator.translate(key, params || lang, params ? lang : void 0);
|
|
1394
|
+
if (result && result !== key && result !== "") {
|
|
1395
|
+
return result;
|
|
1396
|
+
}
|
|
1397
|
+
} catch (error2) {
|
|
1398
|
+
}
|
|
1399
|
+
const interpolate = (text) => {
|
|
1400
|
+
if (!params) return text;
|
|
1401
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, k) => {
|
|
1402
|
+
const value2 = params[k];
|
|
1403
|
+
return value2 !== void 0 ? String(value2) : match;
|
|
1404
|
+
});
|
|
1405
|
+
};
|
|
1406
|
+
const ssrResult = findInSSRTranslations(key, targetLang);
|
|
1407
|
+
if (ssrResult) {
|
|
1408
|
+
return interpolate(ssrResult);
|
|
1409
|
+
}
|
|
1410
|
+
const defaultResult = findInDefaultTranslations(key, targetLang);
|
|
1411
|
+
if (defaultResult) {
|
|
1412
|
+
return interpolate(defaultResult);
|
|
1413
|
+
}
|
|
1414
|
+
if (config.debug) {
|
|
1415
|
+
return interpolate(key);
|
|
1416
|
+
}
|
|
1417
|
+
return "";
|
|
1418
|
+
}, [translator, config.debug, currentLanguage, config.fallbackLanguage, translationVersion, findInSSRTranslations, findInDefaultTranslations]);
|
|
1419
|
+
const tAsync = React.useCallback(async (key, params) => {
|
|
1420
|
+
if (!translator) {
|
|
1421
|
+
if (config.debug) {
|
|
1422
|
+
console.warn("Translator not initialized");
|
|
1423
|
+
}
|
|
1424
|
+
return key;
|
|
1425
|
+
}
|
|
1426
|
+
setIsLoading(true);
|
|
1427
|
+
try {
|
|
1428
|
+
const result = await translator.translateAsync(key, params);
|
|
1429
|
+
return result;
|
|
1430
|
+
} catch (error2) {
|
|
1431
|
+
if (config.debug) {
|
|
1432
|
+
console.error("Translation error:", error2);
|
|
1433
|
+
}
|
|
1434
|
+
return key;
|
|
1435
|
+
} finally {
|
|
1436
|
+
setIsLoading(false);
|
|
1437
|
+
}
|
|
1438
|
+
}, [translator, config.debug]);
|
|
1439
|
+
const tSync = React.useCallback((key, namespace, params) => {
|
|
1440
|
+
if (!translator) {
|
|
1441
|
+
if (config.debug) {
|
|
1442
|
+
console.warn("Translator not initialized");
|
|
1443
|
+
}
|
|
1444
|
+
return key;
|
|
1445
|
+
}
|
|
1446
|
+
return translator.translateSync(key, params);
|
|
1447
|
+
}, [translator, config.debug]);
|
|
1448
|
+
const getRawValue = React.useCallback((key, language) => {
|
|
1449
|
+
if (!translator || !isInitialized) {
|
|
1450
|
+
return void 0;
|
|
1451
|
+
}
|
|
1452
|
+
return translator.getRawValue(key, language);
|
|
1453
|
+
}, [translator, isInitialized]);
|
|
1454
|
+
const tArray = React.useCallback((key, language) => {
|
|
1455
|
+
if (!translator || !isInitialized) {
|
|
1456
|
+
return [];
|
|
1457
|
+
}
|
|
1458
|
+
return translator.tArray(key, language);
|
|
1459
|
+
}, [translator, isInitialized, translationVersion, currentLanguage]);
|
|
1460
|
+
const tPlural = React.useCallback((key, count, params, language) => {
|
|
1461
|
+
if (!translator || !isInitialized) {
|
|
1462
|
+
return key;
|
|
1463
|
+
}
|
|
1464
|
+
return translator.tPlural(key, count, params, language);
|
|
1465
|
+
}, [translator, isInitialized, translationVersion, currentLanguage]);
|
|
1466
|
+
const debug = React.useMemo(() => ({
|
|
1467
|
+
getCurrentLanguage: () => {
|
|
1468
|
+
try {
|
|
1469
|
+
return translator?.getCurrentLanguage() || currentLanguage;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return currentLanguage;
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
getSupportedLanguages: () => {
|
|
1475
|
+
try {
|
|
1476
|
+
return translator?.getSupportedLanguages() || config.supportedLanguages?.map((l) => l.code) || [];
|
|
1477
|
+
} catch {
|
|
1478
|
+
return config.supportedLanguages?.map((l) => l.code) || [];
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
getLoadedNamespaces: () => {
|
|
1482
|
+
try {
|
|
1483
|
+
const debugInfo = translator?.debug();
|
|
1484
|
+
if (debugInfo && debugInfo.loadedNamespaces) {
|
|
1485
|
+
return Array.from(debugInfo.loadedNamespaces);
|
|
1486
|
+
}
|
|
1487
|
+
if (debugInfo && debugInfo.allTranslations) {
|
|
1488
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
1489
|
+
Object.values(debugInfo.allTranslations).forEach((langData) => {
|
|
1490
|
+
if (langData && typeof langData === "object") {
|
|
1491
|
+
Object.keys(langData).forEach((namespace) => {
|
|
1492
|
+
namespaces.add(namespace);
|
|
1493
|
+
});
|
|
148
1494
|
}
|
|
1495
|
+
});
|
|
1496
|
+
return Array.from(namespaces);
|
|
1497
|
+
}
|
|
1498
|
+
return [];
|
|
1499
|
+
} catch (error2) {
|
|
1500
|
+
return [];
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
getAllTranslations: () => {
|
|
1504
|
+
try {
|
|
1505
|
+
return translator?.debug()?.allTranslations || {};
|
|
1506
|
+
} catch (error2) {
|
|
1507
|
+
return {};
|
|
1508
|
+
}
|
|
1509
|
+
},
|
|
1510
|
+
isReady: () => {
|
|
1511
|
+
try {
|
|
1512
|
+
return translator?.isReady() || isInitialized;
|
|
1513
|
+
} catch {
|
|
1514
|
+
return isInitialized;
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
getInitializationError: () => {
|
|
1518
|
+
try {
|
|
1519
|
+
return translator?.getInitializationError() || error;
|
|
1520
|
+
} catch {
|
|
1521
|
+
return error;
|
|
1522
|
+
}
|
|
1523
|
+
},
|
|
1524
|
+
clearCache: () => {
|
|
1525
|
+
try {
|
|
1526
|
+
translator?.clearCache();
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
getCacheStats: () => {
|
|
1531
|
+
try {
|
|
1532
|
+
const debugInfo = translator?.debug();
|
|
1533
|
+
if (debugInfo && debugInfo.cacheStats) {
|
|
1534
|
+
return {
|
|
1535
|
+
size: debugInfo.cacheSize || 0,
|
|
1536
|
+
hits: debugInfo.cacheStats.hits || 0,
|
|
1537
|
+
misses: debugInfo.cacheStats.misses || 0
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
return { size: 0, hits: 0, misses: 0 };
|
|
1541
|
+
} catch (error2) {
|
|
1542
|
+
return { size: 0, hits: 0, misses: 0 };
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
reloadTranslations: async () => {
|
|
1546
|
+
if (translator) {
|
|
1547
|
+
setIsLoading(true);
|
|
1548
|
+
setError(null);
|
|
1549
|
+
try {
|
|
1550
|
+
await translator.initialize();
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
setError(err);
|
|
1553
|
+
} finally {
|
|
1554
|
+
setIsLoading(false);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}), [translator, currentLanguage, error, isInitialized, config.supportedLanguages]);
|
|
1559
|
+
const value = React.useMemo(() => ({
|
|
1560
|
+
currentLanguage,
|
|
1561
|
+
setLanguage,
|
|
1562
|
+
t,
|
|
1563
|
+
tPlural,
|
|
1564
|
+
tArray,
|
|
1565
|
+
tAsync,
|
|
1566
|
+
tSync,
|
|
1567
|
+
getRawValue,
|
|
1568
|
+
isLoading,
|
|
1569
|
+
error,
|
|
1570
|
+
supportedLanguages: config.supportedLanguages,
|
|
1571
|
+
debug,
|
|
1572
|
+
isInitialized,
|
|
1573
|
+
translationVersion
|
|
1574
|
+
}), [currentLanguage, setLanguage, t, tPlural, tArray, tAsync, tSync, getRawValue, isLoading, error, config.supportedLanguages, debug, isInitialized, translationVersion]);
|
|
1575
|
+
return /* @__PURE__ */ jsxRuntime.jsx(I18nContext.Provider, { value, children });
|
|
1576
|
+
}
|
|
1577
|
+
function useI18n() {
|
|
1578
|
+
const context = React.useContext(I18nContext);
|
|
1579
|
+
if (!context) {
|
|
1580
|
+
return {
|
|
1581
|
+
currentLanguage: "ko",
|
|
1582
|
+
setLanguage: () => {
|
|
1583
|
+
},
|
|
1584
|
+
t: (key) => key,
|
|
1585
|
+
tPlural: (key) => key,
|
|
1586
|
+
tAsync: async (key) => key,
|
|
1587
|
+
tSync: (key) => key,
|
|
1588
|
+
getRawValue: () => void 0,
|
|
1589
|
+
tArray: () => [],
|
|
1590
|
+
isLoading: false,
|
|
1591
|
+
error: null,
|
|
1592
|
+
supportedLanguages: [
|
|
1593
|
+
{ code: "ko", name: "Korean", nativeName: "\uD55C\uAD6D\uC5B4" },
|
|
1594
|
+
{ code: "en", name: "English", nativeName: "English" }
|
|
1595
|
+
],
|
|
1596
|
+
isInitialized: false,
|
|
1597
|
+
debug: {
|
|
1598
|
+
getCurrentLanguage: () => "ko",
|
|
1599
|
+
getSupportedLanguages: () => ["ko", "en"],
|
|
1600
|
+
getLoadedNamespaces: () => [],
|
|
1601
|
+
getAllTranslations: () => ({}),
|
|
1602
|
+
isReady: () => false,
|
|
1603
|
+
getInitializationError: () => null,
|
|
1604
|
+
clearCache: () => {
|
|
149
1605
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// Provider 컴포넌트 반환
|
|
155
|
-
return function CoreI18nProvider({ children }) {
|
|
156
|
-
return React.createElement(I18nProvider, { config, children });
|
|
1606
|
+
getCacheStats: () => ({ size: 0, hits: 0, misses: 0 }),
|
|
1607
|
+
reloadTranslations: async () => {
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
157
1610
|
};
|
|
1611
|
+
}
|
|
1612
|
+
return context;
|
|
1613
|
+
}
|
|
1614
|
+
function useTranslation() {
|
|
1615
|
+
const { t, tPlural, tArray, currentLanguage, setLanguage, getRawValue, isLoading, error, supportedLanguages, debug, isInitialized } = useI18n();
|
|
1616
|
+
return {
|
|
1617
|
+
t,
|
|
1618
|
+
tPlural,
|
|
1619
|
+
tArray,
|
|
1620
|
+
currentLanguage,
|
|
1621
|
+
setLanguage,
|
|
1622
|
+
getRawValue,
|
|
1623
|
+
isLoading,
|
|
1624
|
+
error,
|
|
1625
|
+
supportedLanguages,
|
|
1626
|
+
debug,
|
|
1627
|
+
isInitialized
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function useLanguageChange() {
|
|
1631
|
+
const { currentLanguage, setLanguage, supportedLanguages } = useI18n();
|
|
1632
|
+
const changeLanguage = React.useCallback((language) => {
|
|
1633
|
+
const supported = supportedLanguages.find((lang) => lang.code === language);
|
|
1634
|
+
if (supported) {
|
|
1635
|
+
setLanguage(language);
|
|
1636
|
+
} else {
|
|
1637
|
+
if (process.env.NODE_ENV !== "production") console.warn(`Language ${language} is not supported`);
|
|
1638
|
+
}
|
|
1639
|
+
}, [setLanguage, supportedLanguages]);
|
|
1640
|
+
return {
|
|
1641
|
+
currentLanguage,
|
|
1642
|
+
changeLanguage,
|
|
1643
|
+
supportedLanguages
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/index.ts
|
|
1648
|
+
var defaultLanguages = [
|
|
1649
|
+
{ code: "ko", name: "Korean", nativeName: "\uD55C\uAD6D\uC5B4" },
|
|
1650
|
+
{ code: "en", name: "English", nativeName: "English" },
|
|
1651
|
+
{ code: "en-IN", name: "English (India)", nativeName: "English (India)" },
|
|
1652
|
+
{ code: "ja", name: "Japanese", nativeName: "\u65E5\u672C\u8A9E" },
|
|
1653
|
+
{ code: "zh", name: "Chinese (Simplified)", nativeName: "\u7B80\u4F53\u4E2D\u6587" },
|
|
1654
|
+
{ code: "zh-TW", name: "Chinese (Traditional)", nativeName: "\u7E41\u9AD4\u4E2D\u6587" },
|
|
1655
|
+
{ code: "es", name: "Spanish", nativeName: "Espa\xF1ol" },
|
|
1656
|
+
{ code: "ru", name: "Russian", nativeName: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
|
|
1657
|
+
{ code: "de", name: "German", nativeName: "Deutsch" },
|
|
1658
|
+
{ code: "fr", name: "French", nativeName: "Fran\xE7ais" }
|
|
1659
|
+
];
|
|
1660
|
+
function createCoreI18n(options) {
|
|
1661
|
+
const {
|
|
1662
|
+
defaultLanguage = "ko",
|
|
1663
|
+
fallbackLanguage = "en",
|
|
1664
|
+
namespaces = ["common"],
|
|
1665
|
+
debug = false,
|
|
1666
|
+
loadTranslations,
|
|
1667
|
+
translationLoader = "api",
|
|
1668
|
+
translationApiPath = "/api/translations",
|
|
1669
|
+
initialTranslations,
|
|
1670
|
+
supportedLanguages: providedSupportedLanguages,
|
|
1671
|
+
autoLanguageSync = false,
|
|
1672
|
+
// 기본값 false (Zustand 어댑터 등 외부에서 직접 처리)
|
|
1673
|
+
baseUrl,
|
|
1674
|
+
localFallbackBaseUrl
|
|
1675
|
+
} = options || {};
|
|
1676
|
+
let supportedLanguagesConfig;
|
|
1677
|
+
if (providedSupportedLanguages) {
|
|
1678
|
+
if (Array.isArray(providedSupportedLanguages) && providedSupportedLanguages.length > 0) {
|
|
1679
|
+
if (typeof providedSupportedLanguages[0] === "string") {
|
|
1680
|
+
const languageMap = {
|
|
1681
|
+
ko: { name: "Korean", nativeName: "\uD55C\uAD6D\uC5B4" },
|
|
1682
|
+
en: { name: "English", nativeName: "English" },
|
|
1683
|
+
ja: { name: "Japanese", nativeName: "\u65E5\u672C\u8A9E" },
|
|
1684
|
+
zh: { name: "Chinese", nativeName: "\u4E2D\u6587" },
|
|
1685
|
+
es: { name: "Spanish", nativeName: "Espa\xF1ol" },
|
|
1686
|
+
fr: { name: "French", nativeName: "Fran\xE7ais" },
|
|
1687
|
+
de: { name: "German", nativeName: "Deutsch" },
|
|
1688
|
+
pt: { name: "Portuguese", nativeName: "Portugu\xEAs" },
|
|
1689
|
+
it: { name: "Italian", nativeName: "Italiano" },
|
|
1690
|
+
ru: { name: "Russian", nativeName: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" }
|
|
1691
|
+
};
|
|
1692
|
+
supportedLanguagesConfig = providedSupportedLanguages.map((code) => ({
|
|
1693
|
+
code,
|
|
1694
|
+
name: languageMap[code]?.name || code,
|
|
1695
|
+
nativeName: languageMap[code]?.nativeName || code
|
|
1696
|
+
}));
|
|
1697
|
+
} else {
|
|
1698
|
+
supportedLanguagesConfig = providedSupportedLanguages;
|
|
1699
|
+
}
|
|
1700
|
+
} else {
|
|
1701
|
+
supportedLanguagesConfig = defaultLanguages;
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
supportedLanguagesConfig = defaultLanguages;
|
|
1705
|
+
}
|
|
1706
|
+
const apiRouteLoader = async (language, namespace) => {
|
|
1707
|
+
try {
|
|
1708
|
+
if (typeof window !== "undefined") {
|
|
1709
|
+
const apiUrl = `${translationApiPath}/${language}/${namespace}`;
|
|
1710
|
+
const response = await fetch(apiUrl);
|
|
1711
|
+
if (response.ok) {
|
|
1712
|
+
const data = await response.json();
|
|
1713
|
+
if (debug) {
|
|
1714
|
+
console.log(`\u2705 Loaded translation from API: ${language}/${namespace}`);
|
|
1715
|
+
}
|
|
1716
|
+
return data;
|
|
1717
|
+
} else if (response.status === 404) {
|
|
1718
|
+
if (debug) {
|
|
1719
|
+
console.warn(`\u26A0\uFE0F Translation not found in API: ${language}/${namespace}`);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return getDefaultTranslations(language, namespace);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
if (debug) {
|
|
1726
|
+
console.warn(`Failed to load translation from API: ${language}/${namespace}`, error);
|
|
1727
|
+
}
|
|
1728
|
+
return getDefaultTranslations(language, namespace);
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
const staticFileLoader = async (language, namespace) => {
|
|
1732
|
+
try {
|
|
1733
|
+
let data = null;
|
|
1734
|
+
if (typeof window !== "undefined") {
|
|
1735
|
+
const possiblePaths = [
|
|
1736
|
+
`/translations/${language}/${namespace}.json`,
|
|
1737
|
+
`../translations/${language}/${namespace}.json`,
|
|
1738
|
+
`./translations/${language}/${namespace}.json`,
|
|
1739
|
+
`translations/${language}/${namespace}.json`,
|
|
1740
|
+
`../../translations/${language}/${namespace}.json`
|
|
1741
|
+
];
|
|
1742
|
+
for (const path of possiblePaths) {
|
|
1743
|
+
try {
|
|
1744
|
+
const response = await fetch(path);
|
|
1745
|
+
if (response.ok) {
|
|
1746
|
+
data = await response.json();
|
|
1747
|
+
if (debug) {
|
|
1748
|
+
console.log(`\u2705 Loaded translation from static path: ${path}`);
|
|
1749
|
+
}
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
} catch (pathError) {
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (data) {
|
|
1758
|
+
return data;
|
|
1759
|
+
}
|
|
1760
|
+
return getDefaultTranslations(language, namespace);
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
if (debug) {
|
|
1763
|
+
console.warn(`Failed to load translation file: ${language}/${namespace}.json`);
|
|
1764
|
+
}
|
|
1765
|
+
return getDefaultTranslations(language, namespace);
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
const defaultFileLoader = translationLoader === "api" ? apiRouteLoader : translationLoader === "static" ? staticFileLoader : loadTranslations || apiRouteLoader;
|
|
1769
|
+
const config = {
|
|
1770
|
+
defaultLanguage,
|
|
1771
|
+
fallbackLanguage,
|
|
1772
|
+
supportedLanguages: supportedLanguagesConfig,
|
|
1773
|
+
namespaces,
|
|
1774
|
+
loadTranslations: translationLoader === "custom" && loadTranslations ? loadTranslations : defaultFileLoader,
|
|
1775
|
+
initialTranslations,
|
|
1776
|
+
// SSR 번역 데이터 전달
|
|
1777
|
+
debug,
|
|
1778
|
+
missingKeyHandler: (key, language, namespace) => {
|
|
1779
|
+
if (debug) {
|
|
1780
|
+
console.warn(`Missing translation key: ${key}`);
|
|
1781
|
+
if (typeof window !== "undefined" && window.__I18N_DEBUG_MISSING_KEYS__) {
|
|
1782
|
+
const missingKeys = window.__I18N_DEBUG_MISSING_KEYS__;
|
|
1783
|
+
const keyPath = `${language || "unknown"}:${namespace || "unknown"}`;
|
|
1784
|
+
missingKeys[keyPath] = missingKeys[keyPath] || [];
|
|
1785
|
+
if (!missingKeys[keyPath].includes(key)) {
|
|
1786
|
+
missingKeys[keyPath].push(key);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return `[MISSING: ${key}]`;
|
|
1790
|
+
}
|
|
1791
|
+
return key.split(".").pop() || key;
|
|
1792
|
+
},
|
|
1793
|
+
errorHandler: (error, language, namespace) => {
|
|
1794
|
+
if (debug) {
|
|
1795
|
+
console.error(`Translation error for ${language}:${namespace}:`, error);
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
// autoLanguageSync는 기본적으로 false (Zustand 어댑터 등 외부에서 직접 처리하는 경우)
|
|
1799
|
+
// 필요시 options에서 명시적으로 true로 설정 가능
|
|
1800
|
+
autoLanguageSync: options?.autoLanguageSync ?? false
|
|
1801
|
+
};
|
|
1802
|
+
return function CoreI18nProvider({ children }) {
|
|
1803
|
+
return React__default.default.createElement(I18nProvider, { config, children });
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
function CoreProvider({ children }) {
|
|
1807
|
+
return createCoreI18n()({ children });
|
|
1808
|
+
}
|
|
1809
|
+
function createLanguageProvider(language) {
|
|
1810
|
+
return createCoreI18n({ defaultLanguage: language });
|
|
1811
|
+
}
|
|
1812
|
+
function createNamespaceProvider(namespaces) {
|
|
1813
|
+
return createCoreI18n({ namespaces });
|
|
1814
|
+
}
|
|
1815
|
+
function createCustomLoaderProvider(loadTranslations) {
|
|
1816
|
+
return createCoreI18n({ loadTranslations });
|
|
158
1817
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* 네임스페이스별 Provider (네임스페이스만 지정)
|
|
175
|
-
*/
|
|
176
|
-
export function createNamespaceProvider(namespaces) {
|
|
177
|
-
return createCoreI18n({ namespaces });
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* 커스텀 로더 Provider (사용자 정의 번역 로더 사용)
|
|
181
|
-
*/
|
|
182
|
-
export function createCustomLoaderProvider(loadTranslations) {
|
|
183
|
-
return createCoreI18n({ loadTranslations });
|
|
184
|
-
}
|
|
185
|
-
// 핵심 훅들 export
|
|
186
|
-
export { useTranslation, useLanguageChange, useI18n };
|
|
187
|
-
// Provider export
|
|
188
|
-
export { I18nProvider };
|
|
189
|
-
// 핵심 클래스/함수들 export
|
|
190
|
-
export { Translator, ssrTranslate, serverTranslate };
|
|
1818
|
+
|
|
1819
|
+
exports.CoreProvider = CoreProvider;
|
|
1820
|
+
exports.I18nProvider = I18nProvider;
|
|
1821
|
+
exports.Translator = Translator;
|
|
1822
|
+
exports.createCoreI18n = createCoreI18n;
|
|
1823
|
+
exports.createCustomLoaderProvider = createCustomLoaderProvider;
|
|
1824
|
+
exports.createLanguageProvider = createLanguageProvider;
|
|
1825
|
+
exports.createNamespaceProvider = createNamespaceProvider;
|
|
1826
|
+
exports.serverTranslate = serverTranslate;
|
|
1827
|
+
exports.ssrTranslate = ssrTranslate;
|
|
1828
|
+
exports.useI18n = useI18n;
|
|
1829
|
+
exports.useLanguageChange = useLanguageChange;
|
|
1830
|
+
exports.useTranslation = useTranslation;
|
|
1831
|
+
//# sourceMappingURL=index.js.map
|
|
191
1832
|
//# sourceMappingURL=index.js.map
|