@fluojs/i18n 1.0.0-beta.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 +21 -0
- package/README.ko.md +537 -0
- package/README.md +537 -0
- package/dist/adapters.d.ts +180 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +266 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/http.d.ts +120 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +179 -0
- package/dist/icu.d.ts +59 -0
- package/dist/icu.d.ts.map +1 -0
- package/dist/icu.js +142 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/loaders/fs.d.ts +43 -0
- package/dist/loaders/fs.d.ts.map +1 -0
- package/dist/loaders/fs.js +79 -0
- package/dist/loaders/remote.d.ts +146 -0
- package/dist/loaders/remote.d.ts.map +1 -0
- package/dist/loaders/remote.js +268 -0
- package/dist/loaders/shared.d.ts +54 -0
- package/dist/loaders/shared.d.ts.map +1 -0
- package/dist/loaders/shared.js +89 -0
- package/dist/locale-resolution.d.ts +86 -0
- package/dist/locale-resolution.d.ts.map +1 -0
- package/dist/locale-resolution.js +201 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +60 -0
- package/dist/options.d.ts +9 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +169 -0
- package/dist/service.d.ts +104 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +348 -0
- package/dist/typegen.d.ts +60 -0
- package/dist/typegen.d.ts.map +1 -0
- package/dist/typegen.js +215 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +74 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +123 -0
- package/package.json +97 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { I18nError } from '../errors.js';
|
|
2
|
+
import { isPlainObject, snapshotLoaderMessageTree, validateLoaderLocale, validateLoaderNamespace } from './shared.js';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Request metadata passed to a remote catalog provider.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provider abstraction for remote JSON catalog backends such as HTTP APIs, object stores, or databases.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for provider-backed remote catalog loading.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cache key input for opt-in remote catalog caching helpers.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for wrapping a remote catalog loader with explicit in-memory caching.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Invalidation controls exposed by opt-in cached i18n loaders.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
function validateTimeout(timeoutMs) {
|
|
30
|
+
if (timeoutMs === undefined) {
|
|
31
|
+
return DEFAULT_TIMEOUT_MS;
|
|
32
|
+
}
|
|
33
|
+
if (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
34
|
+
throw new I18nError('Remote i18n loader timeoutMs must be a positive integer when provided.', 'I18N_INVALID_LOADER_OPTIONS');
|
|
35
|
+
}
|
|
36
|
+
return timeoutMs;
|
|
37
|
+
}
|
|
38
|
+
function validateCacheTtl(ttlMs) {
|
|
39
|
+
if (typeof ttlMs !== 'number' || !Number.isInteger(ttlMs) || ttlMs <= 0) {
|
|
40
|
+
throw new I18nError('Cached i18n loader ttlMs must be a positive integer.', 'I18N_INVALID_LOADER_OPTIONS');
|
|
41
|
+
}
|
|
42
|
+
return ttlMs;
|
|
43
|
+
}
|
|
44
|
+
function createDefaultCacheKey({
|
|
45
|
+
locale,
|
|
46
|
+
namespace,
|
|
47
|
+
version
|
|
48
|
+
}) {
|
|
49
|
+
return `${locale}\u0000${namespace}\u0000${version ?? ''}`;
|
|
50
|
+
}
|
|
51
|
+
function parseRemoteCatalog(value, locale, namespace) {
|
|
52
|
+
if (value === undefined || value === null) {
|
|
53
|
+
throw new I18nError(`Missing remote i18n catalog: ${locale}/${namespace}`, 'I18N_MISSING_CATALOG');
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
try {
|
|
57
|
+
return snapshotLoaderMessageTree(JSON.parse(value), `catalogs.${locale}.${namespace}`);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof I18nError) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
throw new I18nError(`Malformed remote i18n catalog JSON: ${locale}/${namespace}`, 'I18N_INVALID_CATALOG');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return snapshotLoaderMessageTree(value, `catalogs.${locale}.${namespace}`);
|
|
66
|
+
}
|
|
67
|
+
function throwIfAborted(signal) {
|
|
68
|
+
if (signal.aborted) {
|
|
69
|
+
throw new I18nError('Remote i18n catalog load was aborted.', 'I18N_LOADER_ABORTED');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function createTimeoutError() {
|
|
73
|
+
return new I18nError('Remote i18n catalog load timed out.', 'I18N_LOADER_TIMEOUT');
|
|
74
|
+
}
|
|
75
|
+
function createAbortError() {
|
|
76
|
+
return new I18nError('Remote i18n catalog load was aborted.', 'I18N_LOADER_ABORTED');
|
|
77
|
+
}
|
|
78
|
+
function createProviderError(error) {
|
|
79
|
+
if (error instanceof I18nError) {
|
|
80
|
+
return error;
|
|
81
|
+
}
|
|
82
|
+
const message = error instanceof Error ? error.message : 'Unknown provider failure';
|
|
83
|
+
return new I18nError(`Remote i18n catalog provider failed: ${message}`, 'I18N_LOADER_FAILED');
|
|
84
|
+
}
|
|
85
|
+
function linkCallerAbort(callerSignal, controller) {
|
|
86
|
+
if (callerSignal === undefined) {
|
|
87
|
+
return () => undefined;
|
|
88
|
+
}
|
|
89
|
+
if (callerSignal.aborted) {
|
|
90
|
+
controller.abort(createAbortError());
|
|
91
|
+
return () => undefined;
|
|
92
|
+
}
|
|
93
|
+
const abort = () => controller.abort(createAbortError());
|
|
94
|
+
callerSignal.addEventListener('abort', abort, {
|
|
95
|
+
once: true
|
|
96
|
+
});
|
|
97
|
+
return () => callerSignal.removeEventListener('abort', abort);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Provider-backed remote JSON catalog loader for `@fluojs/i18n/loaders/remote`.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* The loader validates locale and namespace before calling the provider, propagates cancellation through an
|
|
105
|
+
* `AbortSignal`, enforces a per-load timeout, parses JSON strings, validates message tree shape, and always
|
|
106
|
+
* returns a detached immutable catalog snapshot.
|
|
107
|
+
*/
|
|
108
|
+
export class RemoteI18nLoader {
|
|
109
|
+
provider;
|
|
110
|
+
timeoutMs;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a provider-backed remote catalog loader.
|
|
114
|
+
*
|
|
115
|
+
* @param options Remote loader options with a provider and optional timeout.
|
|
116
|
+
*/
|
|
117
|
+
constructor(options) {
|
|
118
|
+
if (!isPlainObject(options) || typeof options.provider !== 'function') {
|
|
119
|
+
throw new I18nError('Remote i18n loader provider must be a function.', 'I18N_INVALID_LOADER_OPTIONS');
|
|
120
|
+
}
|
|
121
|
+
this.provider = options.provider;
|
|
122
|
+
this.timeoutMs = validateTimeout(options.timeoutMs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Loads and validates one remote message catalog through the configured provider.
|
|
127
|
+
*
|
|
128
|
+
* @param locale Locale identifier passed to the provider.
|
|
129
|
+
* @param namespace Namespace identifier passed to the provider.
|
|
130
|
+
* @param options Optional per-load cancellation controls.
|
|
131
|
+
* @returns A detached immutable i18n message tree.
|
|
132
|
+
* @throws {I18nError} When inputs are unsafe, the provider misses/fails, loading times out, JSON is malformed, or catalog shape is invalid.
|
|
133
|
+
*/
|
|
134
|
+
async load(locale, namespace, options = {}) {
|
|
135
|
+
validateLoaderLocale(locale, 'Remote i18n');
|
|
136
|
+
validateLoaderNamespace(namespace, 'Remote i18n');
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const unlinkCallerAbort = linkCallerAbort(options.signal, controller);
|
|
139
|
+
const timeout = setTimeout(() => controller.abort(createTimeoutError()), this.timeoutMs);
|
|
140
|
+
const abortRace = new Promise((_resolve, reject) => {
|
|
141
|
+
controller.signal.addEventListener('abort', () => reject(controller.signal.reason instanceof I18nError ? controller.signal.reason : createAbortError()), {
|
|
142
|
+
once: true
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
throwIfAborted(controller.signal);
|
|
147
|
+
const rawCatalog = await Promise.race([Promise.resolve(this.provider({
|
|
148
|
+
locale,
|
|
149
|
+
namespace,
|
|
150
|
+
signal: controller.signal
|
|
151
|
+
})), abortRace]);
|
|
152
|
+
throwIfAborted(controller.signal);
|
|
153
|
+
return parseRemoteCatalog(rawCatalog, locale, namespace);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (controller.signal.aborted && controller.signal.reason instanceof I18nError) {
|
|
156
|
+
throw controller.signal.reason;
|
|
157
|
+
}
|
|
158
|
+
throw createProviderError(error);
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
unlinkCallerAbort();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Opt-in in-memory caching wrapper for remote i18n catalog loaders.
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* This wrapper never changes `RemoteI18nLoader` defaults. Applications choose it explicitly when they want catalog
|
|
171
|
+
* caching at the loading boundary and can invalidate entries through `invalidate(...)` or `clear()`.
|
|
172
|
+
*/
|
|
173
|
+
export class CachedRemoteI18nLoader {
|
|
174
|
+
cache = new Map();
|
|
175
|
+
getCacheKey;
|
|
176
|
+
loader;
|
|
177
|
+
now;
|
|
178
|
+
ttlMs;
|
|
179
|
+
version;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Creates an explicit cache wrapper around a remote catalog loader.
|
|
183
|
+
*
|
|
184
|
+
* @param options Loader, TTL, version, key, and clock options for the cache wrapper.
|
|
185
|
+
*/
|
|
186
|
+
constructor(options) {
|
|
187
|
+
if (!isPlainObject(options) || typeof options.loader !== 'object' || options.loader === null || typeof options.loader.load !== 'function') {
|
|
188
|
+
throw new I18nError('Cached i18n loader requires a loader with a load function.', 'I18N_INVALID_LOADER_OPTIONS');
|
|
189
|
+
}
|
|
190
|
+
this.loader = options.loader;
|
|
191
|
+
this.ttlMs = validateCacheTtl(options.ttlMs);
|
|
192
|
+
this.version = options.version;
|
|
193
|
+
this.getCacheKey = options.getCacheKey ?? createDefaultCacheKey;
|
|
194
|
+
this.now = options.now ?? Date.now;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Loads a catalog through the wrapped loader and caches successful results until the configured TTL expires.
|
|
199
|
+
*
|
|
200
|
+
* @param locale Locale identifier passed to the wrapped loader.
|
|
201
|
+
* @param namespace Namespace identifier passed to the wrapped loader.
|
|
202
|
+
* @param options Optional per-load cancellation controls for cache misses.
|
|
203
|
+
* @returns A cached or freshly loaded immutable i18n message tree.
|
|
204
|
+
*/
|
|
205
|
+
async load(locale, namespace, options = {}) {
|
|
206
|
+
validateLoaderLocale(locale, 'Cached remote i18n');
|
|
207
|
+
validateLoaderNamespace(namespace, 'Cached remote i18n');
|
|
208
|
+
const cacheKey = this.getCacheKey({
|
|
209
|
+
locale,
|
|
210
|
+
namespace,
|
|
211
|
+
version: this.version
|
|
212
|
+
});
|
|
213
|
+
const cached = this.cache.get(cacheKey);
|
|
214
|
+
const currentTime = this.now();
|
|
215
|
+
if (cached !== undefined && cached.expiresAt > currentTime) {
|
|
216
|
+
return cached.catalog;
|
|
217
|
+
}
|
|
218
|
+
const catalog = await this.loader.load(locale, namespace, options);
|
|
219
|
+
this.cache.set(cacheKey, {
|
|
220
|
+
catalog,
|
|
221
|
+
expiresAt: currentTime + this.ttlMs
|
|
222
|
+
});
|
|
223
|
+
return catalog;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Invalidates one cached catalog entry using the same key policy as `load(...)`.
|
|
228
|
+
*
|
|
229
|
+
* @param locale Locale identifier for the cache entry.
|
|
230
|
+
* @param namespace Namespace identifier for the cache entry.
|
|
231
|
+
*/
|
|
232
|
+
invalidate(locale, namespace) {
|
|
233
|
+
validateLoaderLocale(locale, 'Cached remote i18n');
|
|
234
|
+
validateLoaderNamespace(namespace, 'Cached remote i18n');
|
|
235
|
+
this.cache.delete(this.getCacheKey({
|
|
236
|
+
locale,
|
|
237
|
+
namespace,
|
|
238
|
+
version: this.version
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Clears every cache entry owned by this wrapper.
|
|
244
|
+
*/
|
|
245
|
+
clear() {
|
|
246
|
+
this.cache.clear();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Creates a provider-backed remote JSON catalog loader.
|
|
252
|
+
*
|
|
253
|
+
* @param options Remote loader options with a provider and optional timeout.
|
|
254
|
+
* @returns A remote i18n loader instance.
|
|
255
|
+
*/
|
|
256
|
+
export function createRemoteI18nLoader(options) {
|
|
257
|
+
return new RemoteI18nLoader(options);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Creates an opt-in cached remote catalog loader wrapper.
|
|
262
|
+
*
|
|
263
|
+
* @param options Loader, TTL, version, key, and clock options for the cache wrapper.
|
|
264
|
+
* @returns A cached loader wrapper with explicit invalidation controls.
|
|
265
|
+
*/
|
|
266
|
+
export function createCachedRemoteI18nLoader(options) {
|
|
267
|
+
return new CachedRemoteI18nLoader(options);
|
|
268
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { I18nLocale, I18nMessageTree, I18nTranslationKey } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Optional per-load controls shared by i18n catalog loaders.
|
|
4
|
+
*/
|
|
5
|
+
export interface I18nLoaderLoadOptions {
|
|
6
|
+
/** Optional cancellation signal for loaders that can cancel in-flight work. */
|
|
7
|
+
readonly signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Loader contract for asynchronous locale and namespace catalog sources.
|
|
11
|
+
*/
|
|
12
|
+
export interface I18nLoader {
|
|
13
|
+
/**
|
|
14
|
+
* Loads one locale and namespace catalog tree.
|
|
15
|
+
*
|
|
16
|
+
* @param locale Locale identifier to load.
|
|
17
|
+
* @param namespace Namespace identifier to load.
|
|
18
|
+
* @param options Optional per-load controls such as cancellation.
|
|
19
|
+
* @returns A detached immutable i18n message tree.
|
|
20
|
+
*/
|
|
21
|
+
load(locale: I18nLocale, namespace: I18nTranslationKey, options?: I18nLoaderLoadOptions): Promise<I18nMessageTree>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Checks whether a value is a plain object suitable for loader option or catalog validation.
|
|
25
|
+
*
|
|
26
|
+
* @param value Candidate value to inspect.
|
|
27
|
+
* @returns `true` when the value is a plain object or null-prototype object.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Validates a loader locale before a backend is called.
|
|
32
|
+
*
|
|
33
|
+
* @param locale Candidate locale value supplied by the loader caller.
|
|
34
|
+
* @param label Loader label used in stable error messages.
|
|
35
|
+
* @returns Nothing when the locale is valid.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateLoaderLocale(locale: unknown, label: string): asserts locale is I18nLocale;
|
|
38
|
+
/**
|
|
39
|
+
* Validates a loader namespace before a backend is called.
|
|
40
|
+
*
|
|
41
|
+
* @param namespace Candidate namespace value supplied by the loader caller.
|
|
42
|
+
* @param label Loader label used in stable error messages.
|
|
43
|
+
* @returns Nothing when the namespace is valid.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateLoaderNamespace(namespace: unknown, label: string): asserts namespace is I18nTranslationKey;
|
|
46
|
+
/**
|
|
47
|
+
* Creates a detached immutable message tree from untrusted loader output.
|
|
48
|
+
*
|
|
49
|
+
* @param value Untrusted catalog-like value returned by a loader backend.
|
|
50
|
+
* @param path Diagnostic path used in validation errors.
|
|
51
|
+
* @returns A frozen message tree detached from caller-owned data.
|
|
52
|
+
*/
|
|
53
|
+
export declare function snapshotLoaderMessageTree(value: unknown, path: string): I18nMessageTree;
|
|
54
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/loaders/shared.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKnF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;;OAOG;IACH,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACpH;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO9E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,CAIjG;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAelH;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CA0BvF"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { I18nError } from '../errors.js';
|
|
2
|
+
const SAFE_LOCALE_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?(?:-[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?)*$/;
|
|
3
|
+
const SAFE_NAMESPACE_SEGMENT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?$/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Optional per-load controls shared by i18n catalog loaders.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loader contract for asynchronous locale and namespace catalog sources.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks whether a value is a plain object suitable for loader option or catalog validation.
|
|
15
|
+
*
|
|
16
|
+
* @param value Candidate value to inspect.
|
|
17
|
+
* @returns `true` when the value is a plain object or null-prototype object.
|
|
18
|
+
*/
|
|
19
|
+
export function isPlainObject(value) {
|
|
20
|
+
if (typeof value !== 'object' || value === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const prototype = Object.getPrototypeOf(value);
|
|
24
|
+
return prototype === Object.prototype || prototype === null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates a loader locale before a backend is called.
|
|
29
|
+
*
|
|
30
|
+
* @param locale Candidate locale value supplied by the loader caller.
|
|
31
|
+
* @param label Loader label used in stable error messages.
|
|
32
|
+
* @returns Nothing when the locale is valid.
|
|
33
|
+
*/
|
|
34
|
+
export function validateLoaderLocale(locale, label) {
|
|
35
|
+
if (typeof locale !== 'string' || locale.trim() === '' || !SAFE_LOCALE_PATTERN.test(locale)) {
|
|
36
|
+
throw new I18nError(`${label} locale must be a safe non-empty locale segment.`, 'I18N_INVALID_LOADER_OPTIONS');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates a loader namespace before a backend is called.
|
|
42
|
+
*
|
|
43
|
+
* @param namespace Candidate namespace value supplied by the loader caller.
|
|
44
|
+
* @param label Loader label used in stable error messages.
|
|
45
|
+
* @returns Nothing when the namespace is valid.
|
|
46
|
+
*/
|
|
47
|
+
export function validateLoaderNamespace(namespace, label) {
|
|
48
|
+
if (typeof namespace !== 'string' || namespace.trim() === '') {
|
|
49
|
+
throw new I18nError(`${label} namespace must be a safe non-empty namespace path.`, 'I18N_INVALID_LOADER_OPTIONS');
|
|
50
|
+
}
|
|
51
|
+
const normalized = namespace.replaceAll('\\', '/');
|
|
52
|
+
if (normalized.startsWith('/') || normalized.endsWith('/') || normalized.includes('//')) {
|
|
53
|
+
throw new I18nError(`${label} namespace must be a relative namespace path.`, 'I18N_INVALID_LOADER_OPTIONS');
|
|
54
|
+
}
|
|
55
|
+
for (const segment of normalized.split('/')) {
|
|
56
|
+
if (segment === '.' || segment === '..' || !SAFE_NAMESPACE_SEGMENT_PATTERN.test(segment)) {
|
|
57
|
+
throw new I18nError(`${label} namespace contains an unsafe path segment.`, 'I18N_INVALID_LOADER_OPTIONS');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a detached immutable message tree from untrusted loader output.
|
|
64
|
+
*
|
|
65
|
+
* @param value Untrusted catalog-like value returned by a loader backend.
|
|
66
|
+
* @param path Diagnostic path used in validation errors.
|
|
67
|
+
* @returns A frozen message tree detached from caller-owned data.
|
|
68
|
+
*/
|
|
69
|
+
export function snapshotLoaderMessageTree(value, path) {
|
|
70
|
+
if (!isPlainObject(value)) {
|
|
71
|
+
throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
|
|
72
|
+
}
|
|
73
|
+
const snapshot = {};
|
|
74
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
75
|
+
if (key.trim() === '') {
|
|
76
|
+
throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
|
|
77
|
+
}
|
|
78
|
+
if (typeof entry === 'string') {
|
|
79
|
+
snapshot[key] = entry;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (isPlainObject(entry)) {
|
|
83
|
+
snapshot[key] = snapshotLoaderMessageTree(entry, `${path}.${key}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze(snapshot);
|
|
89
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { I18nLocale } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalized locale candidate returned by HTTP or non-HTTP locale resolvers.
|
|
4
|
+
*/
|
|
5
|
+
export interface LocaleResolverCandidate {
|
|
6
|
+
readonly locale: unknown;
|
|
7
|
+
readonly source?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parsed `Accept-Language` preference with a validated locale range and q-value.
|
|
11
|
+
*/
|
|
12
|
+
export interface AcceptLanguagePreferenceInternal {
|
|
13
|
+
readonly locale: I18nLocale;
|
|
14
|
+
readonly quality: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Input passed to wildcard locale policy callbacks.
|
|
18
|
+
*/
|
|
19
|
+
export interface WildcardLocalePolicyInput {
|
|
20
|
+
/** Default locale configured for the resolver chain. */
|
|
21
|
+
readonly defaultLocale: I18nLocale;
|
|
22
|
+
/** Supported locale allow-list supplied by the caller. */
|
|
23
|
+
readonly supportedLocales?: readonly I18nLocale[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Opt-in policy for resolving an `Accept-Language: *` fallback.
|
|
27
|
+
*/
|
|
28
|
+
export type WildcardLocalePolicy = 'defaultLocale' | 'firstSupportedLocale' | ((input: WildcardLocalePolicyInput) => I18nLocale | undefined);
|
|
29
|
+
/**
|
|
30
|
+
* Options for locale policy helpers that extend `Accept-Language` matching without changing defaults.
|
|
31
|
+
*/
|
|
32
|
+
export interface AcceptLanguageLocalePolicyOptions {
|
|
33
|
+
/** Optional wildcard fallback policy. Omit this to keep `*` fallback-only and non-selecting. */
|
|
34
|
+
readonly wildcardLocale?: WildcardLocalePolicy;
|
|
35
|
+
/** Whether regional ranges such as `en-US` may normalize to supported base locales such as `en`. Defaults to `true`. */
|
|
36
|
+
readonly normalizeToSupportedLocale?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether a value is a syntactically valid locale identifier for resolver input.
|
|
40
|
+
*
|
|
41
|
+
* @param locale Value to validate as a locale identifier.
|
|
42
|
+
* @returns Whether the value is a string matching the locale grammar accepted by i18n resolvers.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isValidLocale(locale: unknown): locale is I18nLocale;
|
|
45
|
+
/**
|
|
46
|
+
* Checks whether a valid locale is allowed by an optional supported-locale list.
|
|
47
|
+
*
|
|
48
|
+
* @param locale Locale identifier to check.
|
|
49
|
+
* @param supportedLocales Optional list of supported locale identifiers.
|
|
50
|
+
* @returns Whether the locale is supported, or `true` when no supported-locale list is configured.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isSupportedLocale(locale: I18nLocale, supportedLocales: readonly I18nLocale[] | undefined): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Normalizes a valid locale candidate to the configured supported locale surface.
|
|
55
|
+
*
|
|
56
|
+
* @param locale Locale candidate from an explicit resolver or language range.
|
|
57
|
+
* @param supportedLocales Optional supported locale allow-list.
|
|
58
|
+
* @param allowBaseLocaleMatch Whether `en-US` may resolve to supported `en`.
|
|
59
|
+
* @returns The supported locale spelling selected by the caller, or `undefined` when unsupported.
|
|
60
|
+
*/
|
|
61
|
+
export declare function normalizeSupportedLocale(locale: I18nLocale, supportedLocales: readonly I18nLocale[] | undefined, allowBaseLocaleMatch?: boolean): I18nLocale | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Normalizes resolver output into a locale candidate object when the output shape is supported.
|
|
64
|
+
*
|
|
65
|
+
* @param result Resolver output to normalize.
|
|
66
|
+
* @returns A locale candidate, or `undefined` when the resolver output should be ignored.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeLocaleResolverResult(result: unknown): LocaleResolverCandidate | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Parses one or more `Accept-Language` header values into sorted locale preferences.
|
|
71
|
+
*
|
|
72
|
+
* @param header Header value or values to parse.
|
|
73
|
+
* @returns Valid preferences sorted by descending q-value and original header order.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseLocalePreferences(header: string | readonly string[] | undefined): readonly AcceptLanguagePreferenceInternal[];
|
|
76
|
+
/**
|
|
77
|
+
* Selects a locale from parsed `Accept-Language` preferences using opt-in policy rules.
|
|
78
|
+
*
|
|
79
|
+
* @param preferences Parsed header preferences ordered by q-value.
|
|
80
|
+
* @param defaultLocale Default locale configured for resolver fallback.
|
|
81
|
+
* @param supportedLocales Optional supported locale allow-list.
|
|
82
|
+
* @param options Wildcard and normalization policy options.
|
|
83
|
+
* @returns The selected supported locale, or `undefined` when no policy-selected locale matches.
|
|
84
|
+
*/
|
|
85
|
+
export declare function selectLocaleFromAcceptLanguagePolicy(preferences: readonly AcceptLanguagePreferenceInternal[], defaultLocale: I18nLocale, supportedLocales: readonly I18nLocale[] | undefined, options?: AcceptLanguageLocalePolicyOptions): I18nLocale | undefined;
|
|
86
|
+
//# sourceMappingURL=locale-resolution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-resolution.d.ts","sourceRoot":"","sources":["../src/locale-resolution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,wDAAwD;IACxD,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,sBAAsB,GACtB,CAAC,CAAC,KAAK,EAAE,yBAAyB,KAAK,UAAU,GAAG,SAAS,CAAC,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,gGAAgG;IAChG,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,wHAAwH;IACxH,QAAQ,CAAC,0BAA0B,CAAC,EAAE,OAAO,CAAC;CAC/C;AAKD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,UAAU,CAEnE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,EAAE,gBAAgB,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,GAAG,OAAO,CAElH;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,UAAU,EAClB,gBAAgB,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,EACnD,oBAAoB,UAAO,GAC1B,UAAU,GAAG,SAAS,CA4BxB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAoBlG;AA2BD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,gCAAgC,EAAE,CA2BlI;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,oCAAoC,CAClD,WAAW,EAAE,SAAS,gCAAgC,EAAE,EACxD,aAAa,EAAE,UAAU,EACzB,gBAAgB,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,EACnD,OAAO,GAAE,iCAAsC,GAC9C,UAAU,GAAG,SAAS,CA4BxB"}
|