@fluojs/i18n 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -30,7 +30,7 @@ fluo 애플리케이션을 위한 프레임워크 비종속 국제화 코어 표
30
30
  npm install @fluojs/i18n
31
31
  ```
32
32
 
33
- Root entry point는 `@fluojs/core`에만 의존합니다. Optional subpath는 integration dependency를 optional peer로 유지합니다. `@fluojs/i18n/icu`를 opt-in하면 `intl-messageformat`, `@fluojs/i18n/http`를 opt-in하면 `@fluojs/http`, `@fluojs/i18n/validation`을 opt-in하면 `@fluojs/validation`을 함께 설치하세요. 기존 subpath 사용자는 이 dependency boundary 변경이 포함된 릴리스로 업그레이드하기 전에 해당 peer dependency를 application 또는 package manifest에 추가해야 합니다.
33
+ Root entry point는 `@fluojs/core`에만 의존하고 Node.js engine floor를 선언하지 않으며 framework-agnostic bundle에서 안전하게 import할 수 있습니다. Optional subpath는 integration dependency를 optional peer로 유지합니다. `@fluojs/i18n/icu`를 opt-in하면 `intl-messageformat`, `@fluojs/i18n/http`를 opt-in하면 `@fluojs/http`, `@fluojs/i18n/validation`을 opt-in하면 `@fluojs/validation`을 함께 설치하세요. 기존 subpath 사용자는 이 dependency boundary 변경이 포함된 릴리스로 업그레이드하기 전에 해당 peer dependency를 application 또는 package manifest에 추가해야 합니다.
34
34
 
35
35
  ## 사용 시점
36
36
 
@@ -68,6 +68,8 @@ import { I18nModule } from '@fluojs/i18n';
68
68
  class AppModule {}
69
69
  ```
70
70
 
71
+ `I18nModule.forRoot(...)`는 기본적으로 `I18nService`를 global provider로 export하므로 root package를 한 번 import한 뒤 sibling module에서도 shared service를 inject할 수 있습니다. Service를 i18n module을 import한 module 안에만 보이게 하려면 `global: false`를 전달하세요.
72
+
71
73
  ## 코어 번역
72
74
 
73
75
  `I18nService`는 결정론적인 번역 조회를 제공합니다.
@@ -419,10 +421,10 @@ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en'
419
421
 
420
422
  | Export | 설명 |
421
423
  |---|---|
422
- | `I18nModule` | DI 등록을 위한 모듈입니다. |
424
+ | `I18nModule` | Core i18n service surface를 등록하는 모듈 facade입니다. Provider는 기본적으로 global이며 `global: false`로 module-local로 유지할 수 있습니다. |
423
425
  | `I18nService` | Detached option/catalog snapshot을 소유하고 translation을 resolve하며 explicit-locale `Intl` formatting helper(`formatDateTime`, `formatNumber`, `formatCurrency`, `formatPercent`, `formatList`, `formatRelativeTime`)를 제공하는 core service입니다. |
424
426
  | `createI18n` | 독립형 서비스를 생성하기 위한 헬퍼입니다. |
425
- | `I18nError` | 패키지 전용 에러 클래스입니다. |
427
+ | `I18nError` | 안정적인 에러 코드를 포함하는 기본 i18n 패키지 에러입니다. |
426
428
 
427
429
  **타입:** `I18nModuleOptions`, `I18nMessageCatalogs`, `I18nMessageTree`, `I18nTranslateOptions`, `I18nInterpolationValues`, `I18nMissingMessageHandler`, `I18nMissingMessageContext`, `I18nLocale`, `I18nTranslationKey`, `I18nErrorCode`, `I18nFallbackLocales`, `I18nFormatOptions`, `I18nFormatterOptions`, `I18nDateTimeFormatOptions`, `I18nNumberFormatOptions`, `I18nCurrencyFormatOptions`, `I18nListFormatOptions`, `I18nRelativeTimeFormatOptions`, `I18nNamedDateTimeFormats`, `I18nNamedNumberFormats`, `I18nNamedListFormats`, `I18nNamedRelativeTimeFormats`.
