@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +537 -0
  3. package/README.md +537 -0
  4. package/dist/adapters.d.ts +180 -0
  5. package/dist/adapters.d.ts.map +1 -0
  6. package/dist/adapters.js +266 -0
  7. package/dist/errors.d.ts +17 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +19 -0
  10. package/dist/http.d.ts +120 -0
  11. package/dist/http.d.ts.map +1 -0
  12. package/dist/http.js +179 -0
  13. package/dist/icu.d.ts +59 -0
  14. package/dist/icu.d.ts.map +1 -0
  15. package/dist/icu.js +142 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +3 -0
  19. package/dist/loaders/fs.d.ts +43 -0
  20. package/dist/loaders/fs.d.ts.map +1 -0
  21. package/dist/loaders/fs.js +79 -0
  22. package/dist/loaders/remote.d.ts +146 -0
  23. package/dist/loaders/remote.d.ts.map +1 -0
  24. package/dist/loaders/remote.js +268 -0
  25. package/dist/loaders/shared.d.ts +54 -0
  26. package/dist/loaders/shared.d.ts.map +1 -0
  27. package/dist/loaders/shared.js +89 -0
  28. package/dist/locale-resolution.d.ts +86 -0
  29. package/dist/locale-resolution.d.ts.map +1 -0
  30. package/dist/locale-resolution.js +201 -0
  31. package/dist/module.d.ts +22 -0
  32. package/dist/module.d.ts.map +1 -0
  33. package/dist/module.js +60 -0
  34. package/dist/options.d.ts +9 -0
  35. package/dist/options.d.ts.map +1 -0
  36. package/dist/options.js +169 -0
  37. package/dist/service.d.ts +104 -0
  38. package/dist/service.d.ts.map +1 -0
  39. package/dist/service.js +348 -0
  40. package/dist/typegen.d.ts +60 -0
  41. package/dist/typegen.d.ts.map +1 -0
  42. package/dist/typegen.js +215 -0
  43. package/dist/types.d.ts +154 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +1 -0
  46. package/dist/validation.d.ts +74 -0
  47. package/dist/validation.d.ts.map +1 -0
  48. package/dist/validation.js +123 -0
  49. package/package.json +97 -0
