@i18n-micro/core 1.1.3 → 1.2.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.md +0 -4
- package/dist/base.d.ts +1 -11
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +93 -107
- package/dist/translation.d.ts +3 -5
- package/package.json +2 -2
- package/src/base.ts +6 -29
- package/src/translation.ts +22 -31
- package/tests/base.test.ts +57 -73
- package/tests/core.test.ts +3 -3
package/README.md
CHANGED
|
@@ -93,10 +93,6 @@ console.log(localizedRoute) // Localized route object
|
|
|
93
93
|
Sets the cache for a specific route and locale.
|
|
94
94
|
- **`mergeTranslation(locale: string, routeName: string, newTranslations: Translations, force = false): void`**:
|
|
95
95
|
Merges new translations into the cache for a specific route and locale.
|
|
96
|
-
- **`mergeGlobalTranslation(locale: string, newTranslations: Translations, force = false): void`**:
|
|
97
|
-
Merges new translations into the global cache for a specific locale.
|
|
98
|
-
- **`hasGeneralTranslation(locale: string): boolean`**:
|
|
99
|
-
Checks if global translations are loaded for the specified locale.
|
|
100
96
|
- **`hasPageTranslation(locale: string, routeName: string): boolean`**:
|
|
101
97
|
Checks if translations for a specific route and locale are loaded.
|
|
102
98
|
- **`hasTranslation(locale: string, key: string): boolean`**:
|
package/dist/base.d.ts
CHANGED
|
@@ -6,12 +6,7 @@ export interface BaseI18nOptions {
|
|
|
6
6
|
plural?: PluralFunc;
|
|
7
7
|
missingWarn?: boolean;
|
|
8
8
|
missingHandler?: (locale: string, key: string, routeName: string) => void;
|
|
9
|
-
getPreviousPageInfo?: () => {
|
|
10
|
-
locale: string;
|
|
11
|
-
routeName: string;
|
|
12
|
-
} | null;
|
|
13
9
|
getCustomMissingHandler?: () => MissingHandler | null;
|
|
14
|
-
enablePreviousPageFallback?: boolean;
|
|
15
10
|
}
|
|
16
11
|
/**
|
|
17
12
|
* Abstract base class for i18n adapters
|
|
@@ -25,12 +20,7 @@ export declare abstract class BaseI18n {
|
|
|
25
20
|
pluralFunc: PluralFunc;
|
|
26
21
|
missingWarn: boolean;
|
|
27
22
|
missingHandler?: (locale: string, key: string, routeName: string) => void;
|
|
28
|
-
getPreviousPageInfo?: () => {
|
|
29
|
-
locale: string;
|
|
30
|
-
routeName: string;
|
|
31
|
-
} | null;
|
|
32
23
|
getCustomMissingHandler?: () => MissingHandler | null;
|
|
33
|
-
enablePreviousPageFallback: boolean;
|
|
34
24
|
constructor(options?: BaseI18nOptions);
|
|
35
25
|
/**
|
|
36
26
|
* Get current locale
|
|
@@ -82,7 +72,7 @@ export declare abstract class BaseI18n {
|
|
|
82
72
|
* Core translation loading logic (without reactivity)
|
|
83
73
|
* Subclasses can override addTranslations/addRouteTranslations to add reactivity
|
|
84
74
|
*/
|
|
85
|
-
loadTranslationsCore(locale: string, translations: Translations, merge: boolean): void;
|
|
75
|
+
loadTranslationsCore(locale: string, translations: Translations, merge: boolean, routeName?: string): void;
|
|
86
76
|
/**
|
|
87
77
|
* Core route translation loading logic (without reactivity)
|
|
88
78
|
* Subclasses can override addRouteTranslations to add reactivity
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class f{formatNumber(t,e,n){return new Intl.NumberFormat(e,n).format(t)}formatDate(t,e,n){const r=new Date(t);return Number.isNaN(r.getTime())?"Invalid Date":new Intl.DateTimeFormat(e,n).format(r)}formatRelativeTime(t,e,n){const r=new Date(t);if(Number.isNaN(r.getTime()))return new Intl.RelativeTimeFormat(e,n).format(0,"second");const i=Math.floor((new Date().getTime()-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:l,seconds:c}of o){const u=Math.floor(i/c);if(u>=1)return new Intl.RelativeTimeFormat(e,n).format(-u,l)}return new Intl.RelativeTimeFormat(e,n).format(0,"second")}}const p=/\{(\w+)\}/g;function h(s,t){return t?s.replace(p,(e,n)=>{const r=t[n];return r!==void 0?String(r):`{${n}}`}):s}function x(s){return s==="prefix"||s==="prefix_and_default"}function T(s){return s==="no_prefix"}function v(s){return s==="prefix"}function $(s){return s==="prefix_except_default"}function S(s){return s==="prefix_and_default"}const m=(s,t,e,n,r)=>{const a=r(s,e);if(!a)return null;const i=a.toString().split("|");if(i.length===0)return null;const o=t<i.length?i[t]:i[i.length-1];return o?o.trim().replace("{count}",t.toString()):null};function g(s,t){if(!s||typeof t!="string")return null;if(t in s){const r=s[t];return r}const e=t.split(".");let n=s;for(const r of e)if(n&&typeof n=="object"&&r in n)n=n[r];else return null;return n??null}function d(s){const t=s?.translations??new Map;return{hasCache(e,n){const r=n||"index";return t.has(`${e}:${r}`)},getCache(e,n){const r=n||"index";return t.get(`${e}:${r}`)},setCache(e,n,r){},hasTranslation(e,n){for(const[r,a]of t)if(r.startsWith(`${e}:`)&&g(a,n)!==null)return!0;return!1},hasPageTranslation(e,n){const r=n||"index";return t.has(`${e}:${r}`)},getTranslation(e,n,r){const a=n||"index";return g(t.get(`${e}:${a}`),r)},loadTranslations(e,n,r="index"){const i=`${e}:${r||"index"}`,o=t.get(i)??{};t.set(i,{...o,...n})},setTranslations(e,n,r="index"){const a=r||"index";t.set(`${e}:${a}`,n)},loadPageTranslations(e,n,r){const i=`${e}:${n||"index"}`,o=t.get(i);!o||Object.keys(o).length===0?t.set(i,r):t.set(i,{...o,...r})},mergeTranslation(e,n,r,a=!1){const o=`${e}:${n||"index"}`,l=t.get(o)??{};t.set(o,{...l,...r})},clearCache(){t.clear()}}}class w{constructor(t={}){this.formatter=new f,this.helper=d(t.storage),this.formatter=new f,this.pluralFunc=t.plural||m,this.missingWarn=t.missingWarn??!0,this.missingHandler=t.missingHandler,this.getCustomMissingHandler=t.getCustomMissingHandler}t(t,e,n,r){if(!t)return"";const a=this.getLocale(),i=r||this.getRoute();let o=this.helper.getTranslation(a,i,t);if(!o){const l=this.getFallbackLocale();a!==l&&(o=this.helper.getTranslation(l,i,t))}if(!o){const l=this.getCustomMissingHandler?.();l?l(a,t,i):this.missingHandler?this.missingHandler(a,t,i):this.missingWarn&&process.env.NODE_ENV!=="production"&&typeof window<"u"&&console.warn(`Not found '${t}' key in '${a}' locale messages for route '${i}'.`),o=n===void 0?t:n||t}return typeof o=="string"&&e?h(o,e):o}ts(t,e,n,r){return this.t(t,e,n,r)?.toString()??n??t}tc(t,e,n){const{count:r,...a}=typeof e=="number"?{count:e}:e;if(r===void 0)return n??t;const i=(l,c,u)=>this.t(l,c,u);return this.pluralFunc(t,Number.parseInt(r.toString(),10),a,this.getLocale(),i)??n??t}tn(t,e){return this.formatter.formatNumber(t,this.getLocale(),e)}td(t,e){return this.formatter.formatDate(t,this.getLocale(),e)}tdr(t,e){return this.formatter.formatRelativeTime(t,this.getLocale(),e)}has(t,e){const n=e||this.getRoute(),r=this.getLocale();return!!this.helper.getTranslation(r,n,t)}clearCache(){this.helper.clearCache()}loadTranslationsCore(t,e,n,r="index"){n?this.helper.mergeTranslation(t,r,e,!0):this.helper.setTranslations(t,e,r)}loadRouteTranslationsCore(t,e,n,r){r?this.helper.mergeTranslation(t,e,n,!0):this.helper.loadPageTranslations(t,e,n)}}exports.BaseI18n=w;exports.FormatService=f;exports.defaultPlural=m;exports.interpolate=h;exports.isNoPrefixStrategy=T;exports.isPrefixAndDefaultStrategy=S;exports.isPrefixExceptDefaultStrategy=$;exports.isPrefixStrategy=v;exports.useTranslationHelper=d;exports.withPrefixStrategy=x;
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
class
|
|
2
|
-
formatNumber(
|
|
3
|
-
return new Intl.NumberFormat(
|
|
1
|
+
class f {
|
|
2
|
+
formatNumber(t, e, n) {
|
|
3
|
+
return new Intl.NumberFormat(e, n).format(t);
|
|
4
4
|
}
|
|
5
|
-
formatDate(
|
|
6
|
-
const r = new Date(
|
|
7
|
-
return Number.isNaN(r.getTime()) ? "Invalid Date" : new Intl.DateTimeFormat(
|
|
5
|
+
formatDate(t, e, n) {
|
|
6
|
+
const r = new Date(t);
|
|
7
|
+
return Number.isNaN(r.getTime()) ? "Invalid Date" : new Intl.DateTimeFormat(e, n).format(r);
|
|
8
8
|
}
|
|
9
|
-
formatRelativeTime(
|
|
10
|
-
const r = new Date(
|
|
9
|
+
formatRelativeTime(t, e, n) {
|
|
10
|
+
const r = new Date(t);
|
|
11
11
|
if (Number.isNaN(r.getTime()))
|
|
12
|
-
return new Intl.RelativeTimeFormat(
|
|
12
|
+
return new Intl.RelativeTimeFormat(e, n).format(0, "second");
|
|
13
13
|
const i = Math.floor(((/* @__PURE__ */ new Date()).getTime() - r.getTime()) / 1e3), o = [
|
|
14
14
|
{ unit: "year", seconds: 31536e3 },
|
|
15
15
|
{ unit: "month", seconds: 2592e3 },
|
|
@@ -21,51 +21,51 @@ class g {
|
|
|
21
21
|
for (const { unit: l, seconds: u } of o) {
|
|
22
22
|
const c = Math.floor(i / u);
|
|
23
23
|
if (c >= 1)
|
|
24
|
-
return new Intl.RelativeTimeFormat(
|
|
24
|
+
return new Intl.RelativeTimeFormat(e, n).format(-c, l);
|
|
25
25
|
}
|
|
26
|
-
return new Intl.RelativeTimeFormat(
|
|
26
|
+
return new Intl.RelativeTimeFormat(e, n).format(0, "second");
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
function m(s,
|
|
31
|
-
return
|
|
32
|
-
const r =
|
|
29
|
+
const g = /\{(\w+)\}/g;
|
|
30
|
+
function m(s, t) {
|
|
31
|
+
return t ? s.replace(g, (e, n) => {
|
|
32
|
+
const r = t[n];
|
|
33
33
|
return r !== void 0 ? String(r) : `{${n}}`;
|
|
34
34
|
}) : s;
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function x(s) {
|
|
37
37
|
return s === "prefix" || s === "prefix_and_default";
|
|
38
38
|
}
|
|
39
39
|
function T(s) {
|
|
40
40
|
return s === "no_prefix";
|
|
41
41
|
}
|
|
42
|
-
function
|
|
42
|
+
function $(s) {
|
|
43
43
|
return s === "prefix";
|
|
44
44
|
}
|
|
45
|
-
function
|
|
45
|
+
function v(s) {
|
|
46
46
|
return s === "prefix_except_default";
|
|
47
47
|
}
|
|
48
|
-
function
|
|
48
|
+
function w(s) {
|
|
49
49
|
return s === "prefix_and_default";
|
|
50
50
|
}
|
|
51
|
-
const d = (s,
|
|
52
|
-
const a = r(s,
|
|
51
|
+
const d = (s, t, e, n, r) => {
|
|
52
|
+
const a = r(s, e);
|
|
53
53
|
if (!a)
|
|
54
54
|
return null;
|
|
55
55
|
const i = a.toString().split("|");
|
|
56
56
|
if (i.length === 0) return null;
|
|
57
|
-
const o =
|
|
58
|
-
return o ? o.trim().replace("{count}",
|
|
57
|
+
const o = t < i.length ? i[t] : i[i.length - 1];
|
|
58
|
+
return o ? o.trim().replace("{count}", t.toString()) : null;
|
|
59
59
|
};
|
|
60
|
-
function
|
|
61
|
-
if (!s || typeof
|
|
62
|
-
if (
|
|
63
|
-
const r = s[
|
|
60
|
+
function h(s, t) {
|
|
61
|
+
if (!s || typeof t != "string") return null;
|
|
62
|
+
if (t in s) {
|
|
63
|
+
const r = s[t];
|
|
64
64
|
return r;
|
|
65
65
|
}
|
|
66
|
-
const
|
|
66
|
+
const e = t.split(".");
|
|
67
67
|
let n = s;
|
|
68
|
-
for (const r of
|
|
68
|
+
for (const r of e)
|
|
69
69
|
if (n && typeof n == "object" && r in n)
|
|
70
70
|
n = n[r];
|
|
71
71
|
else
|
|
@@ -73,131 +73,117 @@ function f(s, e) {
|
|
|
73
73
|
return n ?? null;
|
|
74
74
|
}
|
|
75
75
|
function p(s) {
|
|
76
|
-
const
|
|
76
|
+
const t = s?.translations ?? /* @__PURE__ */ new Map();
|
|
77
77
|
return {
|
|
78
|
-
hasCache(
|
|
79
|
-
const r =
|
|
80
|
-
return
|
|
78
|
+
hasCache(e, n) {
|
|
79
|
+
const r = n || "index";
|
|
80
|
+
return t.has(`${e}:${r}`);
|
|
81
81
|
},
|
|
82
|
-
getCache(
|
|
83
|
-
const r =
|
|
84
|
-
return
|
|
82
|
+
getCache(e, n) {
|
|
83
|
+
const r = n || "index";
|
|
84
|
+
return t.get(`${e}:${r}`);
|
|
85
85
|
},
|
|
86
|
-
setCache(
|
|
86
|
+
setCache(e, n, r) {
|
|
87
87
|
},
|
|
88
|
-
hasTranslation(
|
|
89
|
-
for (const [r, a] of
|
|
90
|
-
if (
|
|
88
|
+
hasTranslation(e, n) {
|
|
89
|
+
for (const [r, a] of t)
|
|
90
|
+
if (r.startsWith(`${e}:`) && h(a, n) !== null)
|
|
91
91
|
return !0;
|
|
92
92
|
return !1;
|
|
93
93
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
hasPageTranslation(t, n) {
|
|
98
|
-
return e.has(`${t}:${n}`);
|
|
94
|
+
hasPageTranslation(e, n) {
|
|
95
|
+
const r = n || "index";
|
|
96
|
+
return t.has(`${e}:${r}`);
|
|
99
97
|
},
|
|
100
|
-
getTranslation(
|
|
101
|
-
const a =
|
|
102
|
-
|
|
103
|
-
const l = e.get(t);
|
|
104
|
-
return f(l, r);
|
|
98
|
+
getTranslation(e, n, r) {
|
|
99
|
+
const a = n || "index";
|
|
100
|
+
return h(t.get(`${e}:${a}`), r);
|
|
105
101
|
},
|
|
106
|
-
loadTranslations(
|
|
107
|
-
const r =
|
|
108
|
-
|
|
102
|
+
loadTranslations(e, n, r = "index") {
|
|
103
|
+
const i = `${e}:${r || "index"}`, o = t.get(i) ?? {};
|
|
104
|
+
t.set(i, { ...o, ...n });
|
|
109
105
|
},
|
|
110
|
-
setTranslations(
|
|
111
|
-
|
|
106
|
+
setTranslations(e, n, r = "index") {
|
|
107
|
+
const a = r || "index";
|
|
108
|
+
t.set(`${e}:${a}`, n);
|
|
112
109
|
},
|
|
113
|
-
loadPageTranslations(
|
|
114
|
-
const
|
|
115
|
-
!
|
|
110
|
+
loadPageTranslations(e, n, r) {
|
|
111
|
+
const i = `${e}:${n || "index"}`, o = t.get(i);
|
|
112
|
+
!o || Object.keys(o).length === 0 ? t.set(i, r) : t.set(i, { ...o, ...r });
|
|
116
113
|
},
|
|
117
|
-
mergeTranslation(
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
},
|
|
121
|
-
mergeGlobalTranslation(t, n, r = !1) {
|
|
122
|
-
const a = e.get(t) ?? {};
|
|
123
|
-
e.set(t, { ...a, ...n });
|
|
114
|
+
mergeTranslation(e, n, r, a = !1) {
|
|
115
|
+
const o = `${e}:${n || "index"}`, l = t.get(o) ?? {};
|
|
116
|
+
t.set(o, { ...l, ...r });
|
|
124
117
|
},
|
|
125
118
|
clearCache() {
|
|
126
|
-
|
|
119
|
+
t.clear();
|
|
127
120
|
}
|
|
128
121
|
};
|
|
129
122
|
}
|
|
130
|
-
class
|
|
131
|
-
constructor(
|
|
132
|
-
this.formatter = new
|
|
123
|
+
class _ {
|
|
124
|
+
constructor(t = {}) {
|
|
125
|
+
this.formatter = new f(), this.helper = p(t.storage), this.formatter = new f(), this.pluralFunc = t.plural || d, this.missingWarn = t.missingWarn ?? !0, this.missingHandler = t.missingHandler, this.getCustomMissingHandler = t.getCustomMissingHandler;
|
|
133
126
|
}
|
|
134
127
|
// --- Public methods (implemented in base class) ---
|
|
135
128
|
/**
|
|
136
129
|
* Get translation for a key
|
|
137
130
|
* Based on logic from src/runtime/plugins/01.plugin.ts
|
|
138
131
|
*/
|
|
139
|
-
t(
|
|
140
|
-
if (!
|
|
132
|
+
t(t, e, n, r) {
|
|
133
|
+
if (!t) return "";
|
|
141
134
|
const a = this.getLocale(), i = r || this.getRoute();
|
|
142
|
-
let o = this.helper.getTranslation(a, i,
|
|
143
|
-
if (!o && this.enablePreviousPageFallback && this.getPreviousPageInfo) {
|
|
144
|
-
const l = this.getPreviousPageInfo();
|
|
145
|
-
if (l) {
|
|
146
|
-
const u = this.helper.getTranslation(l.locale, l.routeName, e);
|
|
147
|
-
u && (o = u, process.env.NODE_ENV !== "production" && console.log(`Using fallback translation from previous route: ${l.routeName} -> ${e}`));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
135
|
+
let o = this.helper.getTranslation(a, i, t);
|
|
150
136
|
if (!o) {
|
|
151
137
|
const l = this.getFallbackLocale();
|
|
152
|
-
a !== l && (o = this.helper.getTranslation(l, i,
|
|
138
|
+
a !== l && (o = this.helper.getTranslation(l, i, t));
|
|
153
139
|
}
|
|
154
140
|
if (!o) {
|
|
155
141
|
const l = this.getCustomMissingHandler?.();
|
|
156
|
-
l ? l(a,
|
|
142
|
+
l ? l(a, t, i) : this.missingHandler ? this.missingHandler(a, t, i) : this.missingWarn && process.env.NODE_ENV !== "production" && typeof window < "u" && console.warn(`Not found '${t}' key in '${a}' locale messages for route '${i}'.`), o = n === void 0 ? t : n || t;
|
|
157
143
|
}
|
|
158
|
-
return typeof o == "string" &&
|
|
144
|
+
return typeof o == "string" && e ? m(o, e) : o;
|
|
159
145
|
}
|
|
160
146
|
/**
|
|
161
147
|
* Get translation as string
|
|
162
148
|
*/
|
|
163
|
-
ts(
|
|
164
|
-
return this.t(
|
|
149
|
+
ts(t, e, n, r) {
|
|
150
|
+
return this.t(t, e, n, r)?.toString() ?? n ?? t;
|
|
165
151
|
}
|
|
166
152
|
/**
|
|
167
153
|
* Plural translation
|
|
168
154
|
*/
|
|
169
|
-
tc(
|
|
170
|
-
const { count: r, ...a } = typeof
|
|
155
|
+
tc(t, e, n) {
|
|
156
|
+
const { count: r, ...a } = typeof e == "number" ? { count: e } : e;
|
|
171
157
|
if (r === void 0)
|
|
172
|
-
return n ??
|
|
158
|
+
return n ?? t;
|
|
173
159
|
const i = (l, u, c) => this.t(l, u, c);
|
|
174
|
-
return this.pluralFunc(
|
|
160
|
+
return this.pluralFunc(t, Number.parseInt(r.toString(), 10), a, this.getLocale(), i) ?? n ?? t;
|
|
175
161
|
}
|
|
176
162
|
/**
|
|
177
163
|
* Format number
|
|
178
164
|
*/
|
|
179
|
-
tn(
|
|
180
|
-
return this.formatter.formatNumber(
|
|
165
|
+
tn(t, e) {
|
|
166
|
+
return this.formatter.formatNumber(t, this.getLocale(), e);
|
|
181
167
|
}
|
|
182
168
|
/**
|
|
183
169
|
* Format date
|
|
184
170
|
*/
|
|
185
|
-
td(
|
|
186
|
-
return this.formatter.formatDate(
|
|
171
|
+
td(t, e) {
|
|
172
|
+
return this.formatter.formatDate(t, this.getLocale(), e);
|
|
187
173
|
}
|
|
188
174
|
/**
|
|
189
175
|
* Format relative time
|
|
190
176
|
*/
|
|
191
|
-
tdr(
|
|
192
|
-
return this.formatter.formatRelativeTime(
|
|
177
|
+
tdr(t, e) {
|
|
178
|
+
return this.formatter.formatRelativeTime(t, this.getLocale(), e);
|
|
193
179
|
}
|
|
194
180
|
/**
|
|
195
181
|
* Check if translation exists
|
|
196
182
|
* Based on logic from src/runtime/plugins/01.plugin.ts
|
|
197
183
|
*/
|
|
198
|
-
has(
|
|
199
|
-
const n =
|
|
200
|
-
return !!this.helper.getTranslation(r, n,
|
|
184
|
+
has(t, e) {
|
|
185
|
+
const n = e || this.getRoute(), r = this.getLocale();
|
|
186
|
+
return !!this.helper.getTranslation(r, n, t);
|
|
201
187
|
}
|
|
202
188
|
/**
|
|
203
189
|
* Clear cache
|
|
@@ -210,26 +196,26 @@ class $ {
|
|
|
210
196
|
* Core translation loading logic (without reactivity)
|
|
211
197
|
* Subclasses can override addTranslations/addRouteTranslations to add reactivity
|
|
212
198
|
*/
|
|
213
|
-
loadTranslationsCore(e,
|
|
214
|
-
n ? this.helper.
|
|
199
|
+
loadTranslationsCore(t, e, n, r = "index") {
|
|
200
|
+
n ? this.helper.mergeTranslation(t, r, e, !0) : this.helper.setTranslations(t, e, r);
|
|
215
201
|
}
|
|
216
202
|
/**
|
|
217
203
|
* Core route translation loading logic (without reactivity)
|
|
218
204
|
* Subclasses can override addRouteTranslations to add reactivity
|
|
219
205
|
*/
|
|
220
|
-
loadRouteTranslationsCore(
|
|
221
|
-
r ? this.helper.mergeTranslation(
|
|
206
|
+
loadRouteTranslationsCore(t, e, n, r) {
|
|
207
|
+
r ? this.helper.mergeTranslation(t, e, n, !0) : this.helper.loadPageTranslations(t, e, n);
|
|
222
208
|
}
|
|
223
209
|
}
|
|
224
210
|
export {
|
|
225
|
-
|
|
226
|
-
|
|
211
|
+
_ as BaseI18n,
|
|
212
|
+
f as FormatService,
|
|
227
213
|
d as defaultPlural,
|
|
228
214
|
m as interpolate,
|
|
229
215
|
T as isNoPrefixStrategy,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
216
|
+
w as isPrefixAndDefaultStrategy,
|
|
217
|
+
v as isPrefixExceptDefaultStrategy,
|
|
218
|
+
$ as isPrefixStrategy,
|
|
233
219
|
p as useTranslationHelper,
|
|
234
|
-
|
|
220
|
+
x as withPrefixStrategy
|
|
235
221
|
};
|
package/dist/translation.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Translations } from '@i18n-micro/types';
|
|
2
2
|
/**
|
|
3
3
|
* Bare Metal: Simple translation storage without Ref, useState, devalue.
|
|
4
|
-
* Map key: locale
|
|
4
|
+
* Map key: `${locale}:${routeName}` (page-specific).
|
|
5
5
|
*/
|
|
6
6
|
export interface TranslationStorage {
|
|
7
7
|
translations: Map<string, Translations>;
|
|
@@ -11,13 +11,11 @@ export declare function useTranslationHelper(storage?: TranslationStorage): {
|
|
|
11
11
|
getCache(locale: string, routeName: string): Translations | undefined;
|
|
12
12
|
setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>): void;
|
|
13
13
|
hasTranslation(locale: string, key: string): boolean;
|
|
14
|
-
hasGeneralTranslation(locale: string): boolean;
|
|
15
14
|
hasPageTranslation(locale: string, routeName: string): boolean;
|
|
16
15
|
getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null;
|
|
17
|
-
loadTranslations(locale: string, data: Translations): void;
|
|
18
|
-
setTranslations(locale: string, data: Translations): void;
|
|
16
|
+
loadTranslations(locale: string, data: Translations, routeName?: string): void;
|
|
17
|
+
setTranslations(locale: string, data: Translations, routeName?: string): void;
|
|
19
18
|
loadPageTranslations(locale: string, routeName: string, data: Translations): void;
|
|
20
19
|
mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force?: boolean): void;
|
|
21
|
-
mergeGlobalTranslation(locale: string, newTranslations: Translations, _force?: boolean): void;
|
|
22
20
|
clearCache(): void;
|
|
23
21
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i18n-micro/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": "s00d/nuxt-i18n-micro",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"keywords": [],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@i18n-micro/types": "1.1.
|
|
25
|
+
"@i18n-micro/types": "1.1.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"vite": "^7.3.1",
|
package/src/base.ts
CHANGED
|
@@ -8,10 +8,7 @@ export interface BaseI18nOptions {
|
|
|
8
8
|
plural?: PluralFunc
|
|
9
9
|
missingWarn?: boolean
|
|
10
10
|
missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
11
|
-
// Optional hooks for Nuxt runtime specific features
|
|
12
|
-
getPreviousPageInfo?: () => { locale: string; routeName: string } | null
|
|
13
11
|
getCustomMissingHandler?: () => MissingHandler | null
|
|
14
|
-
enablePreviousPageFallback?: boolean
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/**
|
|
@@ -27,10 +24,7 @@ export abstract class BaseI18n {
|
|
|
27
24
|
public pluralFunc: PluralFunc
|
|
28
25
|
public missingWarn: boolean
|
|
29
26
|
public missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
30
|
-
// Optional hooks for Nuxt runtime specific features
|
|
31
|
-
public getPreviousPageInfo?: () => { locale: string; routeName: string } | null
|
|
32
27
|
public getCustomMissingHandler?: () => MissingHandler | null
|
|
33
|
-
public enablePreviousPageFallback: boolean
|
|
34
28
|
|
|
35
29
|
constructor(options: BaseI18nOptions = {}) {
|
|
36
30
|
this.helper = useTranslationHelper(options.storage)
|
|
@@ -38,9 +32,7 @@ export abstract class BaseI18n {
|
|
|
38
32
|
this.pluralFunc = options.plural || defaultPlural
|
|
39
33
|
this.missingWarn = options.missingWarn ?? true
|
|
40
34
|
this.missingHandler = options.missingHandler
|
|
41
|
-
this.getPreviousPageInfo = options.getPreviousPageInfo
|
|
42
35
|
this.getCustomMissingHandler = options.getCustomMissingHandler
|
|
43
|
-
this.enablePreviousPageFallback = options.enablePreviousPageFallback ?? false
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
// --- Abstract methods (must be implemented by subclasses) ---
|
|
@@ -74,24 +66,9 @@ export abstract class BaseI18n {
|
|
|
74
66
|
const route = routeName || this.getRoute()
|
|
75
67
|
|
|
76
68
|
// 1. Try to find translation in current locale
|
|
77
|
-
// Note: In Nuxt runtime, server already merges global translations, so we don't need explicit fallback
|
|
78
69
|
let value = this.helper.getTranslation<string>(locale, route, key)
|
|
79
70
|
|
|
80
|
-
// 2.
|
|
81
|
-
if (!value && this.enablePreviousPageFallback && this.getPreviousPageInfo) {
|
|
82
|
-
const prev = this.getPreviousPageInfo()
|
|
83
|
-
if (prev) {
|
|
84
|
-
const prevValue = this.helper.getTranslation<string>(prev.locale, prev.routeName, key)
|
|
85
|
-
if (prevValue) {
|
|
86
|
-
value = prevValue
|
|
87
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
88
|
-
console.log(`Using fallback translation from previous route: ${prev.routeName} -> ${key}`)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 3. Fallback to fallbackLocale if not found and different (for non-Nuxt adapters)
|
|
71
|
+
// 2. Fallback to fallbackLocale if not found and different
|
|
95
72
|
if (!value) {
|
|
96
73
|
const fallbackLocale = this.getFallbackLocale()
|
|
97
74
|
if (locale !== fallbackLocale) {
|
|
@@ -99,7 +76,7 @@ export abstract class BaseI18n {
|
|
|
99
76
|
}
|
|
100
77
|
}
|
|
101
78
|
|
|
102
|
-
//
|
|
79
|
+
// 3. Handle missing
|
|
103
80
|
if (!value) {
|
|
104
81
|
// Call custom handler if set (Nuxt runtime), otherwise use instance handler
|
|
105
82
|
const customHandler = this.getCustomMissingHandler?.()
|
|
@@ -117,7 +94,7 @@ export abstract class BaseI18n {
|
|
|
117
94
|
value = defaultValue === undefined ? key : defaultValue || key
|
|
118
95
|
}
|
|
119
96
|
|
|
120
|
-
//
|
|
97
|
+
// 4. Interpolate
|
|
121
98
|
return typeof value === 'string' && params ? interpolate(value, params) : (value as CleanTranslation)
|
|
122
99
|
}
|
|
123
100
|
|
|
@@ -195,11 +172,11 @@ export abstract class BaseI18n {
|
|
|
195
172
|
* Core translation loading logic (without reactivity)
|
|
196
173
|
* Subclasses can override addTranslations/addRouteTranslations to add reactivity
|
|
197
174
|
*/
|
|
198
|
-
public loadTranslationsCore(locale: string, translations: Translations, merge: boolean): void {
|
|
175
|
+
public loadTranslationsCore(locale: string, translations: Translations, merge: boolean, routeName = 'index'): void {
|
|
199
176
|
if (merge) {
|
|
200
|
-
this.helper.
|
|
177
|
+
this.helper.mergeTranslation(locale, routeName, translations, true)
|
|
201
178
|
} else {
|
|
202
|
-
this.helper.setTranslations(locale, translations)
|
|
179
|
+
this.helper.setTranslations(locale, translations, routeName)
|
|
203
180
|
}
|
|
204
181
|
}
|
|
205
182
|
|
package/src/translation.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Translations } from '@i18n-micro/types'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Bare Metal: Simple translation storage without Ref, useState, devalue.
|
|
5
|
-
* Map key: locale
|
|
5
|
+
* Map key: `${locale}:${routeName}` (page-specific).
|
|
6
6
|
*/
|
|
7
7
|
export interface TranslationStorage {
|
|
8
8
|
translations: Map<string, Translations>
|
|
@@ -36,52 +36,46 @@ export function useTranslationHelper(storage?: TranslationStorage) {
|
|
|
36
36
|
|
|
37
37
|
return {
|
|
38
38
|
hasCache(locale: string, page: string) {
|
|
39
|
-
const
|
|
40
|
-
return translations.has(
|
|
39
|
+
const p = page || 'index'
|
|
40
|
+
return translations.has(`${locale}:${p}`)
|
|
41
41
|
},
|
|
42
42
|
getCache(locale: string, routeName: string) {
|
|
43
|
-
const
|
|
44
|
-
return translations.get(
|
|
43
|
+
const rn = routeName || 'index'
|
|
44
|
+
return translations.get(`${locale}:${rn}`)
|
|
45
45
|
},
|
|
46
46
|
setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>) {
|
|
47
47
|
// No-op for bare metal
|
|
48
48
|
},
|
|
49
49
|
hasTranslation(locale: string, key: string): boolean {
|
|
50
50
|
for (const [k, v] of translations) {
|
|
51
|
-
if (
|
|
51
|
+
if (k.startsWith(`${locale}:`) && findValue(v, key) !== null) {
|
|
52
52
|
return true
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
return false
|
|
56
56
|
},
|
|
57
|
-
hasGeneralTranslation(locale: string): boolean {
|
|
58
|
-
return translations.has(locale)
|
|
59
|
-
},
|
|
60
57
|
hasPageTranslation(locale: string, routeName: string): boolean {
|
|
61
|
-
|
|
58
|
+
const rn = routeName || 'index'
|
|
59
|
+
return translations.has(`${locale}:${rn}`)
|
|
62
60
|
},
|
|
63
61
|
getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const val = findValue<T>(routeData, key)
|
|
67
|
-
if (val !== null) return val
|
|
68
|
-
|
|
69
|
-
const generalData = translations.get(locale)
|
|
70
|
-
return findValue<T>(generalData, key)
|
|
62
|
+
const rn = routeName || 'index'
|
|
63
|
+
return findValue<T>(translations.get(`${locale}:${rn}`), key)
|
|
71
64
|
},
|
|
72
|
-
loadTranslations(locale: string, data: Translations): void {
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
translations.
|
|
65
|
+
loadTranslations(locale: string, data: Translations, routeName = 'index'): void {
|
|
66
|
+
const rn = routeName || 'index'
|
|
67
|
+
const key = `${locale}:${rn}`
|
|
68
|
+
const existing = translations.get(key) ?? {}
|
|
69
|
+
translations.set(key, { ...existing, ...data })
|
|
76
70
|
},
|
|
77
|
-
setTranslations(locale: string, data: Translations): void {
|
|
78
|
-
|
|
79
|
-
translations.set(locale
|
|
71
|
+
setTranslations(locale: string, data: Translations, routeName = 'index'): void {
|
|
72
|
+
const rn = routeName || 'index'
|
|
73
|
+
translations.set(`${locale}:${rn}`, data)
|
|
80
74
|
},
|
|
81
75
|
loadPageTranslations(locale: string, routeName: string, data: Translations): void {
|
|
82
|
-
const
|
|
76
|
+
const rn = routeName || 'index'
|
|
77
|
+
const key = `${locale}:${rn}`
|
|
83
78
|
const existing = translations.get(key)
|
|
84
|
-
// Perf: when existing is empty — keep reference, avoid O(n) copying of large objects
|
|
85
79
|
if (!existing || Object.keys(existing).length === 0) {
|
|
86
80
|
translations.set(key, data)
|
|
87
81
|
} else {
|
|
@@ -89,14 +83,11 @@ export function useTranslationHelper(storage?: TranslationStorage) {
|
|
|
89
83
|
}
|
|
90
84
|
},
|
|
91
85
|
mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force = false): void {
|
|
92
|
-
const
|
|
86
|
+
const rn = routeName || 'index'
|
|
87
|
+
const key = `${locale}:${rn}`
|
|
93
88
|
const existing = translations.get(key) ?? {}
|
|
94
89
|
translations.set(key, { ...existing, ...newTranslations })
|
|
95
90
|
},
|
|
96
|
-
mergeGlobalTranslation(locale: string, newTranslations: Translations, _force = false): void {
|
|
97
|
-
const existing = translations.get(locale) ?? {}
|
|
98
|
-
translations.set(locale, { ...existing, ...newTranslations })
|
|
99
|
-
},
|
|
100
91
|
clearCache(): void {
|
|
101
92
|
translations.clear()
|
|
102
93
|
},
|
package/tests/base.test.ts
CHANGED
|
@@ -38,44 +38,44 @@ class TestI18n extends BaseI18n {
|
|
|
38
38
|
describe('BaseI18n', () => {
|
|
39
39
|
describe('Constructor', () => {
|
|
40
40
|
test('should initialize with default options', () => {
|
|
41
|
-
const i18n = new TestI18n('en', 'en', '
|
|
41
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
42
42
|
expect(i18n.getLocale()).toBe('en')
|
|
43
43
|
expect(i18n.getFallbackLocale()).toBe('en')
|
|
44
|
-
expect(i18n.getRoute()).toBe('
|
|
44
|
+
expect(i18n.getRoute()).toBe('index')
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
test('should initialize with custom storage', () => {
|
|
48
48
|
const storage = { translations: new Map<string, Translations>() }
|
|
49
|
-
const i18n = new TestI18n('en', 'en', '
|
|
49
|
+
const i18n = new TestI18n('en', 'en', 'index', { storage })
|
|
50
50
|
expect(i18n).toBeDefined()
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
test('should initialize with custom plural function', () => {
|
|
54
54
|
const customPlural: PluralFunc = () => 'custom'
|
|
55
|
-
const i18n = new TestI18n('en', 'en', '
|
|
55
|
+
const i18n = new TestI18n('en', 'en', 'index', { plural: customPlural })
|
|
56
56
|
expect(i18n).toBeDefined()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
test('should initialize with missingWarn option', () => {
|
|
60
|
-
const i18n = new TestI18n('en', 'en', '
|
|
60
|
+
const i18n = new TestI18n('en', 'en', 'index', { missingWarn: false })
|
|
61
61
|
expect(i18n).toBeDefined()
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
test('should initialize with missingHandler', () => {
|
|
65
65
|
const handler = jest.fn()
|
|
66
|
-
const i18n = new TestI18n('en', 'en', '
|
|
66
|
+
const i18n = new TestI18n('en', 'en', 'index', { missingHandler: handler })
|
|
67
67
|
expect(i18n).toBeDefined()
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
describe('t() method', () => {
|
|
72
72
|
test('should return empty string for empty key', () => {
|
|
73
|
-
const i18n = new TestI18n('en', 'en', '
|
|
73
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
74
74
|
expect(i18n.t('')).toBe('')
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
test('should return translation for existing key', async () => {
|
|
78
|
-
const i18n = new TestI18n('en', 'en', '
|
|
78
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
79
79
|
const translations: Translations = { greeting: 'Hello' }
|
|
80
80
|
i18n['helper'].loadTranslations('en', translations)
|
|
81
81
|
|
|
@@ -84,7 +84,7 @@ describe('BaseI18n', () => {
|
|
|
84
84
|
|
|
85
85
|
test('should interpolate params in translation', async () => {
|
|
86
86
|
const storage = { translations: new Map<string, Translations>() }
|
|
87
|
-
const i18n = new TestI18n('en', 'en', '
|
|
87
|
+
const i18n = new TestI18n('en', 'en', 'index', { storage })
|
|
88
88
|
const translations: Translations = { greeting: 'Hello, {name}!' }
|
|
89
89
|
await i18n['helper'].loadTranslations('en', translations)
|
|
90
90
|
|
|
@@ -92,18 +92,18 @@ describe('BaseI18n', () => {
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
test('should use defaultValue when translation is missing', () => {
|
|
95
|
-
const i18n = new TestI18n('en', 'en', '
|
|
95
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
96
96
|
expect(i18n.t('missing.key', undefined, 'Default value')).toBe('Default value')
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
test('should return key when translation is missing and no defaultValue', () => {
|
|
100
|
-
const i18n = new TestI18n('en', 'en', '
|
|
100
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
101
101
|
expect(i18n.t('missing.key')).toBe('missing.key')
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
test('should fallback to fallbackLocale when translation is missing', async () => {
|
|
105
105
|
const storage = { translations: new Map<string, Translations>() }
|
|
106
|
-
const i18n = new TestI18n('en', 'fr', '
|
|
106
|
+
const i18n = new TestI18n('en', 'fr', 'index', { storage })
|
|
107
107
|
const translations: Translations = { greeting: 'Bonjour' }
|
|
108
108
|
await i18n['helper'].loadTranslations('fr', translations)
|
|
109
109
|
|
|
@@ -111,53 +111,36 @@ describe('BaseI18n', () => {
|
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
test('should use route-specific translation when routeName is provided', async () => {
|
|
114
|
-
const i18n = new TestI18n('en', 'en', '
|
|
114
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
115
115
|
const routeTranslations: Translations = { title: 'Route Title' }
|
|
116
116
|
await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
|
|
117
117
|
|
|
118
118
|
expect(i18n.t('title', undefined, undefined, 'about')).toBe('Route Title')
|
|
119
119
|
})
|
|
120
120
|
|
|
121
|
-
test('should use previousPageInfo fallback when enabled', async () => {
|
|
122
|
-
const storage = { translations: new Map<string, Translations>() }
|
|
123
|
-
const prevInfo = { locale: 'fr', routeName: 'previous' }
|
|
124
|
-
const i18n = new TestI18n('en', 'en', 'general', {
|
|
125
|
-
storage,
|
|
126
|
-
getPreviousPageInfo: () => prevInfo,
|
|
127
|
-
enablePreviousPageFallback: true,
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const translations: Translations = { greeting: 'Bonjour' }
|
|
131
|
-
await i18n['helper'].loadPageTranslations('fr', 'previous', translations)
|
|
132
|
-
|
|
133
|
-
// Current locale is 'en', route is 'general', translation not found
|
|
134
|
-
// Should fallback to previous page info: locale 'fr', route 'previous'
|
|
135
|
-
expect(i18n.t('greeting')).toBe('Bonjour')
|
|
136
|
-
})
|
|
137
|
-
|
|
138
121
|
test('should call missingHandler when translation is missing', () => {
|
|
139
122
|
const handler = jest.fn()
|
|
140
|
-
const i18n = new TestI18n('en', 'en', '
|
|
123
|
+
const i18n = new TestI18n('en', 'en', 'index', { missingHandler: handler })
|
|
141
124
|
|
|
142
125
|
i18n.t('missing.key')
|
|
143
126
|
|
|
144
|
-
expect(handler).toHaveBeenCalledWith('en', 'missing.key', '
|
|
127
|
+
expect(handler).toHaveBeenCalledWith('en', 'missing.key', 'index')
|
|
145
128
|
})
|
|
146
129
|
|
|
147
130
|
test('should call customMissingHandler when set (Nuxt runtime)', () => {
|
|
148
131
|
const customHandler = jest.fn()
|
|
149
|
-
const i18n = new TestI18n('en', 'en', '
|
|
132
|
+
const i18n = new TestI18n('en', 'en', 'index', {
|
|
150
133
|
getCustomMissingHandler: () => customHandler,
|
|
151
134
|
})
|
|
152
135
|
|
|
153
136
|
i18n.t('missing.key')
|
|
154
137
|
|
|
155
|
-
expect(customHandler).toHaveBeenCalledWith('en', 'missing.key', '
|
|
138
|
+
expect(customHandler).toHaveBeenCalledWith('en', 'missing.key', 'index')
|
|
156
139
|
})
|
|
157
140
|
|
|
158
141
|
test('should not warn when missingWarn is false', () => {
|
|
159
142
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
160
|
-
const i18n = new TestI18n('en', 'en', '
|
|
143
|
+
const i18n = new TestI18n('en', 'en', 'index', { missingWarn: false })
|
|
161
144
|
|
|
162
145
|
i18n.t('missing.key')
|
|
163
146
|
|
|
@@ -168,7 +151,7 @@ describe('BaseI18n', () => {
|
|
|
168
151
|
|
|
169
152
|
describe('ts() method', () => {
|
|
170
153
|
test('should return translation as string', async () => {
|
|
171
|
-
const i18n = new TestI18n('en', 'en', '
|
|
154
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
172
155
|
const translations: Translations = { greeting: 'Hello' }
|
|
173
156
|
i18n['helper'].loadTranslations('en', translations)
|
|
174
157
|
|
|
@@ -176,17 +159,17 @@ describe('BaseI18n', () => {
|
|
|
176
159
|
})
|
|
177
160
|
|
|
178
161
|
test('should return defaultValue when translation is missing', () => {
|
|
179
|
-
const i18n = new TestI18n('en', 'en', '
|
|
162
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
180
163
|
expect(i18n.ts('missing.key', undefined, 'Default')).toBe('Default')
|
|
181
164
|
})
|
|
182
165
|
|
|
183
166
|
test('should return key when translation is missing and no defaultValue', () => {
|
|
184
|
-
const i18n = new TestI18n('en', 'en', '
|
|
167
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
185
168
|
expect(i18n.ts('missing.key')).toBe('missing.key')
|
|
186
169
|
})
|
|
187
170
|
|
|
188
171
|
test('should convert non-string values to string', async () => {
|
|
189
|
-
const i18n = new TestI18n('en', 'en', '
|
|
172
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
190
173
|
const translations: Translations = { count: 42 }
|
|
191
174
|
i18n['helper'].loadTranslations('en', translations)
|
|
192
175
|
|
|
@@ -196,12 +179,12 @@ describe('BaseI18n', () => {
|
|
|
196
179
|
|
|
197
180
|
describe('tc() method', () => {
|
|
198
181
|
test('should return defaultValue when count is undefined', () => {
|
|
199
|
-
const i18n = new TestI18n('en', 'en', '
|
|
182
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
200
183
|
expect(i18n.tc('apples', { other: 'params' }, 'No count')).toBe('No count')
|
|
201
184
|
})
|
|
202
185
|
|
|
203
186
|
test('should use plural function with count', async () => {
|
|
204
|
-
const i18n = new TestI18n('en', 'en', '
|
|
187
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
205
188
|
const translations: Translations = { apples: 'apple|apples' }
|
|
206
189
|
await i18n['helper'].loadTranslations('en', translations)
|
|
207
190
|
|
|
@@ -213,7 +196,7 @@ describe('BaseI18n', () => {
|
|
|
213
196
|
})
|
|
214
197
|
|
|
215
198
|
test('should handle count as number', async () => {
|
|
216
|
-
const i18n = new TestI18n('en', 'en', '
|
|
199
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
217
200
|
const translations: Translations = { apples: 'apple|apples' }
|
|
218
201
|
i18n['helper'].loadTranslations('en', translations)
|
|
219
202
|
|
|
@@ -221,7 +204,7 @@ describe('BaseI18n', () => {
|
|
|
221
204
|
})
|
|
222
205
|
|
|
223
206
|
test('should handle count as Params object', async () => {
|
|
224
|
-
const i18n = new TestI18n('en', 'en', '
|
|
207
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
225
208
|
const translations: Translations = { apples: 'apple|apples' }
|
|
226
209
|
i18n['helper'].loadTranslations('en', translations)
|
|
227
210
|
|
|
@@ -229,7 +212,7 @@ describe('BaseI18n', () => {
|
|
|
229
212
|
})
|
|
230
213
|
|
|
231
214
|
test('should return defaultValue when plural function returns null', () => {
|
|
232
|
-
const i18n = new TestI18n('en', 'en', '
|
|
215
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
233
216
|
// When translation is missing, t() returns key, which is passed to pluralFunc
|
|
234
217
|
// pluralFunc tries to process 'missing.key' as translation, but since it doesn't contain '|',
|
|
235
218
|
// it returns the key itself. So tc returns the key, not defaultValue.
|
|
@@ -241,19 +224,19 @@ describe('BaseI18n', () => {
|
|
|
241
224
|
|
|
242
225
|
describe('tn() method', () => {
|
|
243
226
|
test('should format number with default locale', () => {
|
|
244
|
-
const i18n = new TestI18n('en', 'en', '
|
|
227
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
245
228
|
const result = i18n.tn(1234.56)
|
|
246
229
|
expect(result).toMatch(/1[,.]234[.,]56/)
|
|
247
230
|
})
|
|
248
231
|
|
|
249
232
|
test('should format number with custom options', () => {
|
|
250
|
-
const i18n = new TestI18n('en', 'en', '
|
|
233
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
251
234
|
const result = i18n.tn(1234.56, { style: 'currency', currency: 'USD' })
|
|
252
235
|
expect(result).toContain('1,234.56')
|
|
253
236
|
})
|
|
254
237
|
|
|
255
238
|
test('should use current locale for formatting', () => {
|
|
256
|
-
const i18n = new TestI18n('ru', 'en', '
|
|
239
|
+
const i18n = new TestI18n('ru', 'en', 'index')
|
|
257
240
|
const result = i18n.tn(1234.56)
|
|
258
241
|
// Russian locale uses different number formatting
|
|
259
242
|
expect(result).toBeDefined()
|
|
@@ -262,7 +245,7 @@ describe('BaseI18n', () => {
|
|
|
262
245
|
|
|
263
246
|
describe('td() method', () => {
|
|
264
247
|
test('should format date with default locale', () => {
|
|
265
|
-
const i18n = new TestI18n('en', 'en', '
|
|
248
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
266
249
|
const date = new Date('2024-01-15')
|
|
267
250
|
const result = i18n.td(date)
|
|
268
251
|
expect(result).toBeDefined()
|
|
@@ -270,7 +253,7 @@ describe('BaseI18n', () => {
|
|
|
270
253
|
})
|
|
271
254
|
|
|
272
255
|
test('should format date with custom options', () => {
|
|
273
|
-
const i18n = new TestI18n('en', 'en', '
|
|
256
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
274
257
|
const date = new Date('2024-01-15')
|
|
275
258
|
const result = i18n.td(date, { year: 'numeric', month: 'long', day: 'numeric' })
|
|
276
259
|
expect(result).toContain('2024')
|
|
@@ -278,7 +261,7 @@ describe('BaseI18n', () => {
|
|
|
278
261
|
})
|
|
279
262
|
|
|
280
263
|
test('should handle date as number (timestamp)', () => {
|
|
281
|
-
const i18n = new TestI18n('en', 'en', '
|
|
264
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
282
265
|
const timestamp = new Date('2024-01-15').getTime()
|
|
283
266
|
const result = i18n.td(timestamp)
|
|
284
267
|
expect(result).toBeDefined()
|
|
@@ -286,7 +269,7 @@ describe('BaseI18n', () => {
|
|
|
286
269
|
})
|
|
287
270
|
|
|
288
271
|
test('should handle date as string', () => {
|
|
289
|
-
const i18n = new TestI18n('en', 'en', '
|
|
272
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
290
273
|
const result = i18n.td('2024-01-15')
|
|
291
274
|
expect(result).toBeDefined()
|
|
292
275
|
expect(result).not.toBe('Invalid Date')
|
|
@@ -295,7 +278,7 @@ describe('BaseI18n', () => {
|
|
|
295
278
|
|
|
296
279
|
describe('tdr() method', () => {
|
|
297
280
|
test('should format relative time', () => {
|
|
298
|
-
const i18n = new TestI18n('en', 'en', '
|
|
281
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
299
282
|
const yesterday = new Date(Date.now() - 86400000)
|
|
300
283
|
const result = i18n.tdr(yesterday)
|
|
301
284
|
expect(result).toBeDefined()
|
|
@@ -303,14 +286,14 @@ describe('BaseI18n', () => {
|
|
|
303
286
|
})
|
|
304
287
|
|
|
305
288
|
test('should format relative time with custom options', () => {
|
|
306
|
-
const i18n = new TestI18n('en', 'en', '
|
|
289
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
307
290
|
const yesterday = new Date(Date.now() - 86400000)
|
|
308
291
|
const result = i18n.tdr(yesterday, { numeric: 'always' })
|
|
309
292
|
expect(result).toBeDefined()
|
|
310
293
|
})
|
|
311
294
|
|
|
312
295
|
test('should handle invalid date gracefully', () => {
|
|
313
|
-
const i18n = new TestI18n('en', 'en', '
|
|
296
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
314
297
|
const invalidDate = new Date('invalid')
|
|
315
298
|
const result = i18n.tdr(invalidDate)
|
|
316
299
|
expect(result).toBeDefined()
|
|
@@ -319,7 +302,7 @@ describe('BaseI18n', () => {
|
|
|
319
302
|
|
|
320
303
|
describe('has() method', () => {
|
|
321
304
|
test('should return true when translation exists', async () => {
|
|
322
|
-
const i18n = new TestI18n('en', 'en', '
|
|
305
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
323
306
|
const translations: Translations = { greeting: 'Hello' }
|
|
324
307
|
i18n['helper'].loadTranslations('en', translations)
|
|
325
308
|
|
|
@@ -327,17 +310,17 @@ describe('BaseI18n', () => {
|
|
|
327
310
|
})
|
|
328
311
|
|
|
329
312
|
test('should return false when translation does not exist', () => {
|
|
330
|
-
const i18n = new TestI18n('en', 'en', '
|
|
313
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
331
314
|
expect(i18n.has('missing.key')).toBe(false)
|
|
332
315
|
})
|
|
333
316
|
|
|
334
317
|
test('should check route-specific translation when routeName is provided', async () => {
|
|
335
|
-
const i18n = new TestI18n('en', 'en', '
|
|
318
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
336
319
|
const routeTranslations: Translations = { title: 'Route Title' }
|
|
337
320
|
await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
|
|
338
321
|
|
|
339
322
|
expect(i18n.has('title', 'about')).toBe(true)
|
|
340
|
-
expect(i18n.has('title', '
|
|
323
|
+
expect(i18n.has('title', 'index')).toBe(false)
|
|
341
324
|
})
|
|
342
325
|
|
|
343
326
|
test('should use current route when routeName is not provided', async () => {
|
|
@@ -351,7 +334,7 @@ describe('BaseI18n', () => {
|
|
|
351
334
|
|
|
352
335
|
describe('clearCache() method', () => {
|
|
353
336
|
test('should clear all translations from cache', async () => {
|
|
354
|
-
const i18n = new TestI18n('en', 'en', '
|
|
337
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
355
338
|
const translations: Translations = { greeting: 'Hello' }
|
|
356
339
|
i18n['helper'].loadTranslations('en', translations)
|
|
357
340
|
|
|
@@ -365,7 +348,7 @@ describe('BaseI18n', () => {
|
|
|
365
348
|
|
|
366
349
|
describe('loadTranslationsCore() method', () => {
|
|
367
350
|
test('should load translations when merge is false', async () => {
|
|
368
|
-
const i18n = new TestI18n('en', 'en', '
|
|
351
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
369
352
|
const translations: Translations = { greeting: 'Hello' }
|
|
370
353
|
|
|
371
354
|
i18n['loadTranslationsCore']('en', translations, false)
|
|
@@ -376,7 +359,7 @@ describe('BaseI18n', () => {
|
|
|
376
359
|
})
|
|
377
360
|
|
|
378
361
|
test('should merge translations when merge is true', async () => {
|
|
379
|
-
const i18n = new TestI18n('en', 'en', '
|
|
362
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
380
363
|
const initial: Translations = { greeting: 'Hello' }
|
|
381
364
|
const additional: Translations = { farewell: 'Goodbye' }
|
|
382
365
|
|
|
@@ -392,7 +375,7 @@ describe('BaseI18n', () => {
|
|
|
392
375
|
|
|
393
376
|
describe('loadRouteTranslationsCore() method', () => {
|
|
394
377
|
test('should load route translations when merge is false', async () => {
|
|
395
|
-
const i18n = new TestI18n('en', 'en', '
|
|
378
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
396
379
|
const translations: Translations = { title: 'Page Title' }
|
|
397
380
|
|
|
398
381
|
i18n['loadRouteTranslationsCore']('en', 'about', translations, false)
|
|
@@ -403,7 +386,7 @@ describe('BaseI18n', () => {
|
|
|
403
386
|
})
|
|
404
387
|
|
|
405
388
|
test('should merge route translations when merge is true', async () => {
|
|
406
|
-
const i18n = new TestI18n('en', 'en', '
|
|
389
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
407
390
|
const initial: Translations = { title: 'Page Title' }
|
|
408
391
|
const additional: Translations = { description: 'Page Description' }
|
|
409
392
|
|
|
@@ -419,7 +402,7 @@ describe('BaseI18n', () => {
|
|
|
419
402
|
|
|
420
403
|
describe('Edge cases', () => {
|
|
421
404
|
test('should handle nested translation keys', async () => {
|
|
422
|
-
const i18n = new TestI18n('en', 'en', '
|
|
405
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
423
406
|
const translations: Translations = {
|
|
424
407
|
nested: {
|
|
425
408
|
deep: {
|
|
@@ -433,7 +416,7 @@ describe('BaseI18n', () => {
|
|
|
433
416
|
})
|
|
434
417
|
|
|
435
418
|
test('should handle multiple params in interpolation', async () => {
|
|
436
|
-
const i18n = new TestI18n('en', 'en', '
|
|
419
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
437
420
|
const translations: Translations = {
|
|
438
421
|
message: 'Hello, {name}! You are {age} years old.',
|
|
439
422
|
}
|
|
@@ -443,23 +426,25 @@ describe('BaseI18n', () => {
|
|
|
443
426
|
})
|
|
444
427
|
|
|
445
428
|
test('should handle null defaultValue', () => {
|
|
446
|
-
const i18n = new TestI18n('en', 'en', '
|
|
429
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
447
430
|
expect(i18n.t('missing.key', undefined, null)).toBe('missing.key')
|
|
448
431
|
})
|
|
449
432
|
|
|
450
433
|
test('should handle empty string defaultValue', () => {
|
|
451
|
-
const i18n = new TestI18n('en', 'en', '
|
|
434
|
+
const i18n = new TestI18n('en', 'en', 'index')
|
|
452
435
|
// Empty string is falsy, so it will fall back to key (as per defaultValue || key logic)
|
|
453
436
|
expect(i18n.t('missing.key', undefined, '')).toBe('missing.key')
|
|
454
437
|
})
|
|
455
438
|
|
|
456
439
|
test('should handle route change', async () => {
|
|
457
440
|
const storage = { translations: new Map<string, Translations>() }
|
|
458
|
-
const i18n = new TestI18n('en', 'en', '
|
|
459
|
-
const
|
|
460
|
-
|
|
441
|
+
const i18n = new TestI18n('en', 'en', 'index', { storage })
|
|
442
|
+
const rootTranslations: Translations = { greeting: 'Hello' }
|
|
443
|
+
// Packages (vue, node, etc.) merge root into pages automatically.
|
|
444
|
+
// Core does not — so we simulate pre-merged data here.
|
|
445
|
+
const routeTranslations: Translations = { greeting: 'Hello', title: 'About Page' }
|
|
461
446
|
|
|
462
|
-
await i18n['helper'].loadTranslations('en',
|
|
447
|
+
await i18n['helper'].loadTranslations('en', rootTranslations)
|
|
463
448
|
await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
|
|
464
449
|
|
|
465
450
|
// Verify translations are loaded
|
|
@@ -467,14 +452,13 @@ describe('BaseI18n', () => {
|
|
|
467
452
|
expect(i18n.has('title', 'about')).toBe(true)
|
|
468
453
|
|
|
469
454
|
// Check route-specific translation with explicit routeName
|
|
470
|
-
// Note: getTranslation uses routeName parameter, so this should work
|
|
471
455
|
const titleValue = i18n.t('title', undefined, undefined, 'about')
|
|
472
456
|
expect(titleValue).toBe('About Page')
|
|
473
457
|
|
|
474
458
|
// Change route and check
|
|
475
459
|
i18n.setRoute('about')
|
|
476
460
|
expect(i18n.t('title')).toBe('About Page')
|
|
477
|
-
expect(i18n.t('greeting')).toBe('Hello')
|
|
461
|
+
expect(i18n.t('greeting')).toBe('Hello')
|
|
478
462
|
})
|
|
479
463
|
})
|
|
480
464
|
})
|
package/tests/core.test.ts
CHANGED
|
@@ -63,12 +63,12 @@ describe('Translation Helper', () => {
|
|
|
63
63
|
expect(helper.getTranslation('en', 'home', 'newKey')).toBe('New value')
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
test('
|
|
66
|
+
test('mergeTranslation with index routeName updates index translations', () => {
|
|
67
67
|
const helper = useTranslationHelper()
|
|
68
68
|
helper.loadTranslations('en', translations.en)
|
|
69
69
|
|
|
70
|
-
helper.
|
|
71
|
-
expect(helper.getTranslation('en', 'index', '
|
|
70
|
+
helper.mergeTranslation('en', 'index', { newKey: 'New value' })
|
|
71
|
+
expect(helper.getTranslation('en', 'index', 'newKey')).toBe('New value')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('deepClone creates a deep copy of objects', () => {
|