428
430
 
package/README.md CHANGED
@@ -30,7 +30,7 @@ Framework-agnostic internationalization core surface for fluo applications.
30
30
  npm install @fluojs/i18n
31
31
  ```
32
32
 
33
- The root entry point depends only on `@fluojs/core`. Optional subpaths keep their integration dependencies as optional peers: install `intl-messageformat` for `@fluojs/i18n/icu`, `@fluojs/http` for `@fluojs/i18n/http`, and `@fluojs/validation` for `@fluojs/i18n/validation` when you opt into those surfaces. Existing subpath users should add those peer dependencies to their application or package manifest before upgrading to the release that includes this dependency boundary change.
33
+ The root entry point depends only on `@fluojs/core`, does not declare a Node.js engine floor, and is safe to import in framework-agnostic bundles. Optional subpaths keep their integration dependencies as optional peers: install `intl-messageformat` for `@fluojs/i18n/icu`, `@fluojs/http` for `@fluojs/i18n/http`, and `@fluojs/validation` for `@fluojs/i18n/validation` when you opt into those surfaces. Existing subpath users should add those peer dependencies to their application or package manifest before upgrading to the release that includes this dependency boundary change.
34
34
 
35
35
  ## When to Use
36
36
 
@@ -68,6 +68,8 @@ import { I18nModule } from '@fluojs/i18n';
68
68
  class AppModule {}
69
69
  ```
70
70
 
71
+ `I18nModule.forRoot(...)` exports `I18nService` as a global provider by default so sibling modules can inject the shared service after the root package is imported once. Pass `global: false` when the service should stay visible only to the module that imports the i18n module.
72
+
71
73
  ## Core Translation
72
74
 
73
75
  The `I18nService` provides deterministic translation lookup.
@@ -419,7 +421,7 @@ Both helpers deduplicate keys across locales, sort output for stable diffs, reje
419
421
 
420
422
  | Export | Description |
421
423
  |---|---|
422
- | `I18nModule` | Module facade for registering the core i18n service surface. |
424
+ | `I18nModule` | Module facade for registering the core i18n service surface; providers are global by default and can be kept module-local with `global: false`. |
423
425
  | `I18nService` | Core service that owns detached options/catalog snapshots, resolves translations, and exposes explicit-locale `Intl` formatting helpers (`formatDateTime`, `formatNumber`, `formatCurrency`, `formatPercent`, `formatList`, `formatRelativeTime`). |
424
426
  | `createI18n(options)` | Creates a standalone `I18nService` without module registration. |
425
427
  | `I18nError` | Base i18n package error with a stable error code. |
