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