@hua-labs/i18n-core 1.0.0 → 1.1.0-alpha.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +636 -636
- package/dist/core/debug-tools.js +53 -53
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MissingKeyOverlay.tsx +222 -222
- package/src/core/debug-tools.ts +297 -297
- package/src/core/i18n-resource.ts +179 -179
- package/src/core/lazy-loader.ts +254 -254
- package/src/core/translator-factory.ts +136 -136
- package/src/core/translator.tsx +1193 -1193
- package/src/hooks/useI18n.tsx +594 -594
- package/src/hooks/useTranslation.tsx +61 -61
- package/src/index.ts +297 -297
- package/src/types/index.ts +442 -442
- package/src/utils/default-translations.ts +129 -129
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
import { TranslationNamespace, TranslationData } from '../types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* i18n 리소스 관리자
|
|
5
|
-
* SSR 환경에서 동일 번역 요청 중복 방지 및 전역 캐시 관리
|
|
6
|
-
*/
|
|
7
|
-
export class I18nResourceManager {
|
|
8
|
-
private static instance: I18nResourceManager;
|
|
9
|
-
private globalCache = new Map<string, TranslationNamespace>();
|
|
10
|
-
private loadingPromises = new Map<string, Promise<TranslationNamespace>>();
|
|
11
|
-
private cacheStats = {
|
|
12
|
-
hits: 0,
|
|
13
|
-
misses: 0,
|
|
14
|
-
size: 0
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
private constructor() {}
|
|
18
|
-
|
|
19
|
-
static getInstance(): I18nResourceManager {
|
|
20
|
-
if (!I18nResourceManager.instance) {
|
|
21
|
-
I18nResourceManager.instance = new I18nResourceManager();
|
|
22
|
-
}
|
|
23
|
-
return I18nResourceManager.instance;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 전역 캐시에서 번역 데이터 가져오기
|
|
28
|
-
*/
|
|
29
|
-
async getCachedTranslations(
|
|
30
|
-
language: string,
|
|
31
|
-
namespace: string,
|
|
32
|
-
loader: (lang: string, ns: string) => Promise<TranslationNamespace>
|
|
33
|
-
): Promise<TranslationNamespace> {
|
|
34
|
-
const cacheKey = `${language}:${namespace}`;
|
|
35
|
-
|
|
36
|
-
// 캐시에 있으면 반환
|
|
37
|
-
if (this.globalCache.has(cacheKey)) {
|
|
38
|
-
this.cacheStats.hits++;
|
|
39
|
-
return this.globalCache.get(cacheKey)!;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 이미 로딩 중이면 기존 Promise 반환
|
|
43
|
-
if (this.loadingPromises.has(cacheKey)) {
|
|
44
|
-
return this.loadingPromises.get(cacheKey)!;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 새로 로딩
|
|
48
|
-
this.cacheStats.misses++;
|
|
49
|
-
const loadPromise = loader(language, namespace).then(data => {
|
|
50
|
-
this.globalCache.set(cacheKey, data);
|
|
51
|
-
this.cacheStats.size = this.globalCache.size;
|
|
52
|
-
this.loadingPromises.delete(cacheKey);
|
|
53
|
-
return data;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
this.loadingPromises.set(cacheKey, loadPromise);
|
|
57
|
-
return loadPromise;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 캐시된 번역 데이터 직접 접근
|
|
62
|
-
*/
|
|
63
|
-
getCachedTranslationsSync(language: string, namespace: string): TranslationNamespace | null {
|
|
64
|
-
const cacheKey = `${language}:${namespace}`;
|
|
65
|
-
return this.globalCache.get(cacheKey) || null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 특정 언어의 모든 네임스페이스 가져오기
|
|
70
|
-
*/
|
|
71
|
-
getAllTranslationsForLanguage(language: string): Record<string, TranslationNamespace> {
|
|
72
|
-
const result: Record<string, TranslationNamespace> = {};
|
|
73
|
-
|
|
74
|
-
for (const [key, data] of this.globalCache.entries()) {
|
|
75
|
-
if (key.startsWith(`${language}:`)) {
|
|
76
|
-
const namespace = key.split(':')[1];
|
|
77
|
-
result[namespace] = data;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 모든 캐시된 번역 데이터 가져오기
|
|
86
|
-
*/
|
|
87
|
-
getAllCachedTranslations(): Record<string, Record<string, TranslationNamespace>> {
|
|
88
|
-
const result: Record<string, Record<string, TranslationNamespace>> = {};
|
|
89
|
-
|
|
90
|
-
for (const [key, data] of this.globalCache.entries()) {
|
|
91
|
-
const [language, namespace] = key.split(':');
|
|
92
|
-
if (!result[language]) {
|
|
93
|
-
result[language] = {};
|
|
94
|
-
}
|
|
95
|
-
result[language][namespace] = data;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 캐시 통계 가져오기
|
|
103
|
-
*/
|
|
104
|
-
getCacheStats() {
|
|
105
|
-
return {
|
|
106
|
-
...this.cacheStats,
|
|
107
|
-
hitRate: this.cacheStats.hits / (this.cacheStats.hits + this.cacheStats.misses)
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 캐시 무효화
|
|
113
|
-
*/
|
|
114
|
-
invalidateCache(language?: string, namespace?: string): void {
|
|
115
|
-
if (language && namespace) {
|
|
116
|
-
// 특정 언어/네임스페이스만 무효화
|
|
117
|
-
const cacheKey = `${language}:${namespace}`;
|
|
118
|
-
this.globalCache.delete(cacheKey);
|
|
119
|
-
this.loadingPromises.delete(cacheKey);
|
|
120
|
-
} else if (language) {
|
|
121
|
-
// 특정 언어의 모든 네임스페이스 무효화
|
|
122
|
-
for (const key of this.globalCache.keys()) {
|
|
123
|
-
if (key.startsWith(`${language}:`)) {
|
|
124
|
-
this.globalCache.delete(key);
|
|
125
|
-
this.loadingPromises.delete(key);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
// 전체 캐시 무효화
|
|
130
|
-
this.globalCache.clear();
|
|
131
|
-
this.loadingPromises.clear();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.cacheStats.size = this.globalCache.size;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 캐시 크기 제한 설정
|
|
139
|
-
*/
|
|
140
|
-
setCacheLimit(maxSize: number): void {
|
|
141
|
-
if (this.globalCache.size > maxSize) {
|
|
142
|
-
// LRU 방식으로 오래된 항목 제거
|
|
143
|
-
const entries = Array.from(this.globalCache.entries());
|
|
144
|
-
const toRemove = entries.slice(0, this.globalCache.size - maxSize);
|
|
145
|
-
|
|
146
|
-
for (const [key] of toRemove) {
|
|
147
|
-
this.globalCache.delete(key);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.cacheStats.size = this.globalCache.size;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* SSR 환경에서 하이드레이션
|
|
156
|
-
*/
|
|
157
|
-
hydrateFromSSR(translations: Record<string, Record<string, TranslationNamespace>>): void {
|
|
158
|
-
for (const [language, namespaces] of Object.entries(translations)) {
|
|
159
|
-
for (const [namespace, data] of Object.entries(namespaces)) {
|
|
160
|
-
const cacheKey = `${language}:${namespace}`;
|
|
161
|
-
this.globalCache.set(cacheKey, data);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
this.cacheStats.size = this.globalCache.size;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 메모리 사용량 최적화
|
|
170
|
-
*/
|
|
171
|
-
optimizeMemory(): void {
|
|
172
|
-
// 사용되지 않는 번역 데이터 정리
|
|
173
|
-
// 실제 구현에서는 사용 통계를 기반으로 정리
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 전역 리소스 매니저 인스턴스
|
|
179
|
-
*/
|
|
1
|
+
import { TranslationNamespace, TranslationData } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* i18n 리소스 관리자
|
|
5
|
+
* SSR 환경에서 동일 번역 요청 중복 방지 및 전역 캐시 관리
|
|
6
|
+
*/
|
|
7
|
+
export class I18nResourceManager {
|
|
8
|
+
private static instance: I18nResourceManager;
|
|
9
|
+
private globalCache = new Map<string, TranslationNamespace>();
|
|
10
|
+
private loadingPromises = new Map<string, Promise<TranslationNamespace>>();
|
|
11
|
+
private cacheStats = {
|
|
12
|
+
hits: 0,
|
|
13
|
+
misses: 0,
|
|
14
|
+
size: 0
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
private constructor() {}
|
|
18
|
+
|
|
19
|
+
static getInstance(): I18nResourceManager {
|
|
20
|
+
if (!I18nResourceManager.instance) {
|
|
21
|
+
I18nResourceManager.instance = new I18nResourceManager();
|
|
22
|
+
}
|
|
23
|
+
return I18nResourceManager.instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 전역 캐시에서 번역 데이터 가져오기
|
|
28
|
+
*/
|
|
29
|
+
async getCachedTranslations(
|
|
30
|
+
language: string,
|
|
31
|
+
namespace: string,
|
|
32
|
+
loader: (lang: string, ns: string) => Promise<TranslationNamespace>
|
|
33
|
+
): Promise<TranslationNamespace> {
|
|
34
|
+
const cacheKey = `${language}:${namespace}`;
|
|
35
|
+
|
|
36
|
+
// 캐시에 있으면 반환
|
|
37
|
+
if (this.globalCache.has(cacheKey)) {
|
|
38
|
+
this.cacheStats.hits++;
|
|
39
|
+
return this.globalCache.get(cacheKey)!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 이미 로딩 중이면 기존 Promise 반환
|
|
43
|
+
if (this.loadingPromises.has(cacheKey)) {
|
|
44
|
+
return this.loadingPromises.get(cacheKey)!;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 새로 로딩
|
|
48
|
+
this.cacheStats.misses++;
|
|
49
|
+
const loadPromise = loader(language, namespace).then(data => {
|
|
50
|
+
this.globalCache.set(cacheKey, data);
|
|
51
|
+
this.cacheStats.size = this.globalCache.size;
|
|
52
|
+
this.loadingPromises.delete(cacheKey);
|
|
53
|
+
return data;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.loadingPromises.set(cacheKey, loadPromise);
|
|
57
|
+
return loadPromise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 캐시된 번역 데이터 직접 접근
|
|
62
|
+
*/
|
|
63
|
+
getCachedTranslationsSync(language: string, namespace: string): TranslationNamespace | null {
|
|
64
|
+
const cacheKey = `${language}:${namespace}`;
|
|
65
|
+
return this.globalCache.get(cacheKey) || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 특정 언어의 모든 네임스페이스 가져오기
|
|
70
|
+
*/
|
|
71
|
+
getAllTranslationsForLanguage(language: string): Record<string, TranslationNamespace> {
|
|
72
|
+
const result: Record<string, TranslationNamespace> = {};
|
|
73
|
+
|
|
74
|
+
for (const [key, data] of this.globalCache.entries()) {
|
|
75
|
+
if (key.startsWith(`${language}:`)) {
|
|
76
|
+
const namespace = key.split(':')[1];
|
|
77
|
+
result[namespace] = data;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 모든 캐시된 번역 데이터 가져오기
|
|
86
|
+
*/
|
|
87
|
+
getAllCachedTranslations(): Record<string, Record<string, TranslationNamespace>> {
|
|
88
|
+
const result: Record<string, Record<string, TranslationNamespace>> = {};
|
|
89
|
+
|
|
90
|
+
for (const [key, data] of this.globalCache.entries()) {
|
|
91
|
+
const [language, namespace] = key.split(':');
|
|
92
|
+
if (!result[language]) {
|
|
93
|
+
result[language] = {};
|
|
94
|
+
}
|
|
95
|
+
result[language][namespace] = data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 캐시 통계 가져오기
|
|
103
|
+
*/
|
|
104
|
+
getCacheStats() {
|
|
105
|
+
return {
|
|
106
|
+
...this.cacheStats,
|
|
107
|
+
hitRate: this.cacheStats.hits / (this.cacheStats.hits + this.cacheStats.misses)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 캐시 무효화
|
|
113
|
+
*/
|
|
114
|
+
invalidateCache(language?: string, namespace?: string): void {
|
|
115
|
+
if (language && namespace) {
|
|
116
|
+
// 특정 언어/네임스페이스만 무효화
|
|
117
|
+
const cacheKey = `${language}:${namespace}`;
|
|
118
|
+
this.globalCache.delete(cacheKey);
|
|
119
|
+
this.loadingPromises.delete(cacheKey);
|
|
120
|
+
} else if (language) {
|
|
121
|
+
// 특정 언어의 모든 네임스페이스 무효화
|
|
122
|
+
for (const key of this.globalCache.keys()) {
|
|
123
|
+
if (key.startsWith(`${language}:`)) {
|
|
124
|
+
this.globalCache.delete(key);
|
|
125
|
+
this.loadingPromises.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// 전체 캐시 무효화
|
|
130
|
+
this.globalCache.clear();
|
|
131
|
+
this.loadingPromises.clear();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.cacheStats.size = this.globalCache.size;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 캐시 크기 제한 설정
|
|
139
|
+
*/
|
|
140
|
+
setCacheLimit(maxSize: number): void {
|
|
141
|
+
if (this.globalCache.size > maxSize) {
|
|
142
|
+
// LRU 방식으로 오래된 항목 제거
|
|
143
|
+
const entries = Array.from(this.globalCache.entries());
|
|
144
|
+
const toRemove = entries.slice(0, this.globalCache.size - maxSize);
|
|
145
|
+
|
|
146
|
+
for (const [key] of toRemove) {
|
|
147
|
+
this.globalCache.delete(key);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.cacheStats.size = this.globalCache.size;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* SSR 환경에서 하이드레이션
|
|
156
|
+
*/
|
|
157
|
+
hydrateFromSSR(translations: Record<string, Record<string, TranslationNamespace>>): void {
|
|
158
|
+
for (const [language, namespaces] of Object.entries(translations)) {
|
|
159
|
+
for (const [namespace, data] of Object.entries(namespaces)) {
|
|
160
|
+
const cacheKey = `${language}:${namespace}`;
|
|
161
|
+
this.globalCache.set(cacheKey, data);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.cacheStats.size = this.globalCache.size;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 메모리 사용량 최적화
|
|
170
|
+
*/
|
|
171
|
+
optimizeMemory(): void {
|
|
172
|
+
// 사용되지 않는 번역 데이터 정리
|
|
173
|
+
// 실제 구현에서는 사용 통계를 기반으로 정리
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 전역 리소스 매니저 인스턴스
|
|
179
|
+
*/
|
|
180
180
|
export const i18nResourceManager = I18nResourceManager.getInstance();
|