@@ -0,0 +1,25 @@
1
+ import type { I18nMessageTree } from './types.js';
2
+ /**
3
+ * Checks whether a value is a plain object suitable for i18n option or catalog validation.
4
+ *
5
+ * @param value Candidate value to inspect.
6
+ * @returns `true` when the value is a plain object or null-prototype object.
7
+ */
8
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
9
+ /**
10
+ * Creates a detached immutable message tree from untrusted catalog-like input.
11
+ *
12
+ * @param value Catalog-like value to validate and snapshot.
13
+ * @param path Diagnostic path used in validation errors.
14
+ * @returns A frozen message tree detached from caller-owned data.
15
+ */
16
+ export declare function snapshotMessageTree(value: unknown, path: string): I18nMessageTree;
17
+ /**
18
+ * Resolves a direct or dot-path message from an immutable i18n message tree.
19
+ *
20
+ * @param tree Locale-scoped catalog tree to inspect.
21
+ * @param key Direct key or dot-path key to resolve.
22
+ * @returns The resolved message string, or `undefined` when the key is absent or not a string leaf.
23
+ */
24
+ export declare function resolveCatalogMessage(tree: I18nMessageTree | undefined, key: string): string | undefined;
25
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO9E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CA0BjF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBxG"}
@@ -0,0 +1,68 @@
1
+ import { I18nError } from './errors.js';
2
+ /**
3
+ * Checks whether a value is a plain object suitable for i18n option or catalog validation.
4
+ *
5
+ * @param value Candidate value to inspect.
6
+ * @returns `true` when the value is a plain object or null-prototype object.
7
+ */
8
+ export function isPlainObject(value) {
9
+ if (typeof value !== 'object' || value === null) {
10
+ return false;
11
+ }
12
+ const prototype = Object.getPrototypeOf(value);
13
+ return prototype === Object.prototype || prototype === null;
14
+ }
15
+
16
+ /**
17
+ * Creates a detached immutable message tree from untrusted catalog-like input.
18
+ *
19
+ * @param value Catalog-like value to validate and snapshot.
20
+ * @param path Diagnostic path used in validation errors.
21
+ * @returns A frozen message tree detached from caller-owned data.
22
+ */
23
+ export function snapshotMessageTree(value, path) {
24
+ if (!isPlainObject(value)) {
25
+ throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
26
+ }
27
+ const snapshot = {};
28
+ for (const [key, entry] of Object.entries(value)) {
29
+ if (key.trim() === '') {
30
+ throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
31
+ }
32
+ if (typeof entry === 'string') {
33
+ snapshot[key] = entry;
34
+ continue;
35
+ }
36
+ if (isPlainObject(entry)) {
37
+ snapshot[key] = snapshotMessageTree(entry, `${path}.${key}`);
38
+ continue;
39
+ }
40
+ throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
41
+ }
42
+ return Object.freeze(snapshot);
43
+ }
44
+
45
+ /**
46
+ * Resolves a direct or dot-path message from an immutable i18n message tree.
47
+ *
48
+ * @param tree Locale-scoped catalog tree to inspect.
49
+ * @param key Direct key or dot-path key to resolve.
50
+ * @returns The resolved message string, or `undefined` when the key is absent or not a string leaf.
51
+ */
52
+ export function resolveCatalogMessage(tree, key) {
53
+ if (tree === undefined) {
54
+ return undefined;
55
+ }
56
+ if (Object.hasOwn(tree, key)) {
57
+ const direct = tree[key];
58
+ return typeof direct === 'string' ? direct : undefined;
59
+ }
60
+ let current = tree;
61
+ for (const part of key.split('.')) {
62
+ if (!isPlainObject(current) || !Object.hasOwn(current, part)) {
63
+ return undefined;
64
+ }
65
+ current = current[part];
66
+ }
67
+ return typeof current === 'string' ? current : undefined;
68
+ }
package/dist/icu.d.ts.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"icu.d.ts","sourceRoot":"","sources":["../src/icu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIlD,OAAO,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAuC,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE/G;;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;AAqDD;;;;;;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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import { FormatError, IntlMessageFormat } from 'intl-messageformat';
2
+ import { resolveCatalogMessage } from './catalog.js';
2
3
  import { I18nError } from './errors.js';
3
4
  import { I18nService, createI18n } from './service.js';
4
5
 
@@ -37,31 +38,11 @@ function toMessageFormatValues(values) {
37
38
  }
38
39
  return Object.fromEntries(Object.entries(values));
39
40
  }
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
41
  function resolveMessageLocale(service, key, options) {
61
42
  const resolvedKey = options.namespace === undefined ? key : `${options.namespace}.${key}`;
62
43
  const snapshot = service.snapshotOptions();
63
44
  for (const locale of service.resolveLocales(options.locale)) {
64
- if (resolveMessage(snapshot.catalogs?.[locale], resolvedKey) !== undefined) {
45
+ if (resolveCatalogMessage(snapshot.catalogs?.[locale], resolvedKey) !== undefined) {
65
46
  return locale;
66
47
  }
67
48
  }
@@ -20,13 +20,7 @@ export interface I18nLoader {
20
20
  */
21
21
  load(locale: I18nLocale, namespace: I18nTranslationKey, options?: I18nLoaderLoadOptions): Promise<I18nMessageTree>;
22
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>;
23
+ export { isPlainObject } from '../catalog.js';
30
24
  /**
31
25
  * Validates a loader locale before a backend is called.
32
26
  *
@@ -1 +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"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/loaders/shared.ts"],"names":[],"mappings":"AAEA,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,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;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,CAEvF"}
@@ -1,4 +1,5 @@
1
1
  import { I18nError } from '../errors.js';
2
+ import { snapshotMessageTree } from '../catalog.js';
2
3
  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
4
  const SAFE_NAMESPACE_SEGMENT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?$/;
4
5
 
@@ -10,19 +11,7 @@ const SAFE_NAMESPACE_SEGMENT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9]
10
11
  * Loader contract for asynchronous locale and namespace catalog sources.
11
12
  */
12
13
 
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
- }
14
+ export { isPlainObject } from '../catalog.js';
26
15
 
27
16
  /**
28
17
  * Validates a loader locale before a backend is called.
@@ -67,23 +56,5 @@ export function validateLoaderNamespace(namespace, label) {
67
56
  * @returns A frozen message tree detached from caller-owned data.
68
57
  */
69
58
  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);
59
+ return snapshotMessageTree(value, path);
89
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgF,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAwMlI;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,iBAAsB,GAAG,iBAAiB,CAU5F"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgF,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmKlI;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,iBAAsB,GAAG,iBAAiB,CAU5F"}
package/dist/options.js CHANGED
@@ -1,11 +1,5 @@
1
1
  import { I18nError } from './errors.js';
