@i18n-micro/core 1.3.3 → 1.3.8

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/dist/base.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CleanTranslation, MissingHandler, Params, PluralFunc, TranslationKey, Translations } from '@i18n-micro/types';
2
- import { FormatService } from './format-service';
2
+ import { FormatService, DateTimeFormatsConfig, NumberFormatsConfig } from './format-service';
3
3
  import { TranslationStorage, useTranslationHelper } from './translation';
4
4
  export interface BaseI18nOptions {
5
5
  storage?: TranslationStorage;
@@ -7,6 +7,10 @@ export interface BaseI18nOptions {
7
7
  missingWarn?: boolean;
8
8
  missingHandler?: (locale: string, key: string, routeName: string) => void;
9
9
  getCustomMissingHandler?: () => MissingHandler | null;
10
+ /** Named number formats per locale (Vue I18n-compatible). */
11
+ numberFormats?: NumberFormatsConfig;
12
+ /** Named datetime formats per locale (Vue I18n-compatible `datetimeFormats`). */
13
+ datetimeFormats?: DateTimeFormatsConfig;
10
14
  }
11
15
  /**
12
16
  * Abstract base class for i18n adapters
@@ -21,6 +25,11 @@ export declare abstract class BaseI18n {
21
25
  missingWarn: boolean;
22
26
  missingHandler?: (locale: string, key: string, routeName: string) => void;
23
27
  getCustomMissingHandler?: () => MissingHandler | null;
28
+ /**
29
+ * Set on the server when the SSR payload should carry only the keys the render
30
+ * actually used. `null` on the client and in `chunk` mode, so the lookup path
31
+ * stays a plain property read.
32
+ */
24
33
  constructor(options?: BaseI18nOptions);
25
34
  /**
26
35
  * Get current locale
@@ -50,6 +59,26 @@ export declare abstract class BaseI18n {
50
59
  * Check if translation exists in lookup source.
51
60
  */
52
61
  protected resolveHas(key: TranslationKey, routeContext?: unknown): boolean;
62
+ /**
63
+ * Two live layers as one tree, via {@link mergeTranslationLayers}.
64
+ *
65
+ * Used when the hot path keeps two objects instead of merging them — the fallback locale,
66
+ * or the Nuxt page-transition layer — so the dump answers what `t()` answers.
67
+ */
68
+ protected resolveTranslationTree(lower: Record<string, unknown>, upper: Record<string, unknown>): Translations;
69
+ /**
70
+ * Dump of what `t()` can resolve right now for the active locale and route.
71
+ *
72
+ * Live tree, not a clone — read-only; mutate via {@link setTranslation} / merge helpers.
73
+ * When a second layer is live (fallback locale, Nuxt transition hot-reload), walks both
74
+ * through {@link resolveTranslationTree} so the dump matches `t()`.
75
+ */
76
+ resolveTranslations(routeContext?: unknown): Translations;
77
+ /**
78
+ * Called after {@link setTranslation} changes the dictionary. Adapters override it to
79
+ * bump whatever their reactivity is built on.
80
+ */
81
+ protected onTranslationsChanged(): void;
53
82
  /**
54
83
  * Context passed to missing-key handlers.
55
84
  */
@@ -57,10 +86,20 @@ export declare abstract class BaseI18n {
57
86
  locale: string;
58
87
  routeName: string;
59
88
  };
89
+ /**
90
+ * Dev-only client `console.warn`, gated by `missingWarn`.
91
+ * Shared by missing translations and missing named formats.
92
+ */
93
+ protected warnDev(message: string): void;
60
94
  /**
61
95
  * Warn or invoke handler when translation is missing.
62
96
  */
63
97
  protected warnMissing(key: TranslationKey, routeContext?: unknown): void;
98
+ /**
99
+ * Warn when a named number/datetime format key is missing.
100
+ * Falls back to default Intl options (visible in dev via {@link warnDev}).
101
+ */
102
+ protected warnMissingFormat(kind: 'number' | 'datetime', key: string, locale: string): void;
64
103
  /**
65
104
  * Get translation for a key
66
105
  */
@@ -74,13 +113,19 @@ export declare abstract class BaseI18n {
74
113
  */
75
114
  tc(key: TranslationKey, count: number | Params, defaultValue?: string): string;
76
115
  /**
77
- * Format number
116
+ * Format number.
117
+ * Supports Vue I18n-style named formats: `tn(1000, 'currency')`.
78
118
  */
79
119
  tn(value: number, options?: Intl.NumberFormatOptions): string;
120
+ tn(value: number, key: string, overrides?: Intl.NumberFormatOptions): string;
121
+ tn(value: number, key: string, locale: string, overrides?: Intl.NumberFormatOptions): string;
80
122
  /**
81
- * Format date
123
+ * Format date.
124
+ * Supports Vue I18n-style named formats: `td(date, 'short')`.
82
125
  */
83
126
  td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
127
+ td(value: Date | number | string, key: string, overrides?: Intl.DateTimeFormatOptions): string;
128
+ td(value: Date | number | string, key: string, locale: string, overrides?: Intl.DateTimeFormatOptions): string;
84
129
  /**
85
130
  * Format relative time
86
131
  */
@@ -90,9 +135,22 @@ export declare abstract class BaseI18n {
90
135
  */
91
136
  has(key: TranslationKey, routeContext?: unknown): boolean;
92
137
  /**
93
- * Clear cache
138
+ * Replace the value at `key` — a subtree, a string, a number, anything.
139
+ *
140
+ * `set` and not `merge`: `setTranslation('aaa', { fff: 'ggg' })` leaves `aaa` holding only
141
+ * `fff`, and `setTranslation('aaa', 'fff')` leaves a string where the subtree was. Use
142
+ * `mergeTranslations` when the existing siblings should survive.
143
+ *
144
+ * Writes to the active locale and route, so the change is visible to `t()` immediately and
145
+ * lives exactly as long as the chunk it belongs to.
146
+ */
147
+ setTranslation(key: TranslationKey, value: unknown): void;
148
+ /**
149
+ * Clear translation + formatter caches
94
150
  */
95
151
  clearCache(): void;
152
+ private resolveNumberFormatArgs;
153
+ private resolveDateTimeFormatArgs;
96
154
  /**
97
155
  * Core translation loading logic (without reactivity)
98
156
  * Subclasses can override addTranslations/addRouteTranslations to add reactivity
@@ -1,4 +1,34 @@
1
+ export type NumberFormatsConfig = Record<string, Record<string, Intl.NumberFormatOptions>>;
2
+ export type DateTimeFormatsConfig = Record<string, Record<string, Intl.DateTimeFormatOptions>>;
3
+ export interface FormatServiceOptions {
4
+ numberFormats?: NumberFormatsConfig;
5
+ /** Vue I18n-compatible name (`datetimeFormats`). */
6
+ datetimeFormats?: DateTimeFormatsConfig;
7
+ }
8
+ /**
9
+ * Shared Intl formatters with:
10
+ * - Map cache keyed by locale + options (avoids `new Intl.*Format` on every call)
11
+ * - Named formats (`numberFormats` / `datetimeFormats`) for Vue I18n-compatible `$tn(n, 'currency')`
12
+ */
1
13
  export declare class FormatService {
14
+ private numberFormats;
15
+ private datetimeFormats;
16
+ private numberCache;
17
+ private dateCache;
18
+ private relativeCache;
19
+ constructor(options?: FormatServiceOptions);
20
+ setNumberFormats(formats: NumberFormatsConfig): void;
21
+ setDateTimeFormats(formats: DateTimeFormatsConfig): void;
22
+ getNumberFormats(): NumberFormatsConfig;
23
+ getDateTimeFormats(): DateTimeFormatsConfig;
24
+ clearCache(): void;
25
+ /** Resolve a named number format for a locale (exact match, then language subtag). */
26
+ resolveNumberFormat(locale: string, key: string): Intl.NumberFormatOptions | undefined;
27
+ /** Resolve a named datetime format for a locale (exact match, then language subtag). */
28
+ resolveDateTimeFormat(locale: string, key: string): Intl.DateTimeFormatOptions | undefined;
29
+ getNumberFormatter(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
30
+ getDateTimeFormatter(locale: string, options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
31
+ getRelativeTimeFormatter(locale: string, options?: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat;
2
32
  formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string;
3
33
  formatDate(value: Date | number | string, locale: string, options?: Intl.DateTimeFormatOptions): string;
4
34
  formatRelativeTime(value: Date | number | string, locale: string, options?: Intl.RelativeTimeFormatOptions): string;
@@ -1 +1 @@
1
- export { defaultPlural, getByPath, hasTranslationValue, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, mergeTranslationChunk, resolveTranslation, translationCacheKey, withPrefixStrategy, type MergeTranslationChunkOptions, } from './helpers';
1
+ export { defaultPlural, getByPath, hasTranslationValue, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, collectTranslationPaths, mergeTranslationChunk, mergeTranslationLayers, resolveTranslation, setTranslationAtKey, translationCacheKey, withPrefixStrategy, type MergeTranslationChunkOptions, } from './helpers';
package/dist/helpers.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=/\{(\w+)\}/g,c="index";function d(t,r){return`${t}:${r||c}`}function f(t,r){if(t==null)return null;const n=a(t,r);return n===void 0?null:n}function g(t,r){return f(t,r)!==null}function y(t,r,n){return Object.keys(t).length===0?r:n?.preserveExisting?Object.assign({},r,t):Object.assign({},t,r)}function x(t,r){return!r||t.indexOf("{")===-1?t:t.replace(s,(n,e)=>{const i=r[e];return i!==void 0?String(i):`{${e}}`})}function a(t,r){if(t==null||typeof r!="string"||r.length===0)return;if(Object.prototype.hasOwnProperty.call(t,r))return t[r];if(!r.includes("."))return;const n=r.split(".");let e=t;for(const i of n){if(e==null||typeof e!="object")return;const u=e;if(!Object.prototype.hasOwnProperty.call(u,i))return;e=u[i]}return e}function p(t){return t==="prefix"||t==="prefix_and_default"}function P(t){return t==="no_prefix"}function v(t){return t==="prefix"}function S(t){return t==="prefix_except_default"}function h(t){return t==="prefix_and_default"}const _=(t,r,n,e,i)=>{const u=i(t,n);if(!u)return null;const l=u.toString().split("|");if(l.length===0)return null;const o=r<l.length?l[r]:l[l.length-1];return o?o.trim().replace("{count}",r.toString()):null};exports.defaultPlural=_;exports.getByPath=a;exports.hasTranslationValue=g;exports.interpolate=x;exports.isNoPrefixStrategy=P;exports.isPrefixAndDefaultStrategy=h;exports.isPrefixExceptDefaultStrategy=S;exports.isPrefixStrategy=v;exports.mergeTranslationChunk=y;exports.resolveTranslation=f;exports.translationCacheKey=d;exports.withPrefixStrategy=p;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=/\{(\w+)\}/g,v="index";function x(t,n){return`${t}:${n||v}`}function u(t,n){if(t==null)return null;const o=_(t,n);return o===void 0?null:o}function P(t,n){return u(t,n)!==null}function T(t,n,o){return Object.keys(t).length===0?n:o?.preserveExisting?a(n,t):a(t,n)}const c=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);function a(t,n){const o={...t};for(const e of Object.keys(n)){if(e==="__proto__")continue;const r=n[e],i=o[e];o[e]=c(r)&&c(i)?a(i,r):r}return o}function O(t,n){return!n||t.indexOf("{")===-1?t:t.replace(h,(o,e)=>{const r=n[e];return r!==void 0?String(r):`{${e}}`})}function _(t,n){if(t==null||typeof n!="string"||n.length===0)return;if(Object.prototype.hasOwnProperty.call(t,n))return t[n];if(!n.includes("."))return;const o=n.split(".");let e=t;for(const r of o){if(e==null||typeof e!="object")return;const i=e;if(!Object.prototype.hasOwnProperty.call(i,r))return;e=i[r]}return e}function S(t,n,o){if(typeof n!="string"||n.length===0)return t;if(Object.prototype.hasOwnProperty.call(t,n)||!n.includes("."))return n==="__proto__"?t:{...t,[n]:o};const e=n.split(".");if(e.some(s=>s==="__proto__"))return t;const r={...t};let i=r;for(const s of e.slice(0,-1)){const l=i[s],f=c(l)?{...l}:{};i[s]=f,i=f}return i[e[e.length-1]]=o,r}function b(t,n){const o=y(t,n),e={};return d(t,n,"",e),Object.keys(e).length===0?o:{...o,...e}}function y(t,n){const o={...t};for(const e of Object.keys(n)){if(e==="__proto__")continue;const r=n[e];if(r==null)continue;const i=o[e];o[e]=c(r)&&c(i)?y(i,r):r}return o}function d(t,n,o,e){for(const r of Object.keys(t)){if(r==="__proto__")continue;const i=t[r];if(!c(i))continue;const s=o?`${o}.${r}`:r,l=n[r];c(l)?d(i,l,s,e):l!=null&&g(i,s,e)}}function g(t,n,o){for(const e of Object.keys(t)){if(e==="__proto__")continue;const r=t[e],i=`${n}.${e}`;c(r)?g(r,i,o):o[i]=r}}function p(t,n,o=""){for(const e of Object.keys(t)){if(e==="__proto__")continue;const r=o?`${o}.${e}`:e;n.add(r);const i=t[e];c(i)&&p(i,n,r)}}function $(t){return t==="prefix"||t==="prefix_and_default"}function m(t){return t==="no_prefix"}function A(t){return t==="prefix"}function E(t){return t==="prefix_except_default"}function j(t){return t==="prefix_and_default"}const k=(t,n,o,e,r)=>{const i=r(t,o);if(!i)return null;const s=i.toString().split("|");if(s.length===0)return null;const l=n<s.length?s[n]:s[s.length-1];return l?l.trim().replace("{count}",n.toString()):null};exports.collectTranslationPaths=p;exports.defaultPlural=k;exports.getByPath=_;exports.hasTranslationValue=P;exports.interpolate=O;exports.isNoPrefixStrategy=m;exports.isPrefixAndDefaultStrategy=j;exports.isPrefixExceptDefaultStrategy=E;exports.isPrefixStrategy=A;exports.mergeTranslationChunk=T;exports.mergeTranslationLayers=b;exports.resolveTranslation=u;exports.setTranslationAtKey=S;exports.translationCacheKey=x;exports.withPrefixStrategy=$;
@@ -6,9 +6,51 @@ export interface MergeTranslationChunkOptions {
6
6
  /** When true, existing keys win over incoming. Default: incoming wins. */
7
7
  preserveExisting?: boolean;
8
8
  }
9
+ /**
10
+ * Merge two translation chunks, descending into nested objects.
11
+ *
12
+ * `Object.assign` is wrong here, and quietly so: chunks are trees, so a shallow merge of
13
+ * `{ nav: { about, home } }` with `{ nav: { extra } }` replaces the whole `nav` subtree
14
+ * and loses `about` and `home`. Nothing throws — the keys simply resolve to themselves
15
+ * later, which is the raw-key render the loader exists to prevent.
16
+ *
17
+ * Written here rather than reusing `@i18n-micro/utils/deep-merge`: that package carries
18
+ * build-time dependencies, and `core` is installed by every consumer at runtime.
19
+ */
9
20
  export declare function mergeTranslationChunk(existing: Record<string, unknown>, incoming: Record<string, unknown>, options?: MergeTranslationChunkOptions): Record<string, unknown>;
10
21
  export declare function interpolate(template: string, params: Params): string;
11
22
  export declare function getByPath(obj: Record<string, unknown> | null | undefined, path: string): unknown;
23
+ /**
24
+ * A translation tree with `key` set to `value`, whatever either of them is.
25
+ *
26
+ * Replaces rather than merges: `set('aaa', { x: 1 })` on `{ aaa: { bbb: 'ccc' } }` leaves
27
+ * `aaa` holding only `x`, and `set('aaa', 'text')` leaves a string where a subtree was.
28
+ * Merging is what `mergeTranslationChunk` is for.
29
+ *
30
+ * The tree is not mutated — only the nodes along the path are copied, so the call costs the
31
+ * depth of the key and not the size of the dictionary, and callers holding the old tree
32
+ * (a frozen SSR chunk, a rendered snapshot) keep seeing what they had.
33
+ *
34
+ * Key resolution mirrors {@link getByPath}: an existing flat key wins over the dotted path,
35
+ * so a dictionary written as `{ 'a.b': 'x' }` is updated in place rather than gaining a
36
+ * nested `a.b` that `t('a.b')` would never read.
37
+ */
38
+ export declare function setTranslationAtKey(tree: Record<string, unknown>, key: string, value: unknown): Record<string, unknown>;
39
+ /**
40
+ * Two live lookup layers as one tree, shaped the way a single layer already is.
41
+ *
42
+ * `upper` wins, exactly as a per-key fallthrough does, so the result answers every key the
43
+ * two layers together can answer — with one exception that a tree cannot express: where
44
+ * `upper` holds a scalar and `lower` holds an object at the same path, `t('a')` reads the
45
+ * scalar while `t('a.b')` still reaches into `lower`. Those descendants come back as flat
46
+ * dotted keys, which is what `getByPath` looks at first anyway.
47
+ *
48
+ * The alternative — resolving every collected path one at a time — returned a different
49
+ * shape depending on whether a second layer happened to be live, and repeated each nested
50
+ * leaf as a flat key beside the object holding it.
51
+ */
52
+ export declare function mergeTranslationLayers(lower: Record<string, unknown>, upper: Record<string, unknown>): Record<string, unknown>;
53
+ export declare function collectTranslationPaths(obj: Record<string, unknown>, paths: Set<string>, prefix?: string): void;
12
54
  export declare function withPrefixStrategy(strategy: Strategies): strategy is "prefix" | "prefix_and_default";
13
55
  export declare function isNoPrefixStrategy(strategy: Strategies): strategy is "no_prefix";
14
56
  export declare function isPrefixStrategy(strategy: Strategies): strategy is "prefix";
package/dist/helpers.d.ts CHANGED
@@ -6,9 +6,51 @@ export interface MergeTranslationChunkOptions {
6
6
  /** When true, existing keys win over incoming. Default: incoming wins. */
7
7
  preserveExisting?: boolean;
8
8
  }
9
+ /**
10
+ * Merge two translation chunks, descending into nested objects.
11
+ *
12
+ * `Object.assign` is wrong here, and quietly so: chunks are trees, so a shallow merge of
13
+ * `{ nav: { about, home } }` with `{ nav: { extra } }` replaces the whole `nav` subtree
14
+ * and loses `about` and `home`. Nothing throws — the keys simply resolve to themselves
15
+ * later, which is the raw-key render the loader exists to prevent.
16
+ *
17
+ * Written here rather than reusing `@i18n-micro/utils/deep-merge`: that package carries
18
+ * build-time dependencies, and `core` is installed by every consumer at runtime.
19
+ */
9
20
  export declare function mergeTranslationChunk(existing: Record<string, unknown>, incoming: Record<string, unknown>, options?: MergeTranslationChunkOptions): Record<string, unknown>;
10
21
  export declare function interpolate(template: string, params: Params): string;
11
22
  export declare function getByPath(obj: Record<string, unknown> | null | undefined, path: string): unknown;
23
+ /**
24
+ * A translation tree with `key` set to `value`, whatever either of them is.
25
+ *
26
+ * Replaces rather than merges: `set('aaa', { x: 1 })` on `{ aaa: { bbb: 'ccc' } }` leaves
27
+ * `aaa` holding only `x`, and `set('aaa', 'text')` leaves a string where a subtree was.
28
+ * Merging is what `mergeTranslationChunk` is for.
29
+ *
30
+ * The tree is not mutated — only the nodes along the path are copied, so the call costs the
31
+ * depth of the key and not the size of the dictionary, and callers holding the old tree
32
+ * (a frozen SSR chunk, a rendered snapshot) keep seeing what they had.
33
+ *
34
+ * Key resolution mirrors {@link getByPath}: an existing flat key wins over the dotted path,
35
+ * so a dictionary written as `{ 'a.b': 'x' }` is updated in place rather than gaining a
36
+ * nested `a.b` that `t('a.b')` would never read.
37
+ */
38
+ export declare function setTranslationAtKey(tree: Record<string, unknown>, key: string, value: unknown): Record<string, unknown>;
39
+ /**
40
+ * Two live lookup layers as one tree, shaped the way a single layer already is.
41
+ *
42
+ * `upper` wins, exactly as a per-key fallthrough does, so the result answers every key the
43
+ * two layers together can answer — with one exception that a tree cannot express: where
44
+ * `upper` holds a scalar and `lower` holds an object at the same path, `t('a')` reads the
45
+ * scalar while `t('a.b')` still reaches into `lower`. Those descendants come back as flat
46
+ * dotted keys, which is what `getByPath` looks at first anyway.
47
+ *
48
+ * The alternative — resolving every collected path one at a time — returned a different
49
+ * shape depending on whether a second layer happened to be live, and repeated each nested
50
+ * leaf as a flat key beside the object holding it.
51
+ */
52
+ export declare function mergeTranslationLayers(lower: Record<string, unknown>, upper: Record<string, unknown>): Record<string, unknown>;
53
+ export declare function collectTranslationPaths(obj: Record<string, unknown>, paths: Set<string>, prefix?: string): void;
12
54
  export declare function withPrefixStrategy(strategy: Strategies): strategy is "prefix" | "prefix_and_default";
13
55
  export declare function isNoPrefixStrategy(strategy: Strategies): strategy is "no_prefix";
14
56
  export declare function isPrefixStrategy(strategy: Strategies): strategy is "prefix";
package/dist/helpers.mjs CHANGED
@@ -1,74 +1,141 @@
1
- const f = /\{(\w+)\}/g, c = "index";
2
- function d(r, t) {
3
- return `${r}:${t || c}`;
1
+ const p = /\{(\w+)\}/g, y = "index";
2
+ function O(t, n) {
3
+ return `${t}:${n || y}`;
4
4
  }
5
- function s(r, t) {
6
- if (r == null) return null;
7
- const e = a(r, t);
8
- return e === void 0 ? null : e;
5
+ function g(t, n) {
6
+ if (t == null) return null;
7
+ const o = v(t, n);
8
+ return o === void 0 ? null : o;
9
9
  }
10
- function p(r, t) {
11
- return s(r, t) !== null;
10
+ function x(t, n) {
11
+ return g(t, n) !== null;
12
12
  }
13
- function g(r, t, e) {
14
- return Object.keys(r).length === 0 ? t : e?.preserveExisting ? Object.assign({}, t, r) : Object.assign({}, r, t);
13
+ function T(t, n, o) {
14
+ return Object.keys(t).length === 0 ? n : o?.preserveExisting ? l(n, t) : l(t, n);
15
15
  }
16
- function x(r, t) {
17
- return !t || r.indexOf("{") === -1 ? r : r.replace(f, (e, n) => {
18
- const i = t[n];
19
- return i !== void 0 ? String(i) : `{${n}}`;
16
+ const f = (t) => t !== null && typeof t == "object" && !Array.isArray(t);
17
+ function l(t, n) {
18
+ const o = { ...t };
19
+ for (const e of Object.keys(n)) {
20
+ if (e === "__proto__") continue;
21
+ const r = n[e], i = o[e];
22
+ o[e] = f(r) && f(i) ? l(i, r) : r;
23
+ }
24
+ return o;
25
+ }
26
+ function P(t, n) {
27
+ return !n || t.indexOf("{") === -1 ? t : t.replace(p, (o, e) => {
28
+ const r = n[e];
29
+ return r !== void 0 ? String(r) : `{${e}}`;
20
30
  });
21
31
  }
22
- function a(r, t) {
23
- if (r == null || typeof t != "string" || t.length === 0) return;
24
- if (Object.prototype.hasOwnProperty.call(r, t))
25
- return r[t];
26
- if (!t.includes(".")) return;
27
- const e = t.split(".");
28
- let n = r;
29
- for (const i of e) {
30
- if (n == null || typeof n != "object") return;
31
- const u = n;
32
- if (!Object.prototype.hasOwnProperty.call(u, i)) return;
33
- n = u[i];
32
+ function v(t, n) {
33
+ if (t == null || typeof n != "string" || n.length === 0) return;
34
+ if (Object.prototype.hasOwnProperty.call(t, n))
35
+ return t[n];
36
+ if (!n.includes(".")) return;
37
+ const o = n.split(".");
38
+ let e = t;
39
+ for (const r of o) {
40
+ if (e == null || typeof e != "object") return;
41
+ const i = e;
42
+ if (!Object.prototype.hasOwnProperty.call(i, r)) return;
43
+ e = i[r];
44
+ }
45
+ return e;
46
+ }
47
+ function b(t, n, o) {
48
+ if (typeof n != "string" || n.length === 0) return t;
49
+ if (Object.prototype.hasOwnProperty.call(t, n) || !n.includes("."))
50
+ return n === "__proto__" ? t : { ...t, [n]: o };
51
+ const e = n.split(".");
52
+ if (e.some((c) => c === "__proto__")) return t;
53
+ const r = { ...t };
54
+ let i = r;
55
+ for (const c of e.slice(0, -1)) {
56
+ const s = i[c], u = f(s) ? { ...s } : {};
57
+ i[c] = u, i = u;
58
+ }
59
+ return i[e[e.length - 1]] = o, r;
60
+ }
61
+ function S(t, n) {
62
+ const o = a(t, n), e = {};
63
+ return _(t, n, "", e), Object.keys(e).length === 0 ? o : { ...o, ...e };
64
+ }
65
+ function a(t, n) {
66
+ const o = { ...t };
67
+ for (const e of Object.keys(n)) {
68
+ if (e === "__proto__") continue;
69
+ const r = n[e];
70
+ if (r == null) continue;
71
+ const i = o[e];
72
+ o[e] = f(r) && f(i) ? a(i, r) : r;
73
+ }
74
+ return o;
75
+ }
76
+ function _(t, n, o, e) {
77
+ for (const r of Object.keys(t)) {
78
+ if (r === "__proto__") continue;
79
+ const i = t[r];
80
+ if (!f(i)) continue;
81
+ const c = o ? `${o}.${r}` : r, s = n[r];
82
+ f(s) ? _(i, s, c, e) : s != null && d(i, c, e);
83
+ }
84
+ }
85
+ function d(t, n, o) {
86
+ for (const e of Object.keys(t)) {
87
+ if (e === "__proto__") continue;
88
+ const r = t[e], i = `${n}.${e}`;
89
+ f(r) ? d(r, i, o) : o[i] = r;
90
+ }
91
+ }
92
+ function h(t, n, o = "") {
93
+ for (const e of Object.keys(t)) {
94
+ if (e === "__proto__") continue;
95
+ const r = o ? `${o}.${e}` : e;
96
+ n.add(r);
97
+ const i = t[e];
98
+ f(i) && h(i, n, r);
34
99
  }
35
- return n;
36
100
  }
37
- function v(r) {
38
- return r === "prefix" || r === "prefix_and_default";
101
+ function $(t) {
102
+ return t === "prefix" || t === "prefix_and_default";
39
103
  }
40
- function y(r) {
41
- return r === "no_prefix";
104
+ function E(t) {
105
+ return t === "no_prefix";
42
106
  }
43
- function _(r) {
44
- return r === "prefix";
107
+ function j(t) {
108
+ return t === "prefix";
45
109
  }
46
- function O(r) {
47
- return r === "prefix_except_default";
110
+ function k(t) {
111
+ return t === "prefix_except_default";
48
112
  }
49
- function P(r) {
50
- return r === "prefix_and_default";
113
+ function w(t) {
114
+ return t === "prefix_and_default";
51
115
  }
52
- const S = (r, t, e, n, i) => {
53
- const u = i(r, e);
54
- if (!u)
116
+ const A = (t, n, o, e, r) => {
117
+ const i = r(t, o);
118
+ if (!i)
55
119
  return null;
56
- const l = u.toString().split("|");
57
- if (l.length === 0) return null;
58
- const o = t < l.length ? l[t] : l[l.length - 1];
59
- return o ? o.trim().replace("{count}", t.toString()) : null;
120
+ const c = i.toString().split("|");
121
+ if (c.length === 0) return null;
122
+ const s = n < c.length ? c[n] : c[c.length - 1];
123
+ return s ? s.trim().replace("{count}", n.toString()) : null;
60
124
  };
61
125
  export {
62
- S as defaultPlural,
63
- a as getByPath,
64
- p as hasTranslationValue,
65
- x as interpolate,
66
- y as isNoPrefixStrategy,
67
- P as isPrefixAndDefaultStrategy,
68
- O as isPrefixExceptDefaultStrategy,
69
- _ as isPrefixStrategy,
70
- g as mergeTranslationChunk,
71
- s as resolveTranslation,
72
- d as translationCacheKey,
73
- v as withPrefixStrategy
126
+ h as collectTranslationPaths,
127
+ A as defaultPlural,
128
+ v as getByPath,
129
+ x as hasTranslationValue,
130
+ P as interpolate,
131
+ E as isNoPrefixStrategy,
132
+ w as isPrefixAndDefaultStrategy,
133
+ k as isPrefixExceptDefaultStrategy,
134
+ j as isPrefixStrategy,
135
+ T as mergeTranslationChunk,
136
+ S as mergeTranslationLayers,
137
+ g as resolveTranslation,
138
+ b as setTranslationAtKey,
139
+ O as translationCacheKey,
140
+ $ as withPrefixStrategy
74
141
  };
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./helpers.cjs");class g{formatNumber(t,e,r){return new Intl.NumberFormat(e,r).format(t)}formatDate(t,e,r){const s=new Date(t);return Number.isNaN(s.getTime())?"Invalid Date":new Intl.DateTimeFormat(e,r).format(s)}formatRelativeTime(t,e,r){const s=new Date(t),n=new Intl.RelativeTimeFormat(e,r);if(Number.isNaN(s.getTime()))return n.format(0,"second");const i=Math.floor((Date.now()-s.getTime())/1e3),o=[{unit:"year",seconds:31536e3},{unit:"month",seconds:2592e3},{unit:"day",seconds:86400},{unit:"hour",seconds:3600},{unit:"minute",seconds:60},{unit:"second",seconds:1}];for(const{unit:u,seconds:h}of o){const l=Math.floor(i/h);if(l>=1)return n.format(-l,u)}return n.format(0,"second")}}function f(c){const t=c?.translations??new Map;return{hasCache(e,r){return t.has(a.translationCacheKey(e,r))},getCache(e,r){return t.get(a.translationCacheKey(e,r))},setCache(e,r,s){},hasTranslation(e,r){for(const[s,n]of t)if(s.startsWith(`${e}:`)&&a.getByPath(n,r)!==void 0)return!0;return!1},hasPageTranslation(e,r){return t.has(a.translationCacheKey(e,r))},getTranslation(e,r,s){const n=t.get(a.translationCacheKey(e,r));return n?a.resolveTranslation(n,s):null},loadTranslations(e,r,s="index"){const n=a.translationCacheKey(e,s),i=t.get(n);i?Object.assign(i,r):t.set(n,{...r})},setTranslations(e,r,s="index"){t.set(a.translationCacheKey(e,s),r)},loadPageTranslations(e,r,s){const n=a.translationCacheKey(e,r),i=t.get(n);!i||Object.keys(i).length===0?t.set(n,{...s}):Object.assign(i,s)},mergeTranslation(e,r,s,n=!1){const i=a.translationCacheKey(e,r),o=t.get(i);o?Object.assign(o,s):t.set(i,{...s})},clearCache(){t.clear()}}}class m{constructor(t={}){this.formatter=new g,this.helper=f(t.storage),this.formatter=new g,this.pluralFunc=t.plural||a.defaultPlural,this.missingWarn=t.missingWarn??!0,this.missingHandler=t.missingHandler,this.getCustomMissingHandler=t.getCustomMissingHandler}touch(){}resolveRouteName(t){return typeof t=="string"?t:this.getRoute()}resolveLookup(t,e){const r=this.getLocale(),s=this.resolveRouteName(e);let n=this.helper.getTranslation(r,s,String(t));if(n===null){const i=this.getFallbackLocale();r!==i&&(n=this.helper.getTranslation(i,s,String(t)))}return n}resolveHas(t,e){const r=this.getLocale(),s=this.resolveRouteName(e);return this.helper.getTranslation(r,s,String(t))!==null}getMissingContext(t){return{locale:this.getLocale(),routeName:this.resolveRouteName(t)}}warnMissing(t,e){const{locale:r,routeName:s}=this.getMissingContext(e),n=this.getCustomMissingHandler?.();if(n){n(r,String(t),s);return}if(this.missingHandler){this.missingHandler(r,String(t),s);return}this.missingWarn&&process.env.NODE_ENV!=="production"&&typeof window<"u"&&console.warn(`Not found '${t}' key in '${r}' locale messages for route '${s}'.`)}t(t,e,r,s){if(!t)return"";this.touch();const n=this.resolveLookup(t,s);return n==null?(this.warnMissing(t,s),r===void 0?t:r||t):typeof n!="string"||!e?n:a.interpolate(n,e)}ts(t,e,r,s){return this.t(t,e,r,s)?.toString()??r??t}tc(t,e,r){this.touch();const{count:s,...n}=typeof e=="number"?{count:e}:e;if(s===void 0)return r??t;const i=(u,h,l)=>this.t(u,h,l);return this.pluralFunc(t,Number.parseInt(s.toString(),10),n,this.getLocale(),i)??r??t}tn(t,e){return this.touch(),this.formatter.formatNumber(t,this.getLocale(),e)}td(t,e){return this.touch(),this.formatter.formatDate(t,this.getLocale(),e)}tdr(t,e){return this.touch(),this.formatter.formatRelativeTime(t,this.getLocale(),e)}has(t,e){return this.touch(),this.resolveHas(t,e)}clearCache(){this.helper.clearCache()}loadTranslationsCore(t,e,r,s="index"){r?this.helper.mergeTranslation(t,s,e,!0):this.helper.setTranslations(t,e,s)}loadRouteTranslationsCore(t,e,r,s){s?this.helper.mergeTranslation(t,e,r,!0):this.helper.loadPageTranslations(t,e,r)}}exports.defaultPlural=a.defaultPlural;exports.getByPath=a.getByPath;exports.hasTranslationValue=a.hasTranslationValue;exports.interpolate=a.interpolate;exports.isNoPrefixStrategy=a.isNoPrefixStrategy;exports.isPrefixAndDefaultStrategy=a.isPrefixAndDefaultStrategy;exports.isPrefixExceptDefaultStrategy=a.isPrefixExceptDefaultStrategy;exports.isPrefixStrategy=a.isPrefixStrategy;exports.mergeTranslationChunk=a.mergeTranslationChunk;exports.resolveTranslation=a.resolveTranslation;exports.translationCacheKey=a.translationCacheKey;exports.withPrefixStrategy=a.withPrefixStrategy;exports.BaseI18n=m;exports.FormatService=g;exports.useTranslationHelper=f;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./helpers.cjs");function d(l){if(!l)return"";const t=Object.keys(l).sort();if(t.length===0)return"";const e={};for(const a of t)e[a]=l[a];return JSON.stringify(e)}function m(l,t){const e=d(t);return e?`${l}\0${e}`:l}class g{constructor(t={}){this.numberCache=new Map,this.dateCache=new Map,this.relativeCache=new Map,this.numberFormats=t.numberFormats??{},this.datetimeFormats=t.datetimeFormats??{}}setNumberFormats(t){this.numberFormats=t,this.numberCache.clear()}setDateTimeFormats(t){this.datetimeFormats=t,this.dateCache.clear()}getNumberFormats(){return this.numberFormats}getDateTimeFormats(){return this.datetimeFormats}clearCache(){this.numberCache.clear(),this.dateCache.clear(),this.relativeCache.clear()}resolveNumberFormat(t,e){return this.numberFormats[t]?.[e]??this.numberFormats[t.split("-")[0]]?.[e]}resolveDateTimeFormat(t,e){return this.datetimeFormats[t]?.[e]??this.datetimeFormats[t.split("-")[0]]?.[e]}getNumberFormatter(t,e){const a=m(t,e);let r=this.numberCache.get(a);return r||(r=new Intl.NumberFormat(t,e),this.numberCache.set(a,r)),r}getDateTimeFormatter(t,e){const a=m(t,e);let r=this.dateCache.get(a);return r||(r=new Intl.DateTimeFormat(t,e),this.dateCache.set(a,r)),r}getRelativeTimeFormatter(t,e){const a=m(t,e);let r=this.relativeCache.get(a);return r||(r=new Intl.RelativeTimeFormat(t,e),this.relativeCache.set(a,r)),r}formatNumber(t,e,a){return this.getNumberFormatter(e,a).format(t)}formatDate(t,e,a){const r=new Date(t);return Number.isNaN(r.getTime())?"Invalid Date":this.getDateTimeFormatter(e,a).format(r)}formatRelativeTime(t,e,a){const r=new Date(t),s=this.getRelativeTimeFormatter(e,a);if(Number.isNaN(r.getTime()))return s.format(0,"second");const n=Math.floor((Date.now()-r.getTime())/1e3),o=[{unit:"year",seconds:31536e3},{unit:"month",seconds:2592e3},{unit:"day",seconds:86400},{unit:"hour",seconds:3600},{unit:"minute",seconds:60},{unit:"second",seconds:1}];for(const{unit:h,seconds:u}of o){const c=Math.floor(n/u);if(c>=1)return s.format(-c,h)}return s.format(0,"second")}}function f(l){const t=l?.translations??new Map;return{hasCache(e,a){return t.has(i.translationCacheKey(e,a))},getCache(e,a){return t.get(i.translationCacheKey(e,a))},setCache(e,a,r){},hasTranslation(e,a){for(const[r,s]of t)if(r.startsWith(`${e}:`)&&i.getByPath(s,a)!==void 0)return!0;return!1},hasPageTranslation(e,a){return t.has(i.translationCacheKey(e,a))},getTranslation(e,a,r){const s=t.get(i.translationCacheKey(e,a));return s?i.resolveTranslation(s,r):null},loadTranslations(e,a,r="index"){const s=i.translationCacheKey(e,r),n=t.get(s);n?Object.assign(n,a):t.set(s,{...a})},setTranslations(e,a,r="index"){t.set(i.translationCacheKey(e,r),a)},loadPageTranslations(e,a,r){const s=i.translationCacheKey(e,a),n=t.get(s);!n||Object.keys(n).length===0?t.set(s,{...r}):Object.assign(n,r)},mergeTranslation(e,a,r,s=!1){const n=i.translationCacheKey(e,a),o=t.get(n);o?Object.assign(o,r):t.set(n,{...r})},clearCache(){t.clear()}}}class T{constructor(t={}){this.formatter=new g,this.helper=f(t.storage);const e={numberFormats:t.numberFormats,datetimeFormats:t.datetimeFormats};this.formatter=new g(e),this.pluralFunc=t.plural||i.defaultPlural,this.missingWarn=t.missingWarn??!0,this.missingHandler=t.missingHandler,this.getCustomMissingHandler=t.getCustomMissingHandler}touch(){}resolveRouteName(t){return typeof t=="string"?t:this.getRoute()}resolveLookup(t,e){const a=this.getLocale(),r=this.resolveRouteName(e),s=this.helper.getTranslation(a,r,String(t));if(s!==null)return s;const n=this.getFallbackLocale();return a!==n?this.helper.getTranslation(n,r,String(t)):null}resolveHas(t,e){const a=this.getLocale(),r=this.resolveRouteName(e);return this.helper.getTranslation(a,r,String(t))!==null}resolveTranslationTree(t,e){return i.mergeTranslationLayers(t,e)}resolveTranslations(t){this.touch();const e=this.getLocale(),a=this.resolveRouteName(t),r=this.helper.getCache(e,a)??{},s=this.getFallbackLocale();if(s===e)return r;const n=this.helper.getCache(s,a);return n?this.resolveTranslationTree(n,r):r}onTranslationsChanged(){}getMissingContext(t){return{locale:this.getLocale(),routeName:this.resolveRouteName(t)}}warnDev(t){this.missingWarn&&process.env.NODE_ENV!=="production"&&(typeof window>"u"||console.warn(t))}warnMissing(t,e){const{locale:a,routeName:r}=this.getMissingContext(e),s=this.getCustomMissingHandler?.();if(s){s(a,String(t),r);return}if(this.missingHandler){this.missingHandler(a,String(t),r);return}this.warnDev(`Not found '${t}' key in '${a}' locale messages for route '${r}'.`)}warnMissingFormat(t,e,a){this.warnDev(`Not found '${e}' ${t} format in '${a}' locale. Falling back to default Intl options.`)}t(t,e,a,r){if(!t)return"";this.touch();const s=this.resolveLookup(t,r);return s==null?(this.warnMissing(t,r),a===void 0?t:a||t):typeof s!="string"||!e?s:i.interpolate(s,e)}ts(t,e,a,r){return this.t(t,e,a,r)?.toString()??a??t}tc(t,e,a){this.touch();const{count:r,...s}=typeof e=="number"?{count:e}:e;if(r===void 0)return a??t;const n=(h,u,c)=>this.t(h,u,c);return this.pluralFunc(t,Number.parseInt(r.toString(),10),s,this.getLocale(),n)??a??t}tn(t,e,a,r){this.touch();const s=this.resolveNumberFormatArgs(e,a,r);return this.formatter.formatNumber(t,s.locale,s.options)}td(t,e,a,r){this.touch();const s=this.resolveDateTimeFormatArgs(e,a,r);return this.formatter.formatDate(t,s.locale,s.options)}tdr(t,e){return this.touch(),this.formatter.formatRelativeTime(t,this.getLocale(),e)}has(t,e){return this.touch(),this.resolveHas(t,e)}setTranslation(t,e){const a=this.getLocale(),r=this.getRoute(),s=this.helper.getCache(a,r)??{};this.helper.setTranslations(a,i.setTranslationAtKey(s,String(t),e),r),this.onTranslationsChanged()}clearCache(){this.helper.clearCache(),this.formatter.clearCache()}resolveNumberFormatArgs(t,e,a){if(typeof t!="string")return{locale:this.getLocale(),options:t};let r=this.getLocale(),s;typeof e=="string"?(r=e,s=a):s=e;const n=this.formatter.resolveNumberFormat(r,t);return n||this.warnMissingFormat("number",t,r),!n&&!s?{locale:r,options:void 0}:{locale:r,options:n?{...n,...s}:s}}resolveDateTimeFormatArgs(t,e,a){if(typeof t!="string")return{locale:this.getLocale(),options:t};let r=this.getLocale(),s;typeof e=="string"?(r=e,s=a):s=e;const n=this.formatter.resolveDateTimeFormat(r,t);return n||this.warnMissingFormat("datetime",t,r),!n&&!s?{locale:r,options:void 0}:{locale:r,options:n?{...n,...s}:s}}loadTranslationsCore(t,e,a,r="index"){a?this.helper.mergeTranslation(t,r,e,!0):this.helper.setTranslations(t,e,r)}loadRouteTranslationsCore(t,e,a,r){r?this.helper.mergeTranslation(t,e,a,!0):this.helper.loadPageTranslations(t,e,a)}}function b(l){let t=l.locale,e=l.fallbackLocale??l.locale,a=l.route??"index",r=0;const s=new Set,n=()=>{r++,s.forEach(o=>o())};return{subscribe(o){return s.add(o),()=>s.delete(o)},getSnapshot(){return`${t}:${a}:${r}`},getLocale(){return t},setLocale(o){t!==o&&(t=o,n())},getFallbackLocale(){return e},setFallbackLocale(o){e!==o&&(e=o,n())},getRoute(){return a},setRoute(o){a!==o&&(a=o,n())},notify:n}}exports.collectTranslationPaths=i.collectTranslationPaths;exports.defaultPlural=i.defaultPlural;exports.getByPath=i.getByPath;exports.hasTranslationValue=i.hasTranslationValue;exports.interpolate=i.interpolate;exports.isNoPrefixStrategy=i.isNoPrefixStrategy;exports.isPrefixAndDefaultStrategy=i.isPrefixAndDefaultStrategy;exports.isPrefixExceptDefaultStrategy=i.isPrefixExceptDefaultStrategy;exports.isPrefixStrategy=i.isPrefixStrategy;exports.mergeTranslationChunk=i.mergeTranslationChunk;exports.mergeTranslationLayers=i.mergeTranslationLayers;exports.resolveTranslation=i.resolveTranslation;exports.setTranslationAtKey=i.setTranslationAtKey;exports.translationCacheKey=i.translationCacheKey;exports.withPrefixStrategy=i.withPrefixStrategy;exports.BaseI18n=T;exports.FormatService=g;exports.createReactiveI18nStore=b;exports.useTranslationHelper=f;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { BaseI18n, BaseI18nOptions } from './base';
2
- import { FormatService } from './format-service';
3
- import { defaultPlural, getByPath, hasTranslationValue, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, mergeTranslationChunk, resolveTranslation, translationCacheKey, withPrefixStrategy, MergeTranslationChunkOptions } from './helpers';
2
+ import { FormatService, DateTimeFormatsConfig, FormatServiceOptions, NumberFormatsConfig } from './format-service';
3
+ import { defaultPlural, getByPath, hasTranslationValue, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, collectTranslationPaths, mergeTranslationChunk, mergeTranslationLayers, resolveTranslation, setTranslationAtKey, translationCacheKey, withPrefixStrategy, MergeTranslationChunkOptions } from './helpers';
4
4
  import { TranslationStorage, useTranslationHelper } from './translation';
5
- export { useTranslationHelper, interpolate, getByPath, hasTranslationValue, mergeTranslationChunk, resolveTranslation, translationCacheKey, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural, FormatService, BaseI18n, type MergeTranslationChunkOptions, type TranslationStorage, type BaseI18nOptions, };
5
+ import { createReactiveI18nStore, ReactiveI18nStore } from './reactive-store';
6
+ export { useTranslationHelper, interpolate, getByPath, hasTranslationValue, collectTranslationPaths, mergeTranslationChunk, mergeTranslationLayers, resolveTranslation, setTranslationAtKey, translationCacheKey, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural, FormatService, BaseI18n, createReactiveI18nStore, type MergeTranslationChunkOptions, type TranslationStorage, type BaseI18nOptions, type ReactiveI18nStore, type NumberFormatsConfig, type DateTimeFormatsConfig, type FormatServiceOptions, };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { BaseI18n, BaseI18nOptions } from './base';
2
- import { FormatService } from './format-service';
3
- import { defaultPlural, getByPath, hasTranslationValue, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, mergeTranslationChunk, resolveTranslation, translationCacheKey, withPrefixStrategy, MergeTranslationChunkOptions } from './helpers';
2
+ import { FormatService, DateTimeFormatsConfig, FormatServiceOptions, NumberFormatsConfig } from './format-service';
3
+ import { defaultPlural, getByPath, hasTranslationValue, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, collectTranslationPaths, mergeTranslationChunk, mergeTranslationLayers, resolveTranslation, setTranslationAtKey, translationCacheKey, withPrefixStrategy, MergeTranslationChunkOptions } from './helpers';
4
4
  import { TranslationStorage, useTranslationHelper } from './translation';
5
- export { useTranslationHelper, interpolate, getByPath, hasTranslationValue, mergeTranslationChunk, resolveTranslation, translationCacheKey, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural, FormatService, BaseI18n, type MergeTranslationChunkOptions, type TranslationStorage, type BaseI18nOptions, };
5
+ import { createReactiveI18nStore, ReactiveI18nStore } from './reactive-store';
6
+ export { useTranslationHelper, interpolate, getByPath, hasTranslationValue, collectTranslationPaths, mergeTranslationChunk, mergeTranslationLayers, resolveTranslation, setTranslationAtKey, translationCacheKey, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural, FormatService, BaseI18n, createReactiveI18nStore, type MergeTranslationChunkOptions, type TranslationStorage, type BaseI18nOptions, type ReactiveI18nStore, type NumberFormatsConfig, type DateTimeFormatsConfig, type FormatServiceOptions, };
package/dist/index.mjs CHANGED
@@ -1,18 +1,72 @@
1
- import { translationCacheKey as a, resolveTranslation as f, getByPath as m, defaultPlural as d, interpolate as v } from "./helpers.mjs";
2
- import { hasTranslationValue as w, isNoPrefixStrategy as x, isPrefixAndDefaultStrategy as S, isPrefixExceptDefaultStrategy as D, isPrefixStrategy as H, mergeTranslationChunk as L, withPrefixStrategy as M } from "./helpers.mjs";
3
- class g {
1
+ import { translationCacheKey as l, resolveTranslation as g, getByPath as d, defaultPlural as b, mergeTranslationLayers as F, interpolate as T, setTranslationAtKey as v } from "./helpers.mjs";
2
+ import { collectTranslationPaths as x, hasTranslationValue as y, isNoPrefixStrategy as R, isPrefixAndDefaultStrategy as M, isPrefixExceptDefaultStrategy as $, isPrefixStrategy as H, mergeTranslationChunk as P, withPrefixStrategy as k } from "./helpers.mjs";
3
+ function C(o) {
4
+ if (!o) return "";
5
+ const t = Object.keys(o).sort();
6
+ if (t.length === 0) return "";
7
+ const e = {};
8
+ for (const s of t)
9
+ e[s] = o[s];
10
+ return JSON.stringify(e);
11
+ }
12
+ function m(o, t) {
13
+ const e = C(t);
14
+ return e ? `${o}\0${e}` : o;
15
+ }
16
+ class f {
17
+ constructor(t = {}) {
18
+ this.numberCache = /* @__PURE__ */ new Map(), this.dateCache = /* @__PURE__ */ new Map(), this.relativeCache = /* @__PURE__ */ new Map(), this.numberFormats = t.numberFormats ?? {}, this.datetimeFormats = t.datetimeFormats ?? {};
19
+ }
20
+ setNumberFormats(t) {
21
+ this.numberFormats = t, this.numberCache.clear();
22
+ }
23
+ setDateTimeFormats(t) {
24
+ this.datetimeFormats = t, this.dateCache.clear();
25
+ }
26
+ getNumberFormats() {
27
+ return this.numberFormats;
28
+ }
29
+ getDateTimeFormats() {
30
+ return this.datetimeFormats;
31
+ }
32
+ clearCache() {
33
+ this.numberCache.clear(), this.dateCache.clear(), this.relativeCache.clear();
34
+ }
35
+ /** Resolve a named number format for a locale (exact match, then language subtag). */
36
+ resolveNumberFormat(t, e) {
37
+ return this.numberFormats[t]?.[e] ?? this.numberFormats[t.split("-")[0]]?.[e];
38
+ }
39
+ /** Resolve a named datetime format for a locale (exact match, then language subtag). */
40
+ resolveDateTimeFormat(t, e) {
41
+ return this.datetimeFormats[t]?.[e] ?? this.datetimeFormats[t.split("-")[0]]?.[e];
42
+ }
43
+ getNumberFormatter(t, e) {
44
+ const s = m(t, e);
45
+ let r = this.numberCache.get(s);
46
+ return r || (r = new Intl.NumberFormat(t, e), this.numberCache.set(s, r)), r;
47
+ }
48
+ getDateTimeFormatter(t, e) {
49
+ const s = m(t, e);
50
+ let r = this.dateCache.get(s);
51
+ return r || (r = new Intl.DateTimeFormat(t, e), this.dateCache.set(s, r)), r;
52
+ }
53
+ getRelativeTimeFormatter(t, e) {
54
+ const s = m(t, e);
55
+ let r = this.relativeCache.get(s);
56
+ return r || (r = new Intl.RelativeTimeFormat(t, e), this.relativeCache.set(s, r)), r;
57
+ }
4
58
  formatNumber(t, e, s) {
5
- return new Intl.NumberFormat(e, s).format(t);
59
+ return this.getNumberFormatter(e, s).format(t);
6
60
  }
7
61
  formatDate(t, e, s) {
8
62
  const r = new Date(t);
9
- return Number.isNaN(r.getTime()) ? "Invalid Date" : new Intl.DateTimeFormat(e, s).format(r);
63
+ return Number.isNaN(r.getTime()) ? "Invalid Date" : this.getDateTimeFormatter(e, s).format(r);
10
64
  }
11
65
  formatRelativeTime(t, e, s) {
12
- const r = new Date(t), n = new Intl.RelativeTimeFormat(e, s);
66
+ const r = new Date(t), a = this.getRelativeTimeFormatter(e, s);
13
67
  if (Number.isNaN(r.getTime()))
14
- return n.format(0, "second");
15
- const i = Math.floor((Date.now() - r.getTime()) / 1e3), o = [
68
+ return a.format(0, "second");
69
+ const n = Math.floor((Date.now() - r.getTime()) / 1e3), i = [
16
70
  { unit: "year", seconds: 31536e3 },
17
71
  { unit: "month", seconds: 2592e3 },
18
72
  { unit: "day", seconds: 86400 },
@@ -20,61 +74,71 @@ class g {
20
74
  { unit: "minute", seconds: 60 },
21
75
  { unit: "second", seconds: 1 }
22
76
  ];
23
- for (const { unit: u, seconds: h } of o) {
24
- const l = Math.floor(i / h);
25
- if (l >= 1)
26
- return n.format(-l, u);
77
+ for (const { unit: h, seconds: u } of i) {
78
+ const c = Math.floor(n / u);
79
+ if (c >= 1)
80
+ return a.format(-c, h);
27
81
  }
28
- return n.format(0, "second");
82
+ return a.format(0, "second");
29
83
  }
30
84
  }
31
- function T(c) {
32
- const t = c?.translations ?? /* @__PURE__ */ new Map();
85
+ function p(o) {
86
+ const t = o?.translations ?? /* @__PURE__ */ new Map();
33
87
  return {
34
88
  hasCache(e, s) {
35
- return t.has(a(e, s));
89
+ return t.has(l(e, s));
36
90
  },
37
91
  getCache(e, s) {
38
- return t.get(a(e, s));
92
+ return t.get(l(e, s));
39
93
  },
40
94
  setCache(e, s, r) {
41
95
  },
42
96
  hasTranslation(e, s) {
43
- for (const [r, n] of t)
44
- if (r.startsWith(`${e}:`) && m(n, s) !== void 0)
97
+ for (const [r, a] of t)
98
+ if (r.startsWith(`${e}:`) && d(a, s) !== void 0)
45
99
  return !0;
46
100
  return !1;
47
101
  },
48
102
  hasPageTranslation(e, s) {
49
- return t.has(a(e, s));
103
+ return t.has(l(e, s));
50
104
  },
51
105
  getTranslation(e, s, r) {
52
- const n = t.get(a(e, s));
53
- return n ? f(n, r) : null;
106
+ const a = t.get(l(e, s));
107
+ return a ? g(a, r) : null;
54
108
  },
55
109
  loadTranslations(e, s, r = "index") {
56
- const n = a(e, r), i = t.get(n);
57
- i ? Object.assign(i, s) : t.set(n, { ...s });
110
+ const a = l(e, r), n = t.get(a);
111
+ n ? Object.assign(n, s) : t.set(a, { ...s });
58
112
  },
59
113
  setTranslations(e, s, r = "index") {
60
- t.set(a(e, r), s);
114
+ t.set(l(e, r), s);
61
115
  },
62
116
  loadPageTranslations(e, s, r) {
63
- const n = a(e, s), i = t.get(n);
64
- !i || Object.keys(i).length === 0 ? t.set(n, { ...r }) : Object.assign(i, r);
117
+ const a = l(e, s), n = t.get(a);
118
+ !n || Object.keys(n).length === 0 ? t.set(a, { ...r }) : Object.assign(n, r);
65
119
  },
66
- mergeTranslation(e, s, r, n = !1) {
67
- const i = a(e, s), o = t.get(i);
68
- o ? Object.assign(o, r) : t.set(i, { ...r });
120
+ mergeTranslation(e, s, r, a = !1) {
121
+ const n = l(e, s), i = t.get(n);
122
+ i ? Object.assign(i, r) : t.set(n, { ...r });
69
123
  },
70
124
  clearCache() {
71
125
  t.clear();
72
126
  }
73
127
  };
74
128
  }
75
- class N {
129
+ class w {
130
+ /**
131
+ * Set on the server when the SSR payload should carry only the keys the render
132
+ * actually used. `null` on the client and in `chunk` mode, so the lookup path
133
+ * stays a plain property read.
134
+ */
76
135
  constructor(t = {}) {
77
- this.formatter = new g(), this.helper = T(t.storage), this.formatter = new g(), this.pluralFunc = t.plural || d, this.missingWarn = t.missingWarn ?? !0, this.missingHandler = t.missingHandler, this.getCustomMissingHandler = t.getCustomMissingHandler;
136
+ this.formatter = new f(), this.helper = p(t.storage);
137
+ const e = {
138
+ numberFormats: t.numberFormats,
139
+ datetimeFormats: t.datetimeFormats
140
+ };
141
+ this.formatter = new f(e), this.pluralFunc = t.plural || b, this.missingWarn = t.missingWarn ?? !0, this.missingHandler = t.missingHandler, this.getCustomMissingHandler = t.getCustomMissingHandler;
78
142
  }
79
143
  // --- Protected hooks (subclasses may override) ---
80
144
  /**
@@ -92,13 +156,10 @@ class N {
92
156
  * Lookup translation value. Returns null when missing.
93
157
  */
94
158
  resolveLookup(t, e) {
95
- const s = this.getLocale(), r = this.resolveRouteName(e);
96
- let n = this.helper.getTranslation(s, r, String(t));
97
- if (n === null) {
98
- const i = this.getFallbackLocale();
99
- s !== i && (n = this.helper.getTranslation(i, r, String(t)));
100
- }
101
- return n;
159
+ const s = this.getLocale(), r = this.resolveRouteName(e), a = this.helper.getTranslation(s, r, String(t));
160
+ if (a !== null) return a;
161
+ const n = this.getFallbackLocale();
162
+ return s !== n ? this.helper.getTranslation(n, r, String(t)) : null;
102
163
  }
103
164
  /**
104
165
  * Check if translation exists in lookup source.
@@ -107,26 +168,69 @@ class N {
107
168
  const s = this.getLocale(), r = this.resolveRouteName(e);
108
169
  return this.helper.getTranslation(s, r, String(t)) !== null;
109
170
  }
171
+ /**
172
+ * Two live layers as one tree, via {@link mergeTranslationLayers}.
173
+ *
174
+ * Used when the hot path keeps two objects instead of merging them — the fallback locale,
175
+ * or the Nuxt page-transition layer — so the dump answers what `t()` answers.
176
+ */
177
+ resolveTranslationTree(t, e) {
178
+ return F(t, e);
179
+ }
180
+ /**
181
+ * Dump of what `t()` can resolve right now for the active locale and route.
182
+ *
183
+ * Live tree, not a clone — read-only; mutate via {@link setTranslation} / merge helpers.
184
+ * When a second layer is live (fallback locale, Nuxt transition hot-reload), walks both
185
+ * through {@link resolveTranslationTree} so the dump matches `t()`.
186
+ */
187
+ resolveTranslations(t) {
188
+ this.touch();
189
+ const e = this.getLocale(), s = this.resolveRouteName(t), r = this.helper.getCache(e, s) ?? {}, a = this.getFallbackLocale();
190
+ if (a === e) return r;
191
+ const n = this.helper.getCache(a, s);
192
+ return n ? this.resolveTranslationTree(n, r) : r;
193
+ }
194
+ /**
195
+ * Called after {@link setTranslation} changes the dictionary. Adapters override it to
196
+ * bump whatever their reactivity is built on.
197
+ */
198
+ onTranslationsChanged() {
199
+ }
110
200
  /**
111
201
  * Context passed to missing-key handlers.
112
202
  */
113
203
  getMissingContext(t) {
114
204
  return { locale: this.getLocale(), routeName: this.resolveRouteName(t) };
115
205
  }
206
+ /**
207
+ * Dev-only client `console.warn`, gated by `missingWarn`.
208
+ * Shared by missing translations and missing named formats.
209
+ */
210
+ warnDev(t) {
211
+ this.missingWarn && process.env.NODE_ENV !== "production" && (typeof window > "u" || console.warn(t));
212
+ }
116
213
  /**
117
214
  * Warn or invoke handler when translation is missing.
118
215
  */
119
216
  warnMissing(t, e) {
120
- const { locale: s, routeName: r } = this.getMissingContext(e), n = this.getCustomMissingHandler?.();
121
- if (n) {
122
- n(s, String(t), r);
217
+ const { locale: s, routeName: r } = this.getMissingContext(e), a = this.getCustomMissingHandler?.();
218
+ if (a) {
219
+ a(s, String(t), r);
123
220
  return;
124
221
  }
125
222
  if (this.missingHandler) {
126
223
  this.missingHandler(s, String(t), r);
127
224
  return;
128
225
  }
129
- this.missingWarn && process.env.NODE_ENV !== "production" && typeof window < "u" && console.warn(`Not found '${t}' key in '${s}' locale messages for route '${r}'.`);
226
+ this.warnDev(`Not found '${t}' key in '${s}' locale messages for route '${r}'.`);
227
+ }
228
+ /**
229
+ * Warn when a named number/datetime format key is missing.
230
+ * Falls back to default Intl options (visible in dev via {@link warnDev}).
231
+ */
232
+ warnMissingFormat(t, e, s) {
233
+ this.warnDev(`Not found '${e}' ${t} format in '${s}' locale. Falling back to default Intl options.`);
130
234
  }
131
235
  // --- Public methods (implemented in base class) ---
132
236
  /**
@@ -135,8 +239,8 @@ class N {
135
239
  t(t, e, s, r) {
136
240
  if (!t) return "";
137
241
  this.touch();
138
- const n = this.resolveLookup(t, r);
139
- return n == null ? (this.warnMissing(t, r), s === void 0 ? t : s || t) : typeof n != "string" || !e ? n : v(n, e);
242
+ const a = this.resolveLookup(t, r);
243
+ return a == null ? (this.warnMissing(t, r), s === void 0 ? t : s || t) : typeof a != "string" || !e ? a : T(a, e);
140
244
  }
141
245
  /**
142
246
  * Get translation as string
@@ -149,23 +253,21 @@ class N {
149
253
  */
150
254
  tc(t, e, s) {
151
255
  this.touch();
152
- const { count: r, ...n } = typeof e == "number" ? { count: e } : e;
256
+ const { count: r, ...a } = typeof e == "number" ? { count: e } : e;
153
257
  if (r === void 0)
154
258
  return s ?? t;
155
- const i = (u, h, l) => this.t(u, h, l);
156
- return this.pluralFunc(t, Number.parseInt(r.toString(), 10), n, this.getLocale(), i) ?? s ?? t;
259
+ const n = (h, u, c) => this.t(h, u, c);
260
+ return this.pluralFunc(t, Number.parseInt(r.toString(), 10), a, this.getLocale(), n) ?? s ?? t;
157
261
  }
158
- /**
159
- * Format number
160
- */
161
- tn(t, e) {
162
- return this.touch(), this.formatter.formatNumber(t, this.getLocale(), e);
262
+ tn(t, e, s, r) {
263
+ this.touch();
264
+ const a = this.resolveNumberFormatArgs(e, s, r);
265
+ return this.formatter.formatNumber(t, a.locale, a.options);
163
266
  }
164
- /**
165
- * Format date
166
- */
167
- td(t, e) {
168
- return this.touch(), this.formatter.formatDate(t, this.getLocale(), e);
267
+ td(t, e, s, r) {
268
+ this.touch();
269
+ const a = this.resolveDateTimeFormatArgs(e, s, r);
270
+ return this.formatter.formatDate(t, a.locale, a.options);
169
271
  }
170
272
  /**
171
273
  * Format relative time
@@ -180,10 +282,40 @@ class N {
180
282
  return this.touch(), this.resolveHas(t, e);
181
283
  }
182
284
  /**
183
- * Clear cache
285
+ * Replace the value at `key` — a subtree, a string, a number, anything.
286
+ *
287
+ * `set` and not `merge`: `setTranslation('aaa', { fff: 'ggg' })` leaves `aaa` holding only
288
+ * `fff`, and `setTranslation('aaa', 'fff')` leaves a string where the subtree was. Use
289
+ * `mergeTranslations` when the existing siblings should survive.
290
+ *
291
+ * Writes to the active locale and route, so the change is visible to `t()` immediately and
292
+ * lives exactly as long as the chunk it belongs to.
293
+ */
294
+ setTranslation(t, e) {
295
+ const s = this.getLocale(), r = this.getRoute(), a = this.helper.getCache(s, r) ?? {};
296
+ this.helper.setTranslations(s, v(a, String(t), e), r), this.onTranslationsChanged();
297
+ }
298
+ /**
299
+ * Clear translation + formatter caches
184
300
  */
185
301
  clearCache() {
186
- this.helper.clearCache();
302
+ this.helper.clearCache(), this.formatter.clearCache();
303
+ }
304
+ resolveNumberFormatArgs(t, e, s) {
305
+ if (typeof t != "string")
306
+ return { locale: this.getLocale(), options: t };
307
+ let r = this.getLocale(), a;
308
+ typeof e == "string" ? (r = e, a = s) : a = e;
309
+ const n = this.formatter.resolveNumberFormat(r, t);
310
+ return n || this.warnMissingFormat("number", t, r), !n && !a ? { locale: r, options: void 0 } : { locale: r, options: n ? { ...n, ...a } : a };
311
+ }
312
+ resolveDateTimeFormatArgs(t, e, s) {
313
+ if (typeof t != "string")
314
+ return { locale: this.getLocale(), options: t };
315
+ let r = this.getLocale(), a;
316
+ typeof e == "string" ? (r = e, a = s) : a = e;
317
+ const n = this.formatter.resolveDateTimeFormat(r, t);
318
+ return n || this.warnMissingFormat("datetime", t, r), !n && !a ? { locale: r, options: void 0 } : { locale: r, options: n ? { ...n, ...a } : a };
187
319
  }
188
320
  // --- Public methods (for subclasses to use) ---
189
321
  /**
@@ -201,20 +333,57 @@ class N {
201
333
  r ? this.helper.mergeTranslation(t, e, s, !0) : this.helper.loadPageTranslations(t, e, s);
202
334
  }
203
335
  }
336
+ function L(o) {
337
+ let t = o.locale, e = o.fallbackLocale ?? o.locale, s = o.route ?? "index", r = 0;
338
+ const a = /* @__PURE__ */ new Set(), n = () => {
339
+ r++, a.forEach((i) => i());
340
+ };
341
+ return {
342
+ subscribe(i) {
343
+ return a.add(i), () => a.delete(i);
344
+ },
345
+ getSnapshot() {
346
+ return `${t}:${s}:${r}`;
347
+ },
348
+ getLocale() {
349
+ return t;
350
+ },
351
+ setLocale(i) {
352
+ t !== i && (t = i, n());
353
+ },
354
+ getFallbackLocale() {
355
+ return e;
356
+ },
357
+ setFallbackLocale(i) {
358
+ e !== i && (e = i, n());
359
+ },
360
+ getRoute() {
361
+ return s;
362
+ },
363
+ setRoute(i) {
364
+ s !== i && (s = i, n());
365
+ },
366
+ notify: n
367
+ };
368
+ }
204
369
  export {
205
- N as BaseI18n,
206
- g as FormatService,
207
- d as defaultPlural,
208
- m as getByPath,
209
- w as hasTranslationValue,
210
- v as interpolate,
211
- x as isNoPrefixStrategy,
212
- S as isPrefixAndDefaultStrategy,
213
- D as isPrefixExceptDefaultStrategy,
370
+ w as BaseI18n,
371
+ f as FormatService,
372
+ x as collectTranslationPaths,
373
+ L as createReactiveI18nStore,
374
+ b as defaultPlural,
375
+ d as getByPath,
376
+ y as hasTranslationValue,
377
+ T as interpolate,
378
+ R as isNoPrefixStrategy,
379
+ M as isPrefixAndDefaultStrategy,
380
+ $ as isPrefixExceptDefaultStrategy,
214
381
  H as isPrefixStrategy,
215
- L as mergeTranslationChunk,
216
- f as resolveTranslation,
217
- a as translationCacheKey,
218
- T as useTranslationHelper,
219
- M as withPrefixStrategy
382
+ P as mergeTranslationChunk,
383
+ F as mergeTranslationLayers,
384
+ g as resolveTranslation,
385
+ v as setTranslationAtKey,
386
+ l as translationCacheKey,
387
+ p as useTranslationHelper,
388
+ k as withPrefixStrategy
220
389
  };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared reactive state for framework i18n adapters (subscribe / getSnapshot / notify).
3
+ */
4
+ export interface ReactiveI18nStoreOptions {
5
+ locale: string;
6
+ fallbackLocale?: string;
7
+ route?: string;
8
+ }
9
+ export interface ReactiveI18nStore {
10
+ subscribe(listener: () => void): () => void;
11
+ getSnapshot(): string;
12
+ getLocale(): string;
13
+ setLocale(locale: string): void;
14
+ getFallbackLocale(): string;
15
+ setFallbackLocale(locale: string): void;
16
+ getRoute(): string;
17
+ setRoute(routeName: string): void;
18
+ notify(): void;
19
+ }
20
+ export declare function createReactiveI18nStore(options: ReactiveI18nStoreOptions): ReactiveI18nStore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i18n-micro/core",
3
- "version": "1.3.3",
3
+ "version": "1.3.8",
4
4
  "description": "Core utilities for translations, formatting, and locale routing in Nuxt I18n Micro.",
5
5
  "keywords": [
6
6
  "formatting",
@@ -70,13 +70,13 @@
70
70
  "access": "public"
71
71
  },
72
72
  "dependencies": {
73
- "@i18n-micro/types": "1.2.6"
73
+ "@i18n-micro/types": "1.2.7"
74
74
  },
75
75
  "devDependencies": {
76
76
  "publint": "^0.3.17",
77
- "vite": "^7.3.1",
77
+ "vite": "^7.3.6",
78
78
  "vite-plugin-dts": "^4.5.4",
79
- "vitest": "^3.2.4"
79
+ "vitest": "^4.1.10"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=18"
@@ -84,8 +84,8 @@
84
84
  "scripts": {
85
85
  "build": "vite build",
86
86
  "check:package": "publint",
87
- "test": "jest",
88
- "test:perf": "jest --config jest.perf.config.cjs",
87
+ "test": "vitest run",
88
+ "test:perf": "vitest run --config vitest.perf.config.ts",
89
89
  "test:dist": "vitest run --config vitest.dist.config.ts"
90
90
  }
91
91
  }