package/dist/http.js ADDED
@@ -0,0 +1,179 @@
1
+ import { createContextKey, getContextValue, setContextValue } from '@fluojs/http';
2
+ import { isSupportedLocale, isValidLocale, normalizeLocaleResolverResult, parseLocalePreferences, selectLocaleFromAcceptLanguagePolicy } from './locale-resolution.js';
3
+
4
+ /**
5
+ * Locale metadata stored on a fluo HTTP request context.
6
+ */
7
+
8
+ /**
9
+ * Parsed `Accept-Language` preference ordered by caller priority.
10
+ */
11
+
12
+ /**
13
+ * Input shared by explicit HTTP locale resolvers.
14
+ */
15
+
16
+ /**
17
+ * Result returned by one HTTP locale resolver.
18
+ */
19
+
20
+ /**
21
+ * Explicit locale resolver used by `resolveHttpLocale(...)` in application-defined order.
22
+ */
23
+
24
+ /**
25
+ * Options for resolving a request locale from an ordered resolver chain.
26
+ */
27
+
28
+ /**
29
+ * Options for creating an `Accept-Language` resolver.
30
+ */
31
+
32
+ /**
33
+ * Options for the opt-in `Accept-Language` policy resolver.
34
+ */
35
+
36
+ /**
37
+ * Request-context key used by `setHttpLocale(...)` and `getHttpLocale(...)`.
38
+ */
39
+ export const HTTP_LOCALE_CONTEXT_KEY = createContextKey('fluo.i18n.http.locale');
40
+ const DEFAULT_ACCEPT_LANGUAGE_SOURCE = 'accept-language';
41
+ function readHeader(request, headerName) {
42
+ const normalizedName = headerName.toLowerCase();
43
+ for (const [name, value] of Object.entries(request.headers)) {
44
+ if (name.toLowerCase() === normalizedName) {
45
+ return value;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * Stores locale metadata on a fluo HTTP request context.
53
+ *
54
+ * @param context Request context to update.
55
+ * @param locale Locale selected for the request.
56
+ * @param metadata Additional locale metadata such as a resolver source.
57
+ */
58
+ export function setHttpLocale(context, locale, metadata = {}) {
59
+ setContextValue(context, HTTP_LOCALE_CONTEXT_KEY, Object.freeze({
60
+ ...metadata,
61
+ locale
62
+ }));
63
+ }
64
+
65
+ /**
66
+ * Reads locale metadata from a fluo HTTP request context.
67
+ *
68
+ * @param context Request context to inspect.
69
+ * @returns Stored locale metadata, or `undefined` when the request has no locale.
70
+ */
71
+ export function getHttpLocale(context) {
72
+ return getContextValue(context, HTTP_LOCALE_CONTEXT_KEY);
73
+ }
74
+
75
+ /**
76
+ * Parses an `Accept-Language` header into quality-sorted locale preferences.
77
+ *
78
+ * @param header Raw header value or adapter-provided repeated header values.
79
+ * @returns Valid language ranges ordered by descending q-value and original header order for ties.
80
+ */
81
+ export function parseAcceptLanguage(header) {
82
+ return parseLocalePreferences(header);
83
+ }
84
+
85
+ /**
86
+ * Creates a resolver that selects the first supported `Accept-Language` locale.
87
+ *
88
+ * @param options Header name and source label options.
89
+ * @returns Locale resolver that inspects the current request headers.
90
+ */
91
+ export function createAcceptLanguageLocaleResolver(options = {}) {
92
+ const headerName = options.headerName ?? 'accept-language';
93
+ const source = options.source ?? DEFAULT_ACCEPT_LANGUAGE_SOURCE;
94
+ return ({
95
+ context,
96
+ supportedLocales
97
+ }) => {
98
+ const header = readHeader(context.request, headerName);
99
+ const preferences = parseAcceptLanguage(header);
100
+ for (const preference of preferences) {
101
+ if (preference.locale === '*') {
102
+ continue;
103
+ }
104
+ if (isSupportedLocale(preference.locale, supportedLocales)) {
105
+ return {
106
+ locale: preference.locale,
107
+ source
108
+ };
109
+ }
110
+ }
111
+ return undefined;
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Creates an opt-in resolver that normalizes supported `Accept-Language` ranges and can select a wildcard fallback.
117
+ *
118
+ * @param options Header, source, normalization, and wildcard policy options.
119
+ * @returns Locale resolver that treats `*` as fallback-only and preserves explicit locale preferences first.
120
+ */
121
+ export function createAcceptLanguageLocalePolicyResolver(options = {}) {
122
+ const headerName = options.headerName ?? 'accept-language';
123
+ const source = options.source ?? DEFAULT_ACCEPT_LANGUAGE_SOURCE;
124
+ return ({
125
+ context,
126
+ defaultLocale,
127
+ supportedLocales
128
+ }) => {
129
+ const locale = selectLocaleFromAcceptLanguagePolicy(parseAcceptLanguage(readHeader(context.request, headerName)), defaultLocale, supportedLocales, options);
130
+ if (locale === undefined) {
131
+ return undefined;
132
+ }
133
+ return {
134
+ locale,
135
+ source
136
+ };
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Runs an explicit resolver chain and stores the selected request locale.
142
+ *
143
+ * @param context Request context to resolve and update.
144
+ * @param options Default locale, supported locales, and ordered resolvers.
145
+ * @returns Stored locale metadata selected by the first valid resolver or the configured default locale.
146
+ * @throws {TypeError} When the configured default locale is invalid or unsupported.
147
+ */
148
+ export function resolveHttpLocale(context, options) {
149
+ if (!isValidLocale(options.defaultLocale)) {
150
+ throw new TypeError('defaultLocale must be a syntactically valid locale string.');
151
+ }
152
+ if (!isSupportedLocale(options.defaultLocale, options.supportedLocales)) {
153
+ throw new TypeError('defaultLocale must be listed in supportedLocales when supportedLocales is provided.');
154
+ }
155
+ for (const resolver of options.resolvers ?? []) {
156
+ const result = normalizeLocaleResolverResult(resolver({
157
+ context,
158
+ defaultLocale: options.defaultLocale,
159
+ supportedLocales: options.supportedLocales
160
+ }));
161
+ if (result === undefined || !isValidLocale(result.locale) || !isSupportedLocale(result.locale, options.supportedLocales)) {
162
+ continue;
163
+ }
164
+ setHttpLocale(context, result.locale, {
165
+ source: result.source
166
+ });
167
+ return getHttpLocale(context) ?? {
168
+ locale: result.locale,
169
+ source: result.source
170
+ };
171
+ }
172
+ setHttpLocale(context, options.defaultLocale, {
173
+ source: 'default'
174
+ });
175
+ return getHttpLocale(context) ?? {
176
+ locale: options.defaultLocale,
177
+ source: 'default'
178
+ };
179
+ }
package/dist/icu.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { Formats } from 'intl-messageformat';
2
+ import { I18nService } from './service.js';
3
+ import type { I18nModuleOptions, I18nTranslateOptions } from './types.js';
4
+ /**
5
+ * Primitive values accepted by the ICU MessageFormat subpath.
6
+ */
7
+ export type I18nIcuValue = string | number | bigint | boolean | null | undefined | Date;
8
+ /**
9
+ * Named values available to ICU MessageFormat placeholders.
10
+ */
11
+ export type I18nIcuValues = Readonly<Record<string, I18nIcuValue>>;
12
+ /**
13
+ * Per-call options for ICU MessageFormat translation.
14
+ */
15
+ export interface I18nIcuTranslateOptions extends Omit<I18nTranslateOptions, 'values'> {
16
+ /** Values passed to ICU MessageFormat placeholders after core catalog and fallback resolution. */
17
+ readonly values?: I18nIcuValues;
18
+ /** Optional per-call Intl MessageFormat named format overrides. */
19
+ readonly formats?: Partial<Formats>;
20
+ }
21
+ /**
22
+ * ICU MessageFormat translation service layered on top of the framework-agnostic core `I18nService`.
23
+ *
24
+ * @remarks
25
+ * The root service keeps its simple `{{ name }}` interpolation and fallback behavior. This subpath resolves the
26
+ * message through that service first, then formats the resolved message with ICU MessageFormat plural/select rules.
27
+ */
28
+ export declare class IcuI18nService {
29
+ private readonly service;
30
+ /**
31
+ * Creates an ICU MessageFormat service from root options or an existing core service.
32
+ *
33
+ * @param options Root i18n options or an existing `I18nService` instance.
34
+ */
35
+ constructor(options?: I18nModuleOptions | I18nService);
36
+ /**
37
+ * Returns the underlying core i18n service used for catalog lookup and fallback resolution.
38
+ *
39
+ * @returns The core `I18nService` instance backing this ICU formatter.
40
+ */
41
+ getCoreService(): I18nService;
42
+ /**
43
+ * Resolves a message with the core service and formats it with ICU MessageFormat.
44
+ *
45
+ * @param key Dot-path catalog key, optionally prefixed by `options.namespace`.
46
+ * @param options Per-call locale, ICU values, default value, and optional named format overrides.
47
+ * @returns The resolved and ICU-formatted message.
48
+ * @throws {I18nError} When the core lookup fails or the ICU pattern/values are invalid.
49
+ */
50
+ translate(key: string, options: I18nIcuTranslateOptions): string;
51
+ }
52
+ /**
53
+ * Creates a standalone ICU MessageFormat i18n service.
54
+ *
55
+ * @param options Root i18n options or an existing `I18nService` instance.
56
+ * @returns An `IcuI18nService` that preserves core lookup semantics before ICU formatting.
57
+ */
58
+ export declare function createIcuI18n(options?: I18nModuleOptions | I18nService): IcuI18nService;
59
+ //# sourceMappingURL=icu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icu.d.ts","sourceRoot":"","sources":["../src/icu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAwD,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEhI;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;AAExF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC;IACnF,kGAAkG;IAClG,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAgFD;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IAEtC;;;;OAIG;gBACS,OAAO,GAAE,iBAAiB,GAAG,WAAgB;IAIzD;;;;OAIG;IACH,cAAc,IAAI,WAAW;IAI7B;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;CAsBjE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAiB,GAAG,WAAgB,GAAG,cAAc,CAE3F"}
package/dist/icu.js ADDED
@@ -0,0 +1,142 @@
1
+ import { FormatError, IntlMessageFormat } from 'intl-messageformat';
2
+ import { I18nError } from './errors.js';
3
+ import { I18nService, createI18n } from './service.js';
4
+
5
+ /**
6
+ * Primitive values accepted by the ICU MessageFormat subpath.
7
+ */
8
+
9
+ /**
10
+ * Named values available to ICU MessageFormat placeholders.
11
+ */
12
+
13
+ /**
14
+ * Per-call options for ICU MessageFormat translation.
15
+ */
16
+
17
+ function isCoreInterpolationValue(value) {
18
+ return value === null || value === undefined || ['string', 'number', 'boolean'].includes(typeof value);
19
+ }
20
+ function toCoreInterpolationValues(values) {
21
+ if (values === undefined) {
22
+ return undefined;
23
+ }
24
+ const interpolationValues = {};
25
+ let hasInterpolationValue = false;
26
+ for (const [key, value] of Object.entries(values)) {
27
+ if (isCoreInterpolationValue(value)) {
28
+ interpolationValues[key] = value;
29
+ hasInterpolationValue = true;
30
+ }
31
+ }
32
+ return hasInterpolationValue ? interpolationValues : undefined;
33
+ }
34
+ function toMessageFormatValues(values) {
35
+ if (values === undefined) {
36
+ return undefined;
37
+ }
38
+ return Object.fromEntries(Object.entries(values));
39
+ }
40
+ function hasOwn(value, key) {
41
+ return typeof value === 'object' && value !== null && Object.hasOwn(value, key);
42
+ }
43
+ function resolveMessage(tree, key) {
44
+ if (tree === undefined) {
45
+ return undefined;
46
+ }
47
+ if (hasOwn(tree, key)) {
48
+ const direct = tree[key];
49
+ return typeof direct === 'string' ? direct : undefined;
50
+ }
51
+ let current = tree;
52
+ for (const part of key.split('.')) {
53
+ if (!hasOwn(current, part)) {
54
+ return undefined;
55
+ }
56
+ current = current[part];
57
+ }
58
+ return typeof current === 'string' ? current : undefined;
59
+ }
60
+ function resolveMessageLocale(service, key, options) {
61
+ const resolvedKey = options.namespace === undefined ? key : `${options.namespace}.${key}`;
62
+ const snapshot = service.snapshotOptions();
63
+ for (const locale of service.resolveLocales(options.locale)) {
64
+ if (resolveMessage(snapshot.catalogs?.[locale], resolvedKey) !== undefined) {
65
+ return locale;
66
+ }
67
+ }
68
+ return options.locale;
69
+ }
70
+ function normalizeMessageFormatError(error, key) {
71
+ if (error instanceof FormatError || error instanceof SyntaxError || error instanceof Error) {
72
+ return new I18nError(`Invalid ICU MessageFormat for i18n key: ${key}`, 'I18N_INVALID_MESSAGE_FORMAT');
73
+ }
74
+ return new I18nError(`Invalid ICU MessageFormat for i18n key: ${key}`, 'I18N_INVALID_MESSAGE_FORMAT');
75
+ }
76
+
77
+ /**
78
+ * ICU MessageFormat translation service layered on top of the framework-agnostic core `I18nService`.
79
+ *
80
+ * @remarks
81
+ * The root service keeps its simple `{{ name }}` interpolation and fallback behavior. This subpath resolves the
82
+ * message through that service first, then formats the resolved message with ICU MessageFormat plural/select rules.
83
+ */
84
+ export class IcuI18nService {
85
+ service;
86
+
87
+ /**
88
+ * Creates an ICU MessageFormat service from root options or an existing core service.
89
+ *
90
+ * @param options Root i18n options or an existing `I18nService` instance.
91
+ */
92
+ constructor(options = {}) {
93
+ this.service = options instanceof I18nService ? options : createI18n(options);
94
+ }
95
+
96
+ /**
97
+ * Returns the underlying core i18n service used for catalog lookup and fallback resolution.
98
+ *
99
+ * @returns The core `I18nService` instance backing this ICU formatter.
100
+ */
101
+ getCoreService() {
102
+ return this.service;
103
+ }
104
+
105
+ /**
106
+ * Resolves a message with the core service and formats it with ICU MessageFormat.
107
+ *
108
+ * @param key Dot-path catalog key, optionally prefixed by `options.namespace`.
109
+ * @param options Per-call locale, ICU values, default value, and optional named format overrides.
110
+ * @returns The resolved and ICU-formatted message.
111
+ * @throws {I18nError} When the core lookup fails or the ICU pattern/values are invalid.
112
+ */
113
+ translate(key, options) {
114
+ const message = this.service.translate(key, {
115
+ defaultValue: options.defaultValue,
116
+ locale: options.locale,
117
+ namespace: options.namespace,
118
+ values: toCoreInterpolationValues(options.values)
119
+ });
120
+ const messageLocale = resolveMessageLocale(this.service, key, options);
121
+ try {
122
+ const formatter = new IntlMessageFormat(message, messageLocale, options.formats);
123
+ const formatted = formatter.format(toMessageFormatValues(options.values));
124
+ if (typeof formatted === 'string') {
125
+ return formatted;
126
+ }
127
+ } catch (error) {
128
+ throw normalizeMessageFormatError(error, key);
129
+ }
130
+ throw new I18nError(`Invalid ICU MessageFormat result for i18n key: ${key}`, 'I18N_INVALID_MESSAGE_FORMAT');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Creates a standalone ICU MessageFormat i18n service.
136
+ *
137
+ * @param options Root i18n options or an existing `I18nService` instance.
138
+ * @returns An `IcuI18nService` that preserves core lookup semantics before ICU formatting.
139
+ */
140
+ export function createIcuI18n(options = {}) {
141
+ return new IcuI18nService(options);
142
+ }
@@ -0,0 +1,5 @@
1
+ export { I18nError } from './errors.js';
2
+ export { I18nModule } from './module.js';
3
+ export { createI18n, I18nService } from './service.js';
4
+ export type { I18nErrorCode, I18nFallbackLocales, I18nCurrencyFormatOptions, I18nDateTimeFormatOptions, I18nFormatOptions, I18nFormatterOptions, I18nInterpolationValues, I18nListFormatOptions, I18nLocale, I18nMessageCatalogs, I18nMessageTree, I18nMissingMessageContext, I18nMissingMessageHandler, I18nModuleOptions, I18nNamedDateTimeFormats, I18nNamedListFormats, I18nNamedNumberFormats, I18nNamedRelativeTimeFormats, I18nNumberFormatOptions, I18nRelativeTimeFormatOptions, I18nTranslateOptions, I18nTranslationKey, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,qBAAqB,EACrB,UAAU,EACV,mBAAmB,EACnB,eAAe,EACf,yBAAyB,EACzB,yBAAyB,EACzB,iBAAiB,EACjB,wBAAwB,EACxB,oBAAoB,EACpB,sBAAsB,EACtB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { I18nError } from './errors.js';
2
+ export { I18nModule } from './module.js';
3
+ export { createI18n, I18nService } from './service.js';
@@ -0,0 +1,43 @@
1
+ import type { I18nLocale, I18nMessageTree, I18nTranslationKey } from '../types.js';
2
+ import type { I18nLoader } from './shared.js';
3
+ export type { I18nLoader, I18nLoaderLoadOptions } from './shared.js';
4
+ /**
5
+ * Options for the Node-only filesystem i18n catalog loader.
6
+ */
7
+ export interface FileSystemI18nLoaderOptions {
8
+ /** Root catalog directory containing locale subdirectories. */
9
+ readonly rootDir: string;
10
+ }
11
+ /**
12
+ * Node-only JSON catalog loader for `@fluojs/i18n/loaders/fs`.
13
+ *
14
+ * @remarks
15
+ * Catalogs are read from `${rootDir}/${locale}/${namespace}.json`. Locale and namespace inputs are
16
+ * validated before disk reads, and the resolved path must remain inside `rootDir`.
17
+ */
18
+ export declare class FileSystemI18nLoader implements I18nLoader {
19
+ private readonly rootDir;
20
+ /**
21
+ * Creates a filesystem-backed JSON catalog loader.
22
+ *
23
+ * @param options Loader options with an absolute or relative root catalog directory.
24
+ */
25
+ constructor(options: FileSystemI18nLoaderOptions);
26
+ /**
27
+ * Loads and validates one JSON message catalog from disk.
28
+ *
29
+ * @param locale Locale directory to load from.
30
+ * @param namespace Namespace JSON file path without extension.
31
+ * @returns A detached immutable i18n message tree.
32
+ * @throws {I18nError} When inputs are unsafe, the file is missing, JSON is malformed, or catalog shape is invalid.
33
+ */
34
+ load(locale: I18nLocale, namespace: I18nTranslationKey): Promise<I18nMessageTree>;
35
+ }
36
+ /**
37
+ * Creates a Node-only filesystem JSON catalog loader.
38
+ *
39
+ * @param options Loader options with the root catalog directory.
40
+ * @returns A filesystem-backed i18n loader instance.
41
+ */
42
+ export declare function createFileSystemI18nLoader(options: FileSystemI18nLoaderOptions): FileSystemI18nLoader;
43
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/loaders/fs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,UAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC;;;;OAIG;gBACS,OAAO,EAAE,2BAA2B;IAQhD;;;;;;;OAOG;IACG,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC;CA+BxF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CAErG"}
@@ -0,0 +1,79 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { I18nError } from '../errors.js';
4
+ import { isPlainObject, snapshotLoaderMessageTree, validateLoaderLocale, validateLoaderNamespace } from './shared.js';
5
+ function isWithinDirectory(rootDir, targetPath) {
6
+ const relativePath = relative(rootDir, targetPath);
7
+ return relativePath === '' || !relativePath.startsWith('..') && !isAbsolute(relativePath);
8
+ }
9
+
10
+ /**
11
+ * Options for the Node-only filesystem i18n catalog loader.
12
+ */
13
+
14
+ /**
15
+ * Node-only JSON catalog loader for `@fluojs/i18n/loaders/fs`.
16
+ *
17
+ * @remarks
18
+ * Catalogs are read from `${rootDir}/${locale}/${namespace}.json`. Locale and namespace inputs are
19
+ * validated before disk reads, and the resolved path must remain inside `rootDir`.
20
+ */
21
+ export class FileSystemI18nLoader {
22
+ rootDir;
23
+
24
+ /**
25
+ * Creates a filesystem-backed JSON catalog loader.
26
+ *
27
+ * @param options Loader options with an absolute or relative root catalog directory.
28
+ */
29
+ constructor(options) {
30
+ if (!isPlainObject(options) || typeof options.rootDir !== 'string' || options.rootDir.trim() === '') {
31
+ throw new I18nError('Filesystem i18n loader rootDir must be a non-empty string.', 'I18N_INVALID_LOADER_OPTIONS');
32
+ }
33
+ this.rootDir = resolve(options.rootDir);
34
+ }
35
+
36
+ /**
37
+ * Loads and validates one JSON message catalog from disk.
38
+ *
39
+ * @param locale Locale directory to load from.
40
+ * @param namespace Namespace JSON file path without extension.
41
+ * @returns A detached immutable i18n message tree.
42
+ * @throws {I18nError} When inputs are unsafe, the file is missing, JSON is malformed, or catalog shape is invalid.
43
+ */
44
+ async load(locale, namespace) {
45
+ validateLoaderLocale(locale, 'Filesystem i18n');
46
+ validateLoaderNamespace(namespace, 'Filesystem i18n');
47
+ const namespacePath = namespace.replaceAll('/', sep);
48
+ const catalogPath = resolve(this.rootDir, locale, `${namespacePath}.json`);
49
+ if (!isWithinDirectory(this.rootDir, catalogPath)) {
50
+ throw new I18nError('Filesystem i18n catalog path escapes rootDir.', 'I18N_INVALID_LOADER_OPTIONS');
51
+ }
52
+ let raw;
53
+ try {
54
+ raw = await readFile(catalogPath, 'utf8');
55
+ } catch (error) {
56
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
57
+ throw new I18nError(`Missing i18n catalog file: ${locale}/${namespace}.json`, 'I18N_MISSING_CATALOG');
58
+ }
59
+ throw error;
60
+ }
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ } catch (error) {
65
+ throw new I18nError(`Malformed i18n catalog JSON: ${locale}/${namespace}.json`, 'I18N_INVALID_CATALOG');
66
+ }
67
+ return snapshotLoaderMessageTree(parsed, `catalogs.${locale}.${namespace}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Creates a Node-only filesystem JSON catalog loader.
73
+ *
74
+ * @param options Loader options with the root catalog directory.
75
+ * @returns A filesystem-backed i18n loader instance.
76
+ */
77
+ export function createFileSystemI18nLoader(options) {
78
+ return new FileSystemI18nLoader(options);
79
+ }
@@ -0,0 +1,146 @@
1
+ import type { I18nLocale, I18nMessageTree, I18nTranslationKey } from '../types.js';
2
+ import type { I18nLoader, I18nLoaderLoadOptions } from './shared.js';
3
+ export type { I18nLoader, I18nLoaderLoadOptions } from './shared.js';
4
+ /**
5
+ * Request metadata passed to a remote catalog provider.
6
+ */
7
+ export interface RemoteI18nCatalogRequest {
8
+ /** Locale identifier requested by the loader caller. */
9
+ readonly locale: I18nLocale;
10
+ /** Namespace identifier requested by the loader caller. */
11
+ readonly namespace: I18nTranslationKey;
12
+ /** Cancellation signal owned by the loader for timeout and caller abort propagation. */
13
+ readonly signal: AbortSignal;
14
+ }
15
+ /**
16
+ * Provider abstraction for remote JSON catalog backends such as HTTP APIs, object stores, or databases.
17
+ */
18
+ export type RemoteI18nCatalogProvider = (request: RemoteI18nCatalogRequest) => Promise<unknown> | unknown;
19
+ /**
20
+ * Options for provider-backed remote catalog loading.
21
+ */
22
+ export interface RemoteI18nLoaderOptions {
23
+ /** Backend provider that returns one raw catalog object or JSON string for the requested locale and namespace. */
24
+ readonly provider: RemoteI18nCatalogProvider;
25
+ /** Maximum load duration in milliseconds before the loader aborts and throws `I18N_LOADER_TIMEOUT`. */
26
+ readonly timeoutMs?: number;
27
+ }
28
+ /**
29
+ * Cache key input for opt-in remote catalog caching helpers.
30
+ */
31
+ export interface CachedI18nLoaderKeyInput {
32
+ /** Locale identifier requested by the loader caller. */
33
+ readonly locale: I18nLocale;
34
+ /** Namespace identifier requested by the loader caller. */
35
+ readonly namespace: I18nTranslationKey;
36
+ /** Optional caller-owned catalog version included in the default cache key. */
37
+ readonly version?: string;
38
+ }
39
+ /**
40
+ * Options for wrapping a remote catalog loader with explicit in-memory caching.
41
+ */
42
+ export interface CachedI18nLoaderOptions {
43
+ /** Loader to wrap with opt-in cache behavior. */
44
+ readonly loader: I18nLoader;
45
+ /** Cache entry lifetime in milliseconds. */
46
+ readonly ttlMs: number;
47
+ /** Optional catalog version included in the default `(locale, namespace, version)` cache key. */
48
+ readonly version?: string;
49
+ /** Optional caller-owned cache key function for application-specific invalidation boundaries. */
50
+ readonly getCacheKey?: (input: CachedI18nLoaderKeyInput) => string;
51
+ /** Optional clock used by tests or deterministic runtime wrappers. */
52
+ readonly now?: () => number;
53
+ }
54
+ /**
55
+ * Invalidation controls exposed by opt-in cached i18n loaders.
56
+ */
57
+ export interface CachedI18nLoader extends I18nLoader {
58
+ /** Invalidates one catalog cache entry by locale and namespace. */
59
+ invalidate(locale: I18nLocale, namespace: I18nTranslationKey): void;
60
+ /** Clears every cached catalog entry owned by this wrapper. */
61
+ clear(): void;
62
+ }
63
+ /**
64
+ * Provider-backed remote JSON catalog loader for `@fluojs/i18n/loaders/remote`.
65
+ *
66
+ * @remarks
67
+ * The loader validates locale and namespace before calling the provider, propagates cancellation through an
68
+ * `AbortSignal`, enforces a per-load timeout, parses JSON strings, validates message tree shape, and always
69
+ * returns a detached immutable catalog snapshot.
70
+ */
71
+ export declare class RemoteI18nLoader implements I18nLoader {
72
+ private readonly provider;
73
+ private readonly timeoutMs;
74
+ /**
75
+ * Creates a provider-backed remote catalog loader.
76
+ *
77
+ * @param options Remote loader options with a provider and optional timeout.
78
+ */
79
+ constructor(options: RemoteI18nLoaderOptions);
80
+ /**
81
+ * Loads and validates one remote message catalog through the configured provider.
82
+ *
83
+ * @param locale Locale identifier passed to the provider.
84
+ * @param namespace Namespace identifier passed to the provider.
85
+ * @param options Optional per-load cancellation controls.
86
+ * @returns A detached immutable i18n message tree.
87
+ * @throws {I18nError} When inputs are unsafe, the provider misses/fails, loading times out, JSON is malformed, or catalog shape is invalid.
88
+ */
89
+ load(locale: I18nLocale, namespace: I18nTranslationKey, options?: I18nLoaderLoadOptions): Promise<I18nMessageTree>;
90
+ }
91
+ /**
92
+ * Opt-in in-memory caching wrapper for remote i18n catalog loaders.
93
+ *
94
+ * @remarks
95
+ * This wrapper never changes `RemoteI18nLoader` defaults. Applications choose it explicitly when they want catalog
96
+ * caching at the loading boundary and can invalidate entries through `invalidate(...)` or `clear()`.
97
+ */
98
+ export declare class CachedRemoteI18nLoader implements CachedI18nLoader {
99
+ private readonly cache;
100
+ private readonly getCacheKey;
101
+ private readonly loader;
102
+ private readonly now;
103
+ private readonly ttlMs;
104
+ private readonly version;
105
+ /**
106
+ * Creates an explicit cache wrapper around a remote catalog loader.
107
+ *
108
+ * @param options Loader, TTL, version, key, and clock options for the cache wrapper.
109
+ */
110
+ constructor(options: CachedI18nLoaderOptions);
111
+ /**
112
+ * Loads a catalog through the wrapped loader and caches successful results until the configured TTL expires.
113
+ *
114
+ * @param locale Locale identifier passed to the wrapped loader.
115
+ * @param namespace Namespace identifier passed to the wrapped loader.
116
+ * @param options Optional per-load cancellation controls for cache misses.
117
+ * @returns A cached or freshly loaded immutable i18n message tree.
118
+ */
119
+ load(locale: I18nLocale, namespace: I18nTranslationKey, options?: I18nLoaderLoadOptions): Promise<I18nMessageTree>;
120
+ /**
121
+ * Invalidates one cached catalog entry using the same key policy as `load(...)`.
122
+ *
123
+ * @param locale Locale identifier for the cache entry.
124
+ * @param namespace Namespace identifier for the cache entry.
125
+ */
126
+ invalidate(locale: I18nLocale, namespace: I18nTranslationKey): void;
127
+ /**
128
+ * Clears every cache entry owned by this wrapper.
129
+ */
130
+ clear(): void;
131
+ }
132
+ /**
133
+ * Creates a provider-backed remote JSON catalog loader.
134
+ *
135
+ * @param options Remote loader options with a provider and optional timeout.
136
+ * @returns A remote i18n loader instance.
137
+ */
138
+ export declare function createRemoteI18nLoader(options: RemoteI18nLoaderOptions): RemoteI18nLoader;
139
+ /**
140
+ * Creates an opt-in cached remote catalog loader wrapper.
141
+ *
142
+ * @param options Loader, TTL, version, key, and clock options for the cache wrapper.
143
+ * @returns A cached loader wrapper with explicit invalidation controls.
144
+ */
145
+ export declare function createCachedRemoteI18nLoader(options: CachedI18nLoaderOptions): CachedRemoteI18nLoader;
146
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../../src/loaders/remote.ts"],"names":[],"mappings":"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"}