2
- function isPlainObject(value) {
3
- if (typeof value !== 'object' || value === null) {
4
- return false;
5
- }
6
- const prototype = Object.getPrototypeOf(value);
7
- return prototype === Object.prototype || prototype === null;
8
- }
2
+ import { isPlainObject, snapshotMessageTree } from './catalog.js';
9
3
  function assertLocale(value, label) {
10
4
  if (typeof value !== 'string' || value.trim() === '') {
11
5
  throw new I18nError(`${label} must be a non-empty string.`, 'I18N_INVALID_LOCALE_CONFIG');
@@ -62,27 +56,6 @@ function snapshotFormats(formats) {
62
56
  relativeTime: snapshotNamedIntlFormats(formats.relativeTime, 'formats.relativeTime')
63
57
  });
64
58
  }
65
- function snapshotMessageTree(value, path) {
66
- if (!isPlainObject(value)) {
67
- throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
68
- }
69
- const snapshot = {};
70
- for (const [key, entry] of Object.entries(value)) {
71
- if (key.trim() === '') {
72
- throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
73
- }
74
- if (typeof entry === 'string') {
75
- snapshot[key] = entry;
76
- continue;
77
- }
78
- if (isPlainObject(entry)) {
79
- snapshot[key] = snapshotMessageTree(entry, `${path}.${key}`);
80
- continue;
81
- }
82
- throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
83
- }
84
- return Object.freeze(snapshot);
85
- }
86
59
  function snapshotCatalogs(catalogs, supportedLocales) {
87
60
  if (catalogs === undefined) {
88
61
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,qBAAqB,EACrB,UAAU,EAEV,iBAAiB,EACjB,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACrB,MAAM,YAAY,CAAC;AA+IpB;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C;;;;OAIG;gBACS,OAAO,GAAE,iBAAsB;IAI3C;;;;OAIG;IACH,eAAe,IAAI,iBAAiB;IAIpC;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE;IA2BzD,OAAO,CAAC,mBAAmB;IAkB3B;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAWhF;;;;;;;OAOG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWrE;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAiBzE;;;;;;;OAOG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWtE;;;;;;;OAOG;IACH,UAAU,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM;IAY7E;;;;;;;;OAQG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,GAAG,MAAM;IAWpH;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM;CAoC9D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,WAAW,CAEvE"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,qBAAqB,EACrB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACrB,MAAM,YAAY,CAAC;AA2GpB;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C;;;;OAIG;gBACS,OAAO,GAAE,iBAAsB;IAI3C;;;;OAIG;IACH,eAAe,IAAI,iBAAiB;IAIpC;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE;IA2BzD,OAAO,CAAC,mBAAmB;IAkB3B;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAWhF;;;;;;;OAOG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWrE;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAiBzE;;;;;;;OAOG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWtE;;;;;;;OAOG;IACH,UAAU,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM;IAY7E;;;;;;;;OAQG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,GAAG,MAAM;IAWpH;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM;CAoC9D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,WAAW,CAEvE"}
package/dist/service.js CHANGED
@@ -1,15 +1,6 @@
1
+ import { isPlainObject, resolveCatalogMessage } from './catalog.js';
1
2
  import { snapshotI18nModuleOptions } from './options.js';
2
3
  import { I18nError } from './errors.js';
3
- function hasOwn(value, key) {
4
- return typeof value === 'object' && value !== null && Object.hasOwn(value, key);
5
- }
6
- function isPlainObject(value) {
7
- if (typeof value !== 'object' || value === null) {
8
- return false;
9
- }
10
- const prototype = Object.getPrototypeOf(value);
11
- return prototype === Object.prototype || prototype === null;
12
- }
13
4
  function normalizeTranslationKey(key, namespace) {
14
5
  if (typeof key !== 'string') {
15
6
  throw new I18nError('Translation key must be a string.', 'I18N_INVALID_OPTIONS');
@@ -86,23 +77,6 @@ function interpolate(message, values) {
86
77
  return value === undefined || value === null ? '' : String(value);
87
78
  });
88
79
  }
89
- function resolveMessage(tree, key) {
90
- if (tree === undefined) {
91
- return undefined;
92
- }
93
- if (hasOwn(tree, key)) {
94
- const direct = tree[key];
95
- return typeof direct === 'string' ? direct : undefined;
96
- }
97
- let current = tree;
98
- for (const part of key.split('.')) {
99
- if (!hasOwn(current, part)) {
100
- return undefined;
101
- }
102
- current = current[part];
103
- }
104
- return typeof current === 'string' ? current : undefined;
105
- }
106
80
  function pushUnique(locales, locale) {
107
81
  if (locale !== undefined && !locales.includes(locale)) {
108
82
  locales.push(locale);
@@ -316,7 +290,7 @@ export class I18nService {
316
290
  const resolvedKey = normalizeTranslationKey(key, options.namespace);
317
291
  const locales = this.resolveLocales(options.locale);
318
292
  for (const locale of locales) {
319
- const message = resolveMessage(this.options.catalogs?.[locale], resolvedKey);
293
+ const message = resolveCatalogMessage(this.options.catalogs?.[locale], resolvedKey);
320
294
  if (message !== undefined) {
321
295
  return interpolate(message, options.values);
322
296
  }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "localization",
9
9
  "translations"
10
10
  ],
11
- "version": "1.0.3",
11
+ "version": "2.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -16,9 +16,6 @@
16
16
  "url": "https://github.com/fluojs/fluo.git",
17
17
  "directory": "packages/i18n"
18
18
  },
19
- "engines": {
20
- "node": ">=20.0.0"
21
- },
22
19
  "publishConfig": {
23
20
  "access": "public"
24
21
  },
@@ -63,12 +60,12 @@
63
60
  "dist"
64
61
  ],
65
62
  "dependencies": {
66
- "@fluojs/core": "^1.0.3"
63
+ "@fluojs/core": "^1.1.0"
67
64
  },
68
65
  "peerDependencies": {
69
66
  "intl-messageformat": "^11.2.4",
70
- "@fluojs/http": "^1.1.0",
71
- "@fluojs/validation": "^1.0.4"
67
+ "@fluojs/http": "^2.0.1",
68
+ "@fluojs/validation": "^1.0.6"
72
69
  },
73
70
  "peerDependenciesMeta": {
74
71
  "@fluojs/http": {
@@ -84,8 +81,9 @@
84
81
  "devDependencies": {
85
82
  "intl-messageformat": "^11.2.4",
86
83
  "vitest": "^3.2.4",
87
- "@fluojs/http": "^1.1.0",
88
- "@fluojs/validation": "^1.0.4"
84
+ "@fluojs/testing": "^2.0.0",
85
+ "@fluojs/http": "^2.0.1",
86
+ "@fluojs/validation": "^1.0.6"
89
87
  },
90
88
  "scripts": {
91
89
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",