@hua-labs/i18n-loaders 2.0.5 → 2.1.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/package.json +6 -6
- package/dist/index.d.ts +0 -80
- package/dist/index.js +0 -272
- package/dist/index.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hua-labs/i18n-loaders",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Production-ready loaders, caching, and preloading helpers for @hua-labs/i18n-core.",
|
|
5
|
-
"main": "./dist/index.
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
".": {
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"import": "./dist/index.mjs",
|
|
16
|
-
"
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"sideEffects": false,
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"node": ">=20.0.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@hua-labs/i18n-core": "
|
|
24
|
+
"@hua-labs/i18n-core": "2.1.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": ">=19.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^25.
|
|
30
|
+
"@types/node": "^25.3.0",
|
|
31
31
|
"@types/react": "^19.2.14",
|
|
32
|
-
"eslint": "^10.0.
|
|
32
|
+
"eslint": "^10.0.1",
|
|
33
33
|
"react": "^19.2.4",
|
|
34
34
|
"tsup": "^8.5.1",
|
|
35
35
|
"typescript": "^5.9.3",
|
package/dist/index.d.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
type TranslationRecord = Record<string, unknown>;
|
|
2
|
-
type TranslationLoader = (language: string, namespace: string) => Promise<TranslationRecord>;
|
|
3
|
-
/**
|
|
4
|
-
* 캐시 무효화 함수 타입
|
|
5
|
-
*/
|
|
6
|
-
interface CacheInvalidation {
|
|
7
|
-
/**
|
|
8
|
-
* 특정 언어/네임스페이스의 캐시 무효화
|
|
9
|
-
* @param language - 언어 코드 (선택적, 없으면 모든 언어)
|
|
10
|
-
* @param namespace - 네임스페이스 (선택적, 없으면 모든 네임스페이스)
|
|
11
|
-
*/
|
|
12
|
-
invalidate?: (language?: string, namespace?: string) => void;
|
|
13
|
-
/**
|
|
14
|
-
* 전체 캐시 클리어
|
|
15
|
-
*/
|
|
16
|
-
clear?: () => void;
|
|
17
|
-
}
|
|
18
|
-
interface ApiLoaderOptions {
|
|
19
|
-
translationApiPath?: string;
|
|
20
|
-
baseUrl?: string;
|
|
21
|
-
localFallbackBaseUrl?: string;
|
|
22
|
-
cacheTtlMs?: number;
|
|
23
|
-
disableCache?: boolean;
|
|
24
|
-
requestInit?: RequestInit | ((language: string, namespace: string) => RequestInit | undefined);
|
|
25
|
-
fetcher?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
26
|
-
logger?: Pick<typeof console, 'log' | 'warn' | 'error'>;
|
|
27
|
-
/** 재시도 횟수 (기본값: 0, 재시도 안 함) */
|
|
28
|
-
retryCount?: number;
|
|
29
|
-
/** 재시도 지연 시간 (밀리초, 기본값: 1000) */
|
|
30
|
-
retryDelay?: number;
|
|
31
|
-
/**
|
|
32
|
-
* 개발 모드에서 자동 캐시 무효화
|
|
33
|
-
* 기본값: true (개발 모드에서만)
|
|
34
|
-
* 개발 중 번역 파일 변경 시 캐시를 자동으로 무효화하여 최신 번역을 즉시 반영
|
|
35
|
-
*/
|
|
36
|
-
autoInvalidateInDev?: boolean;
|
|
37
|
-
}
|
|
38
|
-
interface PreloadOptions {
|
|
39
|
-
logger?: Pick<typeof console, 'log' | 'warn'>;
|
|
40
|
-
suppressErrors?: boolean;
|
|
41
|
-
}
|
|
42
|
-
type DefaultTranslations = Record<string, Record<string, TranslationRecord>>;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* API 기반 번역 로더 생성
|
|
46
|
-
*
|
|
47
|
-
* @param options - 로더 옵션
|
|
48
|
-
* @returns 번역 로더 및 캐시 무효화 함수
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```typescript
|
|
52
|
-
* const { loader, invalidate, clear } = createApiTranslationLoader({
|
|
53
|
-
* translationApiPath: '/api/translations',
|
|
54
|
-
* autoInvalidateInDev: true
|
|
55
|
-
* });
|
|
56
|
-
*
|
|
57
|
-
* // 특정 언어/네임스페이스 무효화
|
|
58
|
-
* invalidate('ko', 'common');
|
|
59
|
-
*
|
|
60
|
-
* // 전체 캐시 클리어
|
|
61
|
-
* clear();
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
declare function createApiTranslationLoader(options?: ApiLoaderOptions): TranslationLoader & {
|
|
65
|
-
invalidate: (language?: string, namespace?: string) => void;
|
|
66
|
-
clear: () => void;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
declare function preloadNamespaces(language: string, namespaces: string[], loader: TranslationLoader, options?: PreloadOptions): Promise<{
|
|
70
|
-
fulfilled: string[];
|
|
71
|
-
rejected: any[];
|
|
72
|
-
}>;
|
|
73
|
-
declare function warmFallbackLanguages(currentLanguage: string, languages: string[], namespaces: string[], loader: TranslationLoader, options?: PreloadOptions): Promise<{
|
|
74
|
-
fulfilled: string[];
|
|
75
|
-
rejected: any[];
|
|
76
|
-
}[]>;
|
|
77
|
-
|
|
78
|
-
declare function withDefaultTranslations(loader: TranslationLoader, defaults: DefaultTranslations): TranslationLoader;
|
|
79
|
-
|
|
80
|
-
export { type ApiLoaderOptions, type CacheInvalidation, type DefaultTranslations, type PreloadOptions, type TranslationLoader, type TranslationRecord, createApiTranslationLoader, preloadNamespaces, warmFallbackLanguages, withDefaultTranslations };
|
package/dist/index.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// src/api-loader.ts
|
|
4
|
-
var FIVE_MINUTES = 5 * 60 * 1e3;
|
|
5
|
-
var defaultFetcher = (input, init) => fetch(input, init);
|
|
6
|
-
function isRetryableError(error) {
|
|
7
|
-
if (error instanceof TypeError) {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
if (error instanceof Error) {
|
|
11
|
-
const message = error.message.toLowerCase();
|
|
12
|
-
if (message.includes("failed to fetch") || message.includes("networkerror") || message.includes("network request failed")) {
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
if (error && typeof error === "object" && "status" in error) {
|
|
17
|
-
const status = error.status;
|
|
18
|
-
if (status >= 500 && status < 600) {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
if (status === 408) {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
function createApiTranslationLoader(options = {}) {
|
|
28
|
-
const translationApiPath = options.translationApiPath ?? "/api/translations";
|
|
29
|
-
const cacheTtlMs = options.cacheTtlMs ?? FIVE_MINUTES;
|
|
30
|
-
const fetcher = options.fetcher ?? defaultFetcher;
|
|
31
|
-
const logger = options.logger ?? console;
|
|
32
|
-
const retryCount = options.retryCount ?? 0;
|
|
33
|
-
const retryDelay = options.retryDelay ?? 1e3;
|
|
34
|
-
const autoInvalidateInDev = options.autoInvalidateInDev ?? (typeof process !== "undefined" && process.env.NODE_ENV === "development");
|
|
35
|
-
const localCache = /* @__PURE__ */ new Map();
|
|
36
|
-
const inFlightRequests = /* @__PURE__ */ new Map();
|
|
37
|
-
const buildUrl = (language, namespace) => {
|
|
38
|
-
const safeNamespace = namespace.replace(/[^a-zA-Z0-9-_]/g, "");
|
|
39
|
-
const path = `${translationApiPath}/${language}/${safeNamespace}`;
|
|
40
|
-
if (typeof window !== "undefined") {
|
|
41
|
-
return path;
|
|
42
|
-
}
|
|
43
|
-
if (options.baseUrl) {
|
|
44
|
-
return `${options.baseUrl}${path}`;
|
|
45
|
-
}
|
|
46
|
-
if (process.env.NEXT_PUBLIC_SITE_URL) {
|
|
47
|
-
return `${process.env.NEXT_PUBLIC_SITE_URL}${path}`;
|
|
48
|
-
}
|
|
49
|
-
if (process.env.VERCEL_URL) {
|
|
50
|
-
const vercelUrl = process.env.VERCEL_URL.startsWith("http") ? process.env.VERCEL_URL : `https://${process.env.VERCEL_URL}`;
|
|
51
|
-
return `${vercelUrl}${path}`;
|
|
52
|
-
}
|
|
53
|
-
const fallbackBase = options.localFallbackBaseUrl ?? "http://localhost:3000";
|
|
54
|
-
return `${fallbackBase}${path}`;
|
|
55
|
-
};
|
|
56
|
-
const getRequestInit = (language, namespace) => {
|
|
57
|
-
if (typeof options.requestInit === "function") {
|
|
58
|
-
return options.requestInit(language, namespace) ?? {};
|
|
59
|
-
}
|
|
60
|
-
return options.requestInit ?? {};
|
|
61
|
-
};
|
|
62
|
-
const getCached = (cacheKey) => {
|
|
63
|
-
if (options.disableCache) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
const entry = localCache.get(cacheKey);
|
|
67
|
-
if (!entry) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
if (entry.expiresAt < Date.now()) {
|
|
71
|
-
localCache.delete(cacheKey);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
return entry.data;
|
|
75
|
-
};
|
|
76
|
-
const setCached = (cacheKey, data) => {
|
|
77
|
-
if (options.disableCache) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
localCache.set(cacheKey, {
|
|
81
|
-
data,
|
|
82
|
-
expiresAt: Date.now() + cacheTtlMs
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
const loadTranslations = async (language, namespace) => {
|
|
86
|
-
const cacheKey = `${language}:${namespace}`;
|
|
87
|
-
const cached = getCached(cacheKey);
|
|
88
|
-
if (cached) {
|
|
89
|
-
return cached;
|
|
90
|
-
}
|
|
91
|
-
const inFlight = inFlightRequests.get(cacheKey);
|
|
92
|
-
if (inFlight) {
|
|
93
|
-
return inFlight;
|
|
94
|
-
}
|
|
95
|
-
const url = buildUrl(language, namespace);
|
|
96
|
-
const requestInit = getRequestInit(language, namespace);
|
|
97
|
-
const performRequest = async (attempt) => {
|
|
98
|
-
try {
|
|
99
|
-
const response = await fetcher(url, {
|
|
100
|
-
cache: "no-store",
|
|
101
|
-
...requestInit
|
|
102
|
-
});
|
|
103
|
-
if (!response.ok) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`[i18n-loaders] Failed to load ${language}/${namespace} (${response.status})`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
const data = await response.json();
|
|
109
|
-
setCached(cacheKey, data);
|
|
110
|
-
return data;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
const isRetryable = isRetryableError(error);
|
|
113
|
-
if (isRetryable && attempt < retryCount) {
|
|
114
|
-
logger.warn?.(
|
|
115
|
-
`[i18n-loaders] Translation fetch failed (attempt ${attempt + 1}/${retryCount + 1}), retrying...`,
|
|
116
|
-
language,
|
|
117
|
-
namespace,
|
|
118
|
-
error
|
|
119
|
-
);
|
|
120
|
-
const delay = retryDelay * Math.pow(2, attempt);
|
|
121
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
122
|
-
return performRequest(attempt + 1);
|
|
123
|
-
}
|
|
124
|
-
logger.warn?.(
|
|
125
|
-
"[i18n-loaders] translation fetch failed",
|
|
126
|
-
language,
|
|
127
|
-
namespace,
|
|
128
|
-
error
|
|
129
|
-
);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const requestPromise = performRequest(0).finally(() => {
|
|
134
|
-
inFlightRequests.delete(cacheKey);
|
|
135
|
-
});
|
|
136
|
-
inFlightRequests.set(cacheKey, requestPromise);
|
|
137
|
-
return requestPromise;
|
|
138
|
-
};
|
|
139
|
-
const invalidate = (language, namespace) => {
|
|
140
|
-
if (language && namespace) {
|
|
141
|
-
const cacheKey = `${language}:${namespace}`;
|
|
142
|
-
localCache.delete(cacheKey);
|
|
143
|
-
logger.log?.("[i18n-loaders] Cache invalidated:", cacheKey);
|
|
144
|
-
} else if (language) {
|
|
145
|
-
const prefix = `${language}:`;
|
|
146
|
-
for (const key of localCache.keys()) {
|
|
147
|
-
if (key.startsWith(prefix)) {
|
|
148
|
-
localCache.delete(key);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
logger.log?.("[i18n-loaders] Cache invalidated for language:", language);
|
|
152
|
-
} else if (namespace) {
|
|
153
|
-
const suffix = `:${namespace}`;
|
|
154
|
-
for (const key of localCache.keys()) {
|
|
155
|
-
if (key.endsWith(suffix)) {
|
|
156
|
-
localCache.delete(key);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
logger.log?.("[i18n-loaders] Cache invalidated for namespace:", namespace);
|
|
160
|
-
} else {
|
|
161
|
-
clear();
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
const clear = () => {
|
|
165
|
-
localCache.clear();
|
|
166
|
-
logger.log?.("[i18n-loaders] Cache cleared");
|
|
167
|
-
};
|
|
168
|
-
if (autoInvalidateInDev && typeof window !== "undefined") {
|
|
169
|
-
window.addEventListener("focus", () => {
|
|
170
|
-
if (process.env.NODE_ENV === "development") {
|
|
171
|
-
logger.log?.("[i18n-loaders] Development mode: Auto-invalidating cache on focus");
|
|
172
|
-
clear();
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
const loader = loadTranslations;
|
|
177
|
-
loader.invalidate = invalidate;
|
|
178
|
-
loader.clear = clear;
|
|
179
|
-
return loader;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/preload.ts
|
|
183
|
-
var defaultLogger = console;
|
|
184
|
-
async function preloadNamespaces(language, namespaces, loader, options = {}) {
|
|
185
|
-
const logger = options.logger ?? defaultLogger;
|
|
186
|
-
const results = await Promise.allSettled(
|
|
187
|
-
namespaces.map(async (namespace) => {
|
|
188
|
-
await loader(language, namespace);
|
|
189
|
-
return namespace;
|
|
190
|
-
})
|
|
191
|
-
);
|
|
192
|
-
const fulfilled = results.filter(
|
|
193
|
-
(result) => result.status === "fulfilled"
|
|
194
|
-
);
|
|
195
|
-
const rejected = results.filter(
|
|
196
|
-
(result) => result.status === "rejected"
|
|
197
|
-
);
|
|
198
|
-
if (fulfilled.length > 0) {
|
|
199
|
-
logger.log?.(
|
|
200
|
-
`[i18n-loaders] Preloaded ${fulfilled.length}/${namespaces.length} namespaces for ${language}`
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
if (rejected.length > 0 && !options.suppressErrors) {
|
|
204
|
-
logger.warn?.(
|
|
205
|
-
`[i18n-loaders] Failed to preload ${rejected.length} namespaces for ${language}`
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
fulfilled: fulfilled.map((result) => result.value),
|
|
210
|
-
rejected: rejected.map((result) => result.reason)
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
async function warmFallbackLanguages(currentLanguage, languages, namespaces, loader, options = {}) {
|
|
214
|
-
const targets = languages.filter((language) => language !== currentLanguage);
|
|
215
|
-
if (targets.length === 0) {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
return Promise.all(
|
|
219
|
-
targets.map(
|
|
220
|
-
(language) => preloadNamespaces(language, namespaces, loader, options)
|
|
221
|
-
)
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// src/defaults.ts
|
|
226
|
-
function withDefaultTranslations(loader, defaults) {
|
|
227
|
-
return async (language, namespace) => {
|
|
228
|
-
const fallback = defaults[language]?.[namespace];
|
|
229
|
-
try {
|
|
230
|
-
const remote = await loader(language, namespace);
|
|
231
|
-
if (!remote || typeof remote === "object" && Object.keys(remote).length === 0) {
|
|
232
|
-
if (fallback) {
|
|
233
|
-
return fallback;
|
|
234
|
-
}
|
|
235
|
-
return remote || {};
|
|
236
|
-
}
|
|
237
|
-
if (!fallback) {
|
|
238
|
-
return remote;
|
|
239
|
-
}
|
|
240
|
-
return mergeTranslations(fallback, remote);
|
|
241
|
-
} catch (error) {
|
|
242
|
-
if (fallback) {
|
|
243
|
-
return fallback;
|
|
244
|
-
}
|
|
245
|
-
throw error;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
function mergeTranslations(base, override) {
|
|
250
|
-
const result = { ...base };
|
|
251
|
-
for (const [key, value] of Object.entries(override)) {
|
|
252
|
-
if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
253
|
-
result[key] = mergeTranslations(
|
|
254
|
-
result[key],
|
|
255
|
-
value
|
|
256
|
-
);
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
result[key] = value;
|
|
260
|
-
}
|
|
261
|
-
return result;
|
|
262
|
-
}
|
|
263
|
-
function isPlainObject(value) {
|
|
264
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
exports.createApiTranslationLoader = createApiTranslationLoader;
|
|
268
|
-
exports.preloadNamespaces = preloadNamespaces;
|
|
269
|
-
exports.warmFallbackLanguages = warmFallbackLanguages;
|
|
270
|
-
exports.withDefaultTranslations = withDefaultTranslations;
|
|
271
|
-
//# sourceMappingURL=index.js.map
|
|
272
|
-
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-loader.ts","../src/preload.ts","../src/defaults.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { ApiLoaderOptions, TranslationLoader, TranslationRecord } from './types';\n\ninterface CacheEntry {\n data: TranslationRecord;\n expiresAt: number;\n}\n\nconst FIVE_MINUTES = 5 * 60 * 1000;\n\nconst defaultFetcher = (input: RequestInfo | URL, init?: RequestInit) =>\n fetch(input, init);\n\n/**\n * 재시도 가능한 에러인지 확인\n */\nfunction isRetryableError(error: unknown): boolean {\n // 네트워크 에러 (TypeError)\n if (error instanceof TypeError) {\n return true;\n }\n\n // Fetch API 에러 메시지 확인\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n if (\n message.includes('failed to fetch') ||\n message.includes('networkerror') ||\n message.includes('network request failed')\n ) {\n return true;\n }\n }\n\n // Response 객체가 있는 경우 HTTP 상태 코드 확인\n if (error && typeof error === 'object' && 'status' in error) {\n const status = (error as { status: number }).status;\n // 5xx 서버 에러는 재시도 가능\n if (status >= 500 && status < 600) {\n return true;\n }\n // 408 Request Timeout도 재시도 가능\n if (status === 408) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * API 기반 번역 로더 생성\n * \n * @param options - 로더 옵션\n * @returns 번역 로더 및 캐시 무효화 함수\n * \n * @example\n * ```typescript\n * const { loader, invalidate, clear } = createApiTranslationLoader({\n * translationApiPath: '/api/translations',\n * autoInvalidateInDev: true\n * });\n * \n * // 특정 언어/네임스페이스 무효화\n * invalidate('ko', 'common');\n * \n * // 전체 캐시 클리어\n * clear();\n * ```\n */\nexport function createApiTranslationLoader(\n options: ApiLoaderOptions = {}\n): TranslationLoader & { invalidate: (language?: string, namespace?: string) => void; clear: () => void } {\n const translationApiPath = options.translationApiPath ?? '/api/translations';\n const cacheTtlMs = options.cacheTtlMs ?? FIVE_MINUTES;\n const fetcher = options.fetcher ?? defaultFetcher;\n const logger = options.logger ?? console;\n const retryCount = options.retryCount ?? 0;\n const retryDelay = options.retryDelay ?? 1000;\n const autoInvalidateInDev = options.autoInvalidateInDev ?? (typeof process !== 'undefined' && process.env.NODE_ENV === 'development');\n const localCache = new Map<string, CacheEntry>();\n const inFlightRequests = new Map<string, Promise<TranslationRecord>>();\n\n const buildUrl = (language: string, namespace: string) => {\n const safeNamespace = namespace.replace(/[^a-zA-Z0-9-_]/g, '');\n const path = `${translationApiPath}/${language}/${safeNamespace}`;\n\n if (typeof window !== 'undefined') {\n return path;\n }\n\n if (options.baseUrl) {\n return `${options.baseUrl}${path}`;\n }\n\n if (process.env.NEXT_PUBLIC_SITE_URL) {\n return `${process.env.NEXT_PUBLIC_SITE_URL}${path}`;\n }\n\n if (process.env.VERCEL_URL) {\n const vercelUrl = process.env.VERCEL_URL.startsWith('http')\n ? process.env.VERCEL_URL\n : `https://${process.env.VERCEL_URL}`;\n return `${vercelUrl}${path}`;\n }\n\n const fallbackBase = options.localFallbackBaseUrl ?? 'http://localhost:3000';\n return `${fallbackBase}${path}`;\n };\n\n const getRequestInit = (language: string, namespace: string): RequestInit => {\n if (typeof options.requestInit === 'function') {\n return options.requestInit(language, namespace) ?? {};\n }\n\n return options.requestInit ?? {};\n };\n\n const getCached = (cacheKey: string) => {\n if (options.disableCache) {\n return null;\n }\n\n const entry = localCache.get(cacheKey);\n if (!entry) {\n return null;\n }\n\n if (entry.expiresAt < Date.now()) {\n localCache.delete(cacheKey);\n return null;\n }\n\n return entry.data;\n };\n\n const setCached = (cacheKey: string, data: TranslationRecord) => {\n if (options.disableCache) {\n return;\n }\n\n localCache.set(cacheKey, {\n data,\n expiresAt: Date.now() + cacheTtlMs\n });\n };\n\n const loadTranslations: TranslationLoader = async (language, namespace) => {\n const cacheKey = `${language}:${namespace}`;\n const cached = getCached(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const inFlight = inFlightRequests.get(cacheKey);\n if (inFlight) {\n return inFlight;\n }\n\n const url = buildUrl(language, namespace);\n const requestInit = getRequestInit(language, namespace);\n\n const performRequest = async (attempt: number): Promise<TranslationRecord> => {\n try {\n const response = await fetcher(url, {\n cache: 'no-store',\n ...requestInit\n });\n\n if (!response.ok) {\n throw new Error(\n `[i18n-loaders] Failed to load ${language}/${namespace} (${response.status})`\n );\n }\n\n const data = (await response.json()) as TranslationRecord;\n setCached(cacheKey, data);\n return data;\n } catch (error) {\n // 재시도 가능한 에러인지 확인\n const isRetryable = isRetryableError(error);\n\n if (isRetryable && attempt < retryCount) {\n logger.warn?.(\n `[i18n-loaders] Translation fetch failed (attempt ${attempt + 1}/${retryCount + 1}), retrying...`,\n language,\n namespace,\n error\n );\n \n // 지수 백오프: 각 재시도마다 지연 시간 증가\n const delay = retryDelay * Math.pow(2, attempt);\n await new Promise(resolve => setTimeout(resolve, delay));\n \n return performRequest(attempt + 1);\n }\n\n // 재시도 불가능하거나 재시도 횟수 초과\n logger.warn?.(\n '[i18n-loaders] translation fetch failed',\n language,\n namespace,\n error\n );\n throw error;\n }\n };\n\n const requestPromise = performRequest(0)\n .finally(() => {\n inFlightRequests.delete(cacheKey);\n });\n\n inFlightRequests.set(cacheKey, requestPromise);\n return requestPromise;\n };\n\n /**\n * 특정 언어/네임스페이스의 캐시 무효화\n */\n const invalidate = (language?: string, namespace?: string) => {\n if (language && namespace) {\n // 특정 언어/네임스페이스만 무효화\n const cacheKey = `${language}:${namespace}`;\n localCache.delete(cacheKey);\n logger.log?.('[i18n-loaders] Cache invalidated:', cacheKey);\n } else if (language) {\n // 특정 언어의 모든 네임스페이스 무효화\n const prefix = `${language}:`;\n for (const key of localCache.keys()) {\n if (key.startsWith(prefix)) {\n localCache.delete(key);\n }\n }\n logger.log?.('[i18n-loaders] Cache invalidated for language:', language);\n } else if (namespace) {\n // 특정 네임스페이스의 모든 언어 무효화\n const suffix = `:${namespace}`;\n for (const key of localCache.keys()) {\n if (key.endsWith(suffix)) {\n localCache.delete(key);\n }\n }\n logger.log?.('[i18n-loaders] Cache invalidated for namespace:', namespace);\n } else {\n // 전체 캐시 무효화\n clear();\n }\n };\n\n /**\n * 전체 캐시 클리어\n */\n const clear = () => {\n localCache.clear();\n logger.log?.('[i18n-loaders] Cache cleared');\n };\n\n // 개발 모드에서 자동 무효화 설정\n if (autoInvalidateInDev && typeof window !== 'undefined') {\n // 개발 모드에서 페이지 포커스 시 캐시 무효화 (번역 파일 변경 감지)\n window.addEventListener('focus', () => {\n if (process.env.NODE_ENV === 'development') {\n logger.log?.('[i18n-loaders] Development mode: Auto-invalidating cache on focus');\n clear();\n }\n });\n }\n\n // loadTranslations 함수에 invalidate와 clear 메서드 추가\n const loader = loadTranslations as TranslationLoader & {\n invalidate: (language?: string, namespace?: string) => void;\n clear: () => void;\n };\n \n loader.invalidate = invalidate;\n loader.clear = clear;\n\n return loader;\n}\n\n","import { PreloadOptions, TranslationLoader } from './types';\n\nconst defaultLogger = console;\n\nexport async function preloadNamespaces(\n language: string,\n namespaces: string[],\n loader: TranslationLoader,\n options: PreloadOptions = {}\n) {\n const logger = options.logger ?? defaultLogger;\n\n const results = await Promise.allSettled(\n namespaces.map(async (namespace) => {\n await loader(language, namespace);\n return namespace;\n })\n );\n\n const fulfilled = results.filter(\n (result): result is PromiseFulfilledResult<string> =>\n result.status === 'fulfilled'\n );\n const rejected = results.filter(\n (result): result is PromiseRejectedResult => result.status === 'rejected'\n );\n\n if (fulfilled.length > 0) {\n logger.log?.(\n `[i18n-loaders] Preloaded ${fulfilled.length}/${namespaces.length} namespaces for ${language}`\n );\n }\n\n if (rejected.length > 0 && !options.suppressErrors) {\n logger.warn?.(\n `[i18n-loaders] Failed to preload ${rejected.length} namespaces for ${language}`\n );\n }\n\n return {\n fulfilled: fulfilled.map((result) => result.value),\n rejected: rejected.map((result) => result.reason)\n };\n}\n\nexport async function warmFallbackLanguages(\n currentLanguage: string,\n languages: string[],\n namespaces: string[],\n loader: TranslationLoader,\n options: PreloadOptions = {}\n) {\n const targets = languages.filter((language) => language !== currentLanguage);\n if (targets.length === 0) {\n return [];\n }\n\n return Promise.all(\n targets.map((language) =>\n preloadNamespaces(language, namespaces, loader, options)\n )\n );\n}\n\n","import {\n DefaultTranslations,\n TranslationLoader,\n TranslationRecord\n} from './types';\n\nexport function withDefaultTranslations(\n loader: TranslationLoader,\n defaults: DefaultTranslations\n): TranslationLoader {\n return async (language, namespace) => {\n const fallback = defaults[language]?.[namespace];\n\n try {\n const remote = await loader(language, namespace);\n \n // API 응답이 빈 객체이거나 null/undefined인 경우 fallback 반환\n if (!remote || (typeof remote === 'object' && Object.keys(remote).length === 0)) {\n if (fallback) {\n return fallback;\n }\n return remote || {};\n }\n \n if (!fallback) {\n return remote;\n }\n\n return mergeTranslations(fallback, remote);\n } catch (error) {\n if (fallback) {\n return fallback;\n }\n throw error;\n }\n };\n}\n\nfunction mergeTranslations(\n base: TranslationRecord,\n override: TranslationRecord\n): TranslationRecord {\n const result: TranslationRecord = { ...base };\n\n for (const [key, value] of Object.entries(override)) {\n if (isPlainObject(value) && isPlainObject(result[key])) {\n result[key] = mergeTranslations(\n result[key] as TranslationRecord,\n value as TranslationRecord\n );\n continue;\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\nfunction isPlainObject(value: unknown): value is TranslationRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n"]}
|