nuxt-i18n-micro 3.30.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/dist/client/200.html +1 -1
- package/dist/client/404.html +1 -1
- package/dist/client/_nuxt/BVna_KKx.js +3 -0
- package/dist/client/_nuxt/Bw81RhAv.js +1 -0
- package/dist/client/_nuxt/CPz_jhsD.js +1 -0
- package/dist/client/_nuxt/CmvObVGq.js +1 -0
- package/dist/client/_nuxt/DKUOBR6F.js +5 -0
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/f0c8d4bf-40d2-4915-8b4a-ebe46bde61c8.json +1 -0
- package/dist/client/_nuxt/{entry.BM-mexAv.css → entry.PSWtHhay.css} +1 -1
- package/dist/client/_nuxt/error-404.xs3fxhVJ.css +1 -0
- package/dist/client/_nuxt/error-500.C2S-4iLg.css +1 -0
- package/dist/client/index.html +1 -1
- package/dist/module.d.mts +14 -5
- package/dist/module.json +4 -1
- package/dist/module.mjs +850 -475
- package/dist/runtime/components/i18n-group.d.vue.ts +17 -9
- package/dist/runtime/components/i18n-group.vue +2 -4
- package/dist/runtime/components/i18n-group.vue.d.ts +17 -9
- package/dist/runtime/components/i18n-link.d.vue.ts +9 -6
- package/dist/runtime/components/i18n-link.vue +7 -28
- package/dist/runtime/components/i18n-link.vue.d.ts +9 -6
- package/dist/runtime/components/i18n-switcher.d.vue.ts +128 -54
- package/dist/runtime/components/i18n-switcher.vue +56 -156
- package/dist/runtime/components/i18n-switcher.vue.d.ts +128 -54
- package/dist/runtime/components/i18n-t.d.vue.ts +59 -116
- package/dist/runtime/components/i18n-t.vue +24 -71
- package/dist/runtime/components/i18n-t.vue.d.ts +59 -116
- package/dist/runtime/composables/useI18n.js +2 -38
- package/dist/runtime/composables/useI18nHead.js +21 -6
- package/dist/runtime/composables/useI18nLocale.d.ts +4 -1
- package/dist/runtime/composables/useI18nLocale.js +20 -22
- package/dist/runtime/composables/useLocaleHead.d.ts +26 -31
- package/dist/runtime/composables/useLocaleHead.js +47 -167
- package/dist/runtime/devtools/bridge.d.ts +2 -2
- package/dist/runtime/devtools/vue-devtools.d.ts +2 -2
- package/dist/runtime/devtools/vue-devtools.js +5 -5
- package/dist/runtime/middleware/i18n-redirect.global.d.ts +3 -2
- package/dist/runtime/middleware/i18n-redirect.global.js +30 -41
- package/dist/runtime/plugins/01.plugin.d.ts +156 -34
- package/dist/runtime/plugins/01.plugin.js +184 -138
- package/dist/runtime/plugins/02.meta.js +36 -43
- package/dist/runtime/plugins/04.vue-devtools.js +1 -1
- package/dist/runtime/plugins/06.redirect.d.ts +3 -3
- package/dist/runtime/plugins/06.redirect.js +65 -91
- package/dist/runtime/server/{utils → internal}/dev-live-translations.js +4 -3
- package/dist/runtime/server/internal/server-translation-cache.d.ts +9 -0
- package/dist/runtime/server/internal/server-translation-cache.js +10 -0
- package/dist/runtime/server/internal/shared-cache.d.ts +24 -0
- package/dist/runtime/server/internal/shared-cache.js +71 -0
- package/dist/runtime/server/internal/translation-watcher.d.ts +79 -0
- package/dist/runtime/server/internal/translation-watcher.js +105 -0
- package/dist/runtime/server/middleware/i18n.global.js +24 -37
- package/dist/runtime/server/payload-source.assets.d.ts +1 -0
- package/dist/runtime/server/payload-source.assets.js +3 -8
- package/dist/runtime/server/payload-source.fs.d.ts +1 -0
- package/dist/runtime/server/payload-source.fs.js +18 -7
- package/dist/runtime/server/plugins/failed-read.d.ts +7 -0
- package/dist/runtime/server/plugins/failed-read.js +23 -0
- package/dist/runtime/server/plugins/payload-cache.d.ts +6 -0
- package/dist/runtime/server/plugins/payload-cache.js +16 -0
- package/dist/runtime/server/plugins/watcher.dev.js +15 -79
- package/dist/runtime/server/routes/i18n.js +13 -14
- package/dist/runtime/server/utils/locale-detector.d.ts +32 -2
- package/dist/runtime/server/utils/locale-detector.js +48 -27
- package/dist/runtime/server/utils/locale-server-middleware.js +14 -33
- package/dist/runtime/server/utils/payload-cache.d.ts +4 -0
- package/dist/runtime/server/utils/payload-cache.js +13 -0
- package/dist/runtime/server/utils/server-loader.d.ts +17 -6
- package/dist/runtime/server/utils/server-loader.js +105 -51
- package/dist/runtime/server/utils/translation-server-middleware.js +18 -35
- package/dist/runtime/utils/cache-keys.d.ts +6 -0
- package/dist/runtime/utils/cache-keys.js +2 -0
- package/dist/runtime/utils/i18n-api.d.ts +185 -0
- package/dist/runtime/utils/i18n-api.js +196 -0
- package/dist/runtime/utils/i18n-config.d.ts +18 -0
- package/dist/runtime/utils/i18n-config.js +47 -0
- package/dist/runtime/utils/i18n-navigation.d.ts +26 -0
- package/dist/runtime/utils/i18n-navigation.js +96 -0
- package/dist/runtime/utils/nuxt-i18n-server.d.ts +26 -0
- package/dist/runtime/utils/nuxt-i18n-server.js +83 -0
- package/dist/runtime/utils/nuxt-i18n.d.ts +81 -96
- package/dist/runtime/utils/nuxt-i18n.js +180 -252
- package/dist/runtime/utils/page-head.d.ts +12 -0
- package/dist/runtime/utils/page-head.js +6 -0
- package/dist/runtime/utils/request-url.d.ts +6 -0
- package/dist/runtime/utils/request-url.js +11 -0
- package/dist/runtime/utils/storage.d.ts +31 -19
- package/dist/runtime/utils/storage.js +100 -49
- package/dist/runtime/utils/translation-entry.d.ts +6 -0
- package/dist/runtime/utils/translation-entry.js +16 -0
- package/dist/types.d.mts +4 -6
- package/internals.d.mts +9 -2
- package/package.json +78 -79
- package/dist/client/_nuxt/BOxWElQP.js +0 -21
- package/dist/client/_nuxt/CUWB4CwN.js +0 -3
- package/dist/client/_nuxt/CmUbe-uv.js +0 -1
- package/dist/client/_nuxt/DAzShkZF.js +0 -1
- package/dist/client/_nuxt/MokPP5zi.js +0 -1
- package/dist/client/_nuxt/builds/meta/60974e48-d00e-4f9b-9467-9d4b322519da.json +0 -1
- package/dist/client/_nuxt/error-404.D9Svp6Hk.css +0 -1
- package/dist/client/_nuxt/error-500.B-mjI9_9.css +0 -1
- package/dist/runtime/plugins/03.define.d.ts +0 -7
- package/dist/runtime/plugins/03.define.js +0 -42
- package/dist/runtime/plugins/05.hooks.d.ts +0 -2
- package/dist/runtime/plugins/05.hooks.js +0 -47
- package/dist/runtime/server/utils/server-translation-cache.d.ts +0 -11
- package/dist/runtime/server/utils/server-translation-cache.js +0 -20
- /package/dist/runtime/server/{utils → internal}/dev-live-translations.d.ts +0 -0
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BaseI18n,
|
|
3
|
-
|
|
3
|
+
FormatService,
|
|
4
4
|
mergeTranslationChunk,
|
|
5
|
-
|
|
5
|
+
resolveCachedTranslation,
|
|
6
6
|
setTranslationAtKey,
|
|
7
7
|
translationCacheKey
|
|
8
8
|
} from "@i18n-micro/core";
|
|
9
|
-
import {
|
|
10
|
-
import { shallowRef, triggerRef
|
|
9
|
+
import { PREMERGED_ROOT_PAGE } from "@i18n-micro/utils/merge-source";
|
|
10
|
+
import { shallowRef, triggerRef } from "vue";
|
|
11
11
|
import { translationStorage } from "./storage.js";
|
|
12
|
-
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
path,
|
|
18
|
-
fullPath,
|
|
19
|
-
href: fullPath
|
|
20
|
-
};
|
|
21
|
-
if (result.query && Object.keys(result.query).length) out.query = result.query;
|
|
22
|
-
if (result.hash) out.hash = result.hash;
|
|
23
|
-
return out;
|
|
24
|
-
}
|
|
25
|
-
function extractSwitchLocalePath(localeRoute, resolveRouter) {
|
|
26
|
-
if (!localeRoute || typeof localeRoute !== "object") return String(localeRoute ?? "");
|
|
27
|
-
if ("fullPath" in localeRoute && localeRoute.fullPath) return localeRoute.fullPath;
|
|
28
|
-
if ("path" in localeRoute && localeRoute.path) return localeRoute.path;
|
|
29
|
-
if ("name" in localeRoute && localeRoute.name && resolveRouter.hasRoute(String(localeRoute.name))) {
|
|
30
|
-
return resolveRouter.resolve(localeRoute).fullPath;
|
|
31
|
-
}
|
|
32
|
-
return "";
|
|
12
|
+
const formatters = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
export function formatterFor(config) {
|
|
14
|
+
let formatter = formatters.get(config);
|
|
15
|
+
if (!formatter) formatters.set(config, formatter = new FormatService(config));
|
|
16
|
+
return formatter;
|
|
33
17
|
}
|
|
34
18
|
export class NuxtI18n extends BaseI18n {
|
|
35
19
|
storage;
|
|
@@ -43,6 +27,10 @@ export class NuxtI18n extends BaseI18n {
|
|
|
43
27
|
* already installed. Cleared in `finishTransition`.
|
|
44
28
|
*/
|
|
45
29
|
outgoingTranslations = null;
|
|
30
|
+
/** A lookup was answered from `outgoingTranslations`: only then does dropping it change a render. */
|
|
31
|
+
outgoingServed = false;
|
|
32
|
+
/** Chunks that hold only what was patched in: no page's file has reached them, so to a loader they are not loaded. */
|
|
33
|
+
fragments = /* @__PURE__ */ new Set();
|
|
46
34
|
currentLocale = "";
|
|
47
35
|
currentRouteName = "";
|
|
48
36
|
resolveRouteContext;
|
|
@@ -67,41 +55,35 @@ export class NuxtI18n extends BaseI18n {
|
|
|
67
55
|
getChunk(locale, routeName) {
|
|
68
56
|
return this.storage.translations.get(this.getCacheKey(locale, routeName)) ?? {};
|
|
69
57
|
}
|
|
70
|
-
|
|
58
|
+
/** The chunk is loaded. What a patch alone stored is a fragment, see `installChunk`. */
|
|
59
|
+
hasChunk(locale, routeName) {
|
|
71
60
|
const key = this.getCacheKey(locale, routeName);
|
|
72
|
-
this.storage.translations.
|
|
73
|
-
this.evictChunks(key);
|
|
61
|
+
return this.storage.translations.has(key) && !this.fragments.has(key);
|
|
74
62
|
}
|
|
75
63
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* The loader only installs the chunk for the page being rendered, but SSR may
|
|
79
|
-
* load buckets for other `(locale, route)` pairs — `$tForRoute()` resolves against
|
|
80
|
-
* `getChunk()` directly. Without this they hydrate as missing until navigation.
|
|
81
|
-
*
|
|
82
|
-
* Existing entries win: anything already loaded is kept as-is.
|
|
64
|
+
* A chunk fresh from a load or the cache, under what was patched in before it came: patches win. Without any, it
|
|
65
|
+
* is stored as it is: a stored tree is never written to, so it needs no copy.
|
|
83
66
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (lastKey) this.evictChunks(lastKey);
|
|
93
|
-
}
|
|
94
|
-
hasChunk(locale, routeName) {
|
|
95
|
-
return this.storage.translations.has(this.getCacheKey(locale, routeName));
|
|
67
|
+
installChunk(locale, routeName, data) {
|
|
68
|
+
const existing = this.getChunk(locale, routeName);
|
|
69
|
+
const chunk = Object.keys(existing).length ? mergeTranslationChunk(existing, data, { preserveExisting: true }) : data;
|
|
70
|
+
const key = this.getCacheKey(locale, routeName);
|
|
71
|
+
this.storage.translations.set(key, chunk);
|
|
72
|
+
this.fragments.delete(key);
|
|
73
|
+
this.evictChunks(key);
|
|
74
|
+
return chunk;
|
|
96
75
|
}
|
|
97
76
|
mergeChunk(locale, routeName, data) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const merged = mergeTranslationChunk(existing, data);
|
|
101
|
-
this.storage.translations.set(cacheKey, merged);
|
|
102
|
-
this.evictChunks(cacheKey);
|
|
77
|
+
const merged = mergeTranslationChunk(this.getChunk(locale, routeName), data);
|
|
78
|
+
this.storePatched(this.getCacheKey(locale, routeName), merged);
|
|
103
79
|
return merged;
|
|
104
80
|
}
|
|
81
|
+
/** A tree a patch made: over no loaded chunk (a stand-in, an evicted one) it is a fragment, and the next visit still loads. */
|
|
82
|
+
storePatched(key, tree) {
|
|
83
|
+
if (!this.storage.translations.has(key)) this.fragments.add(key);
|
|
84
|
+
this.storage.translations.set(key, tree);
|
|
85
|
+
this.evictChunks(key);
|
|
86
|
+
}
|
|
105
87
|
/**
|
|
106
88
|
* FIFO eviction for `storage.translations` when `cacheMaxSize` is set.
|
|
107
89
|
* Prefers keeping the just-written key and the active page chunk. When those
|
|
@@ -112,17 +94,13 @@ export class NuxtI18n extends BaseI18n {
|
|
|
112
94
|
evictChunks(keep) {
|
|
113
95
|
const max = this.cacheMaxSize;
|
|
114
96
|
if (max <= 0 || this.storage.translations.size <= max) return;
|
|
97
|
+
const { translations } = this.storage;
|
|
115
98
|
const current = this.getCacheKey(this.currentLocale, this.currentRouteName);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (this.storage.translations.size <= max) return;
|
|
122
|
-
for (const key of this.storage.translations.keys()) {
|
|
123
|
-
if (this.storage.translations.size <= max) break;
|
|
124
|
-
if (key === keep) continue;
|
|
125
|
-
this.storage.translations.delete(key);
|
|
99
|
+
const victims = [...translations.keys()].filter((key) => key !== keep && key !== current);
|
|
100
|
+
if (current !== keep) victims.push(current);
|
|
101
|
+
for (const key of victims) {
|
|
102
|
+
if (translations.size <= max) return;
|
|
103
|
+
translations.delete(key);
|
|
126
104
|
}
|
|
127
105
|
}
|
|
128
106
|
getLocale() {
|
|
@@ -134,9 +112,6 @@ export class NuxtI18n extends BaseI18n {
|
|
|
134
112
|
getRoute() {
|
|
135
113
|
return this.currentRouteName || "index";
|
|
136
114
|
}
|
|
137
|
-
getCurrentLocale() {
|
|
138
|
-
return this.currentLocale;
|
|
139
|
-
}
|
|
140
115
|
getCurrentRouteName() {
|
|
141
116
|
return this.currentRouteName;
|
|
142
117
|
}
|
|
@@ -155,8 +130,13 @@ export class NuxtI18n extends BaseI18n {
|
|
|
155
130
|
};
|
|
156
131
|
}
|
|
157
132
|
notifyContextChange(reason) {
|
|
158
|
-
for (const listener of this.contextListeners) {
|
|
159
|
-
listener
|
|
133
|
+
for (const listener of [...this.contextListeners]) {
|
|
134
|
+
if (!this.contextListeners.has(listener)) continue;
|
|
135
|
+
try {
|
|
136
|
+
listener(reason);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("[i18n] context listener failed:", error);
|
|
139
|
+
}
|
|
160
140
|
}
|
|
161
141
|
}
|
|
162
142
|
/**
|
|
@@ -171,45 +151,61 @@ export class NuxtI18n extends BaseI18n {
|
|
|
171
151
|
*/
|
|
172
152
|
applySwitchContext(locale, routeName, data) {
|
|
173
153
|
this.outgoingTranslations = this.currentLocale === locale ? this.cachedTranslations : null;
|
|
174
|
-
this.
|
|
154
|
+
this.outgoingServed = false;
|
|
155
|
+
this.cachedTranslations = data ?? {};
|
|
175
156
|
this.currentLocale = locale;
|
|
176
157
|
this.currentRouteName = routeName || "";
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
158
|
+
this.notifyLoaded();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Swap the active dictionary for a fresh copy of the same page. Not a page change: a live
|
|
162
|
+
* transition keeps its outgoing layer, the leaving page is still on screen.
|
|
163
|
+
*/
|
|
164
|
+
replaceDictionary(data) {
|
|
165
|
+
this.cachedTranslations = data;
|
|
166
|
+
this.notifyLoaded();
|
|
167
|
+
this.notifyContextChange("refresh");
|
|
168
|
+
}
|
|
169
|
+
/** The leaving page's chunk while it may still render, or null. */
|
|
170
|
+
get outgoing() {
|
|
171
|
+
return this.outgoingTranslations;
|
|
180
172
|
}
|
|
181
|
-
/**
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
/**
|
|
174
|
+
* The outgoing page has unmounted, so its keys must stop resolving. `outgoing` names which
|
|
175
|
+
* page left: a transition that ends after the next navigation must not drop that one's chunk.
|
|
176
|
+
*/
|
|
177
|
+
finishTransition(outgoing = this.outgoingTranslations) {
|
|
178
|
+
if (outgoing === null || outgoing !== this.outgoingTranslations) return;
|
|
184
179
|
this.outgoingTranslations = null;
|
|
185
|
-
triggerRef(this.contextSignal);
|
|
180
|
+
if (this.outgoingServed) triggerRef(this.contextSignal);
|
|
186
181
|
this.notifyContextChange("refresh");
|
|
187
182
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
/** Merge a fragment into a chunk and, when that chunk is on screen, into the active dictionary. */
|
|
184
|
+
patch(locale, routeName, translations) {
|
|
185
|
+
const stored = this.storage.translations.get(this.getCacheKey(locale, routeName));
|
|
186
|
+
const merged = this.mergeChunk(locale, routeName, translations);
|
|
187
|
+
if (locale !== this.currentLocale || (routeName || "index") !== this.getRoute()) return;
|
|
188
|
+
const onScreen = this.cachedTranslations;
|
|
189
|
+
this.cachedTranslations = stored === onScreen ? merged : mergeTranslationChunk(onScreen, translations);
|
|
190
|
+
this.notifyLoaded();
|
|
191
|
+
}
|
|
192
|
+
/** The active dictionary changed: what rendered from it renders again. */
|
|
193
|
+
notifyLoaded() {
|
|
191
194
|
triggerRef(this.contextSignal);
|
|
192
195
|
this.notifyContextChange("load");
|
|
193
196
|
}
|
|
197
|
+
mergeTranslations(newTranslations) {
|
|
198
|
+
this.patch(this.currentLocale, this.currentRouteName, newTranslations);
|
|
199
|
+
}
|
|
194
200
|
async loadPageTranslations(locale, routeName, translations) {
|
|
195
|
-
this.
|
|
196
|
-
if (locale === this.currentLocale && routeName === this.currentRouteName) {
|
|
197
|
-
this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, translations);
|
|
198
|
-
triggerRef(this.contextSignal);
|
|
199
|
-
this.notifyContextChange("load");
|
|
200
|
-
}
|
|
201
|
+
this.patch(locale, routeName, translations);
|
|
201
202
|
}
|
|
202
203
|
async mergeTranslationAsync(locale, routeName, newTranslations, loadIfMissing) {
|
|
203
204
|
if (!this.hasChunk(locale, routeName)) {
|
|
204
205
|
const loaded = await loadIfMissing(locale, routeName || void 0);
|
|
205
|
-
this.
|
|
206
|
-
}
|
|
207
|
-
this.mergeChunk(locale, routeName, newTranslations);
|
|
208
|
-
if (locale === this.currentLocale && routeName === this.currentRouteName) {
|
|
209
|
-
this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations);
|
|
210
|
-
triggerRef(this.contextSignal);
|
|
211
|
-
this.notifyContextChange("load");
|
|
206
|
+
if (!this.hasChunk(locale, routeName) && Object.keys(loaded).length) this.installChunk(locale, routeName, loaded);
|
|
212
207
|
}
|
|
208
|
+
this.patch(locale, routeName, newTranslations);
|
|
213
209
|
}
|
|
214
210
|
tForRoute(route) {
|
|
215
211
|
return (key, params, defaultValue) => this.t(key, params, defaultValue, route);
|
|
@@ -220,17 +216,24 @@ export class NuxtI18n extends BaseI18n {
|
|
|
220
216
|
touch() {
|
|
221
217
|
this.contextSignal.value;
|
|
222
218
|
}
|
|
219
|
+
/** The chunk a route reads from, when routes are known here. */
|
|
220
|
+
chunkKeyOf(routeContext) {
|
|
221
|
+
if (!this.resolveRouteContext) return void 0;
|
|
222
|
+
const { locale, routeName } = this.resolveRouteContext(routeContext);
|
|
223
|
+
return this.getCacheKey(locale, routeName);
|
|
224
|
+
}
|
|
223
225
|
resolveLookup(key, routeContext) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
})() : this.cachedTranslations;
|
|
228
|
-
const value = resolveTranslation(translations, String(key));
|
|
226
|
+
const chunkKey = routeContext ? this.chunkKeyOf(routeContext) : void 0;
|
|
227
|
+
const translations = chunkKey === void 0 ? this.cachedTranslations : this.storage.translations.get(chunkKey) ?? {};
|
|
228
|
+
const value = resolveCachedTranslation(translations, String(key));
|
|
229
229
|
if (value !== null || translations !== this.cachedTranslations) return value;
|
|
230
|
-
|
|
230
|
+
if (this.outgoingTranslations === null) return null;
|
|
231
|
+
const leaving = resolveCachedTranslation(this.outgoingTranslations, String(key));
|
|
232
|
+
if (leaving !== null) this.outgoingServed = true;
|
|
233
|
+
return leaving;
|
|
231
234
|
}
|
|
232
|
-
resolveHas(key) {
|
|
233
|
-
return this.resolveLookup(key) !== null;
|
|
235
|
+
resolveHas(key, routeContext) {
|
|
236
|
+
return this.resolveLookup(key, routeContext) !== null;
|
|
234
237
|
}
|
|
235
238
|
/**
|
|
236
239
|
* Dump what `$t` can resolve right now — including the hot-reload buffer while a transition
|
|
@@ -243,6 +246,7 @@ export class NuxtI18n extends BaseI18n {
|
|
|
243
246
|
return this.getChunk(locale, routeName);
|
|
244
247
|
}
|
|
245
248
|
if (this.outgoingTranslations === null) return this.cachedTranslations;
|
|
249
|
+
this.outgoingServed = true;
|
|
246
250
|
return this.resolveTranslationTree(this.outgoingTranslations, this.cachedTranslations);
|
|
247
251
|
}
|
|
248
252
|
/**
|
|
@@ -254,11 +258,8 @@ export class NuxtI18n extends BaseI18n {
|
|
|
254
258
|
*/
|
|
255
259
|
setTranslation(key, value) {
|
|
256
260
|
this.cachedTranslations = setTranslationAtKey(this.cachedTranslations, String(key), value);
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
this.currentRouteName,
|
|
260
|
-
setTranslationAtKey(this.getChunk(this.currentLocale, this.currentRouteName), String(key), value)
|
|
261
|
-
);
|
|
261
|
+
const chunk = this.getChunk(this.currentLocale, this.currentRouteName);
|
|
262
|
+
this.storePatched(this.getCacheKey(this.currentLocale, this.currentRouteName), setTranslationAtKey(chunk, String(key), value));
|
|
262
263
|
triggerRef(this.contextSignal);
|
|
263
264
|
this.notifyContextChange("refresh");
|
|
264
265
|
}
|
|
@@ -280,7 +281,13 @@ export class NuxtI18n extends BaseI18n {
|
|
|
280
281
|
console.warn(`[i18n] Missing key '${key}' in '${locale}' for route '${routeName}'`);
|
|
281
282
|
}
|
|
282
283
|
}
|
|
284
|
+
/** Drop every stored chunk but keep the dictionary on screen until a reload replaces it. */
|
|
285
|
+
dropChunks() {
|
|
286
|
+
this.fragments.clear();
|
|
287
|
+
super.clearCache();
|
|
288
|
+
}
|
|
283
289
|
clearCache() {
|
|
290
|
+
this.fragments.clear();
|
|
284
291
|
super.clearCache();
|
|
285
292
|
this.cachedTranslations = {};
|
|
286
293
|
this.outgoingTranslations = null;
|
|
@@ -294,14 +301,13 @@ export class NuxtTranslationLoader {
|
|
|
294
301
|
}
|
|
295
302
|
pendingLoads = /* @__PURE__ */ new Map();
|
|
296
303
|
/**
|
|
297
|
-
* Monotonic token so a slower in-flight `switchContext`
|
|
298
|
-
*
|
|
304
|
+
* Monotonic token so a slower in-flight `switchContext` (`$switchLocale` without a URL to move)
|
|
305
|
+
* cannot overwrite a newer one. Navigations do not need it: they apply when they commit.
|
|
299
306
|
*/
|
|
300
307
|
switchGeneration = 0;
|
|
301
308
|
/**
|
|
302
309
|
* Bump the switch generation so any in-flight `switchContext` becomes a no-op.
|
|
303
|
-
* Call on every navigation that may supersede a pending load
|
|
304
|
-
* target `(locale, routeName)` already matches the applied context (caller skips switch).
|
|
310
|
+
* Call on every navigation that may supersede a pending load.
|
|
305
311
|
*/
|
|
306
312
|
invalidatePendingSwitches() {
|
|
307
313
|
this.switchGeneration++;
|
|
@@ -312,28 +318,26 @@ export class NuxtTranslationLoader {
|
|
|
312
318
|
return i18n.getChunk(locale, routeName);
|
|
313
319
|
}
|
|
314
320
|
const cached = translationStorage.getFromCache(locale, routeName);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return
|
|
321
|
+
return cached && i18n.installChunk(locale, routeName, cached);
|
|
322
|
+
}
|
|
323
|
+
/** The chunk, or `{}` when the load fails (a page without translations still renders). */
|
|
324
|
+
async loadAsync(locale, routeName) {
|
|
325
|
+
return await this.loadChunk(locale, routeName) ?? {};
|
|
320
326
|
}
|
|
321
|
-
|
|
322
|
-
|
|
327
|
+
/** The loaded chunk, or null when the load failed (reported here): a reload must tell that from an empty chunk. */
|
|
328
|
+
loadChunk(locale, routeName) {
|
|
329
|
+
const { i18n, loadOptions } = this.options;
|
|
323
330
|
const cacheKey = i18n.getCacheKey(locale, routeName);
|
|
324
331
|
const pending = this.pendingLoads.get(cacheKey);
|
|
325
332
|
if (pending) return pending;
|
|
326
333
|
const promise = (async () => {
|
|
327
334
|
try {
|
|
328
|
-
|
|
329
|
-
const existing = i18n.getChunk(locale, routeName);
|
|
330
|
-
const mergedChunk = mergeTranslationChunk(existing, result.data, { preserveExisting: true });
|
|
331
|
-
i18n.setChunk(locale, routeName, mergedChunk);
|
|
332
|
-
return mergedChunk;
|
|
335
|
+
return i18n.installChunk(locale, routeName, await translationStorage.load(locale, routeName, loadOptions));
|
|
333
336
|
} catch (e) {
|
|
334
|
-
if (
|
|
337
|
+
if (process.env.NODE_ENV !== "production") console.error("[i18n] Load error:", e);
|
|
335
338
|
else console.warn("[i18n] Translation load failed:", locale, routeName);
|
|
336
|
-
|
|
339
|
+
this.options.onFailedRead?.();
|
|
340
|
+
return null;
|
|
337
341
|
} finally {
|
|
338
342
|
this.pendingLoads.delete(cacheKey);
|
|
339
343
|
}
|
|
@@ -342,138 +346,62 @@ export class NuxtTranslationLoader {
|
|
|
342
346
|
return promise;
|
|
343
347
|
}
|
|
344
348
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
349
|
+
* Reload the current context's dictionary (after a cache clear, an HMR edit). Unlike
|
|
350
|
+
* {@link switchContext} it does not supersede an in-flight navigation: the fresh chunk is
|
|
351
|
+
* applied only if the context is still the one it was loaded for.
|
|
352
|
+
*/
|
|
353
|
+
async refresh() {
|
|
354
|
+
const { i18n } = this.options;
|
|
355
|
+
const locale = i18n.getLocale();
|
|
356
|
+
const routeName = i18n.getCurrentRouteName() || void 0;
|
|
357
|
+
const data = await this.loadChunk(locale, routeName);
|
|
358
|
+
if (data === null) return;
|
|
359
|
+
if (i18n.getLocale() !== locale || (i18n.getCurrentRouteName() || void 0) !== routeName) return;
|
|
360
|
+
i18n.replaceDictionary(data);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Drop every cached chunk (storage and instance) and reload the current page's dictionary.
|
|
364
|
+
* The page keeps its current dictionary until the fresh one is applied: no raw keys between.
|
|
365
|
+
*/
|
|
366
|
+
async reload() {
|
|
367
|
+
translationStorage.clear();
|
|
368
|
+
this.options.i18n.dropChunks();
|
|
369
|
+
await this.refresh();
|
|
370
|
+
}
|
|
371
|
+
/** The chunk of a context, cached or loaded, or null when its load failed. */
|
|
372
|
+
fetchContext(locale, routeName) {
|
|
373
|
+
return this.loadFromCacheSync(locale, routeName) ?? this.loadChunk(locale, routeName);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Make a fetched chunk the active context. `false` when its load failed: that keeps what is on screen, as
|
|
377
|
+
* refresh() does, since an empty dictionary would turn the strings that still work (a layout's) into raw keys.
|
|
378
|
+
* Only a first render has nothing to keep. A prerender throws there instead: no next request would load it again.
|
|
379
|
+
*/
|
|
380
|
+
applyContext(locale, routeName, data) {
|
|
381
|
+
const { i18n, prerender } = this.options;
|
|
382
|
+
if (data === null && i18n.getLocale()) return false;
|
|
383
|
+
if (data === null && prerender) throw new Error(`[i18n] Translation load failed: ${locale} ${routeName}`);
|
|
384
|
+
i18n.applySwitchContext(locale, routeName, data);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Load and apply translations for a locale/route. Returns `false` when the switch did not
|
|
389
|
+
* apply — a newer navigation superseded it, or its chunk failed to load — and callers must
|
|
390
|
+
* skip post-await side effects (e.g. `setLocale`). The next navigation loads it again.
|
|
348
391
|
*/
|
|
349
392
|
async switchContext(locale, routeName) {
|
|
350
393
|
const generation = ++this.switchGeneration;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
394
|
+
const data = await this.fetchContext(locale, routeName);
|
|
395
|
+
const shown = data ?? await this.firstRenderStandIn(locale, routeName);
|
|
396
|
+
return generation === this.switchGeneration && this.applyContext(locale, routeName, shown);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* A first render whose chunk failed has nothing to keep, but its root dictionary can be at hand (the shared
|
|
400
|
+
* cache in a host outage): the layout's strings stay text. A prerender must fail instead, it would keep the file.
|
|
401
|
+
*/
|
|
402
|
+
firstRenderStandIn(locale, routeName) {
|
|
403
|
+
const { i18n, prerender } = this.options;
|
|
404
|
+
if (prerender || i18n.getLocale() || routeName === PREMERGED_ROOT_PAGE) return null;
|
|
405
|
+
return this.fetchContext(locale, PREMERGED_ROOT_PAGE);
|
|
358
406
|
}
|
|
359
|
-
}
|
|
360
|
-
export function createNuxtI18nPluginApi(deps) {
|
|
361
|
-
const {
|
|
362
|
-
i18n,
|
|
363
|
-
loader,
|
|
364
|
-
i18nStrategy,
|
|
365
|
-
i18nConfig,
|
|
366
|
-
router,
|
|
367
|
-
getCurrentLocale,
|
|
368
|
-
getEffectiveLocale,
|
|
369
|
-
getPluginRouteName,
|
|
370
|
-
getRouteName,
|
|
371
|
-
i18nRouteParams,
|
|
372
|
-
setLocale,
|
|
373
|
-
isValidLocale,
|
|
374
|
-
navigateTo,
|
|
375
|
-
setMissingHandler
|
|
376
|
-
} = deps;
|
|
377
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
378
|
-
const isNoPrefix = isNoPrefixStrategy(i18nConfig.strategy);
|
|
379
|
-
const switchLocaleLogic = (toLocale, i18nParams, to) => {
|
|
380
|
-
const fromLocale = getCurrentLocale();
|
|
381
|
-
const currentRoute = router.currentRoute.value;
|
|
382
|
-
let resolvedTarget;
|
|
383
|
-
if (typeof to === "string") {
|
|
384
|
-
resolvedTarget = router.resolve(i18nStrategy.formatPathForResolve(to, fromLocale, toLocale));
|
|
385
|
-
} else {
|
|
386
|
-
resolvedTarget = to ?? currentRoute;
|
|
387
|
-
}
|
|
388
|
-
const switchedRoute = i18nStrategy.switchLocaleRoute(fromLocale, toLocale, resolvedTarget, {
|
|
389
|
-
i18nRouteParams: i18nParams
|
|
390
|
-
});
|
|
391
|
-
if (typeof switchedRoute === "string" && (switchedRoute.startsWith("http://") || switchedRoute.startsWith("https://"))) {
|
|
392
|
-
return navigateTo(switchedRoute, { redirectCode: 200, external: true });
|
|
393
|
-
}
|
|
394
|
-
if (isNoPrefix) {
|
|
395
|
-
;
|
|
396
|
-
switchedRoute.force = true;
|
|
397
|
-
return router.replace(switchedRoute);
|
|
398
|
-
}
|
|
399
|
-
return router.push(switchedRoute);
|
|
400
|
-
};
|
|
401
|
-
const helper = {
|
|
402
|
-
async mergeTranslation(locale, routeName, newTranslations, _force = false) {
|
|
403
|
-
await i18n.mergeTranslationAsync(locale, routeName, newTranslations, (l, r) => loader.loadAsync(l, r));
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
const switchContext = (locale, routeName) => loader.switchContext(locale, routeName);
|
|
407
|
-
const provide = {
|
|
408
|
-
i18nStrategy,
|
|
409
|
-
getLocale: (route) => getEffectiveLocale(route, (r) => getCurrentLocale(r)),
|
|
410
|
-
getLocaleName: () => i18nStrategy.getCurrentLocaleName(router.currentRoute.value, getCurrentLocale() ?? null),
|
|
411
|
-
defaultLocale: () => i18nConfig.defaultLocale,
|
|
412
|
-
getLocales: () => i18nConfig.locales || [],
|
|
413
|
-
getRouteName,
|
|
414
|
-
t: i18n.t.bind(i18n),
|
|
415
|
-
ts: (key, params, defaultValue, route) => {
|
|
416
|
-
const value = route ? i18n.t(key, params, defaultValue, route) : i18n.ts(key, params, defaultValue);
|
|
417
|
-
return value?.toString() ?? defaultValue ?? key;
|
|
418
|
-
},
|
|
419
|
-
_t: (route) => i18n.tForRoute(route),
|
|
420
|
-
_ts: (route) => i18n.tsForRoute(route),
|
|
421
|
-
tc: i18n.tc.bind(i18n),
|
|
422
|
-
tn: i18n.tn.bind(i18n),
|
|
423
|
-
td: i18n.td.bind(i18n),
|
|
424
|
-
tdr: i18n.tdr.bind(i18n),
|
|
425
|
-
has: i18n.has.bind(i18n),
|
|
426
|
-
resolveTranslations: i18n.resolveTranslations.bind(i18n),
|
|
427
|
-
setTranslation: i18n.setTranslation.bind(i18n),
|
|
428
|
-
mergeTranslations: i18n.mergeTranslations.bind(i18n),
|
|
429
|
-
switchLocaleRoute: (toLocale) => {
|
|
430
|
-
const route = router.currentRoute.value;
|
|
431
|
-
const fromLocale = getCurrentLocale(route);
|
|
432
|
-
return i18nStrategy.switchLocaleRoute(fromLocale, toLocale, route, { i18nRouteParams: unref(i18nRouteParams.value) });
|
|
433
|
-
},
|
|
434
|
-
clearCache: () => {
|
|
435
|
-
translationStorage.clear();
|
|
436
|
-
i18n.clearCache();
|
|
437
|
-
},
|
|
438
|
-
switchLocalePath: (toLocale) => {
|
|
439
|
-
const route = router.currentRoute.value;
|
|
440
|
-
const fromLocale = getCurrentLocale(route);
|
|
441
|
-
const switched = i18nStrategy.switchLocaleRoute(fromLocale, toLocale, route, { i18nRouteParams: unref(i18nRouteParams.value) });
|
|
442
|
-
return applyTrailingSlash(extractSwitchLocalePath(switched, router), i18nConfig.trailingSlash);
|
|
443
|
-
},
|
|
444
|
-
switchLocale: async (toLocale) => {
|
|
445
|
-
if (!isValidLocale(toLocale)) {
|
|
446
|
-
if (isDev) console.warn(`[i18n] Invalid locale '${toLocale}'`);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (isNoPrefixStrategy(i18nConfig.strategy) || i18nConfig.hashMode) {
|
|
450
|
-
const route = router.currentRoute.value;
|
|
451
|
-
const routeName = getPluginRouteName(route, toLocale);
|
|
452
|
-
const applied = await loader.switchContext(toLocale, routeName);
|
|
453
|
-
if (!applied) return;
|
|
454
|
-
}
|
|
455
|
-
setLocale(toLocale);
|
|
456
|
-
return switchLocaleLogic(toLocale, unref(i18nRouteParams.value));
|
|
457
|
-
},
|
|
458
|
-
switchRoute: (route, toLocale) => {
|
|
459
|
-
return switchLocaleLogic(toLocale ?? getCurrentLocale(), unref(i18nRouteParams.value), route);
|
|
460
|
-
},
|
|
461
|
-
localeRoute(to, locale) {
|
|
462
|
-
const targetLocale = locale !== void 0 && locale !== "" ? String(locale) : getCurrentLocale();
|
|
463
|
-
const result = i18nStrategy.localeRoute(targetLocale, to, router.currentRoute.value);
|
|
464
|
-
return toRouteLocationResolved(result);
|
|
465
|
-
},
|
|
466
|
-
localePath(to, locale) {
|
|
467
|
-
const targetLocale = locale !== void 0 && locale !== "" ? String(locale) : getCurrentLocale();
|
|
468
|
-
const result = i18nStrategy.localeRoute(targetLocale, to, router.currentRoute.value);
|
|
469
|
-
return applyTrailingSlash(result.fullPath ?? result.path ?? "", i18nConfig.trailingSlash);
|
|
470
|
-
},
|
|
471
|
-
setI18nRouteParams: (value) => {
|
|
472
|
-
i18nRouteParams.value = value;
|
|
473
|
-
return i18nRouteParams.value;
|
|
474
|
-
},
|
|
475
|
-
loadPageTranslations: i18n.loadPageTranslations.bind(i18n),
|
|
476
|
-
setMissingHandler
|
|
477
|
-
};
|
|
478
|
-
return { helper, switchContext, provide };
|
|
479
407
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { I18nHeadInput } from '@i18n-micro/types';
|
|
2
|
+
/**
|
|
3
|
+
* The page's overrides. Out of `composables/`, which is auto-imported. The merge stays in `02.meta`:
|
|
4
|
+
* a page head set on the server only reaches the client in the payload, with no `useI18nHead` call there.
|
|
5
|
+
*/
|
|
6
|
+
export declare const usePageHead: () => import("vue").Ref<I18nHeadInput | null, I18nHeadInput | null>;
|
|
7
|
+
/**
|
|
8
|
+
* The route of the page the caller lives in, which stays on that page while the next one loads: a route that differs
|
|
9
|
+
* from the router's means the page was left. Null outside a page (a `<RouterView>`): a layout or `app.vue` is never
|
|
10
|
+
* left, and its own `useRoute()` lags until `page:finish`, so that comparison would read every navigation as leaving.
|
|
11
|
+
*/
|
|
12
|
+
export declare const usePageRoute: () => import("vue-router").RouteLocationNormalizedLoadedGeneric | null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { hasInjectionContext, inject } from "vue";
|
|
2
|
+
import { viewDepthKey } from "vue-router";
|
|
3
|
+
import { useRoute } from "#app/composables/router";
|
|
4
|
+
import { useState } from "#app/composables/state";
|
|
5
|
+
export const usePageHead = () => useState("i18n-head-page", () => null);
|
|
6
|
+
export const usePageRoute = () => hasInjectionContext() && inject(viewDepthKey, 0) ? useRoute() : null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
/**
|
|
3
|
+
* `getRequestURL` for a pathname. It builds on `Host` and the request line, which a client writes: what does not parse must not
|
|
4
|
+
* cost the page. With a leading `/` any line is a path, so `GET http://[/` is a page that does not exist.
|
|
5
|
+
*/
|
|
6
|
+
export declare function requestURL(event: H3Event): URL;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getRequestURL } from "h3";
|
|
2
|
+
export function requestURL(event) {
|
|
3
|
+
try {
|
|
4
|
+
return getRequestURL(event);
|
|
5
|
+
} catch {
|
|
6
|
+
const path = event.path;
|
|
7
|
+
let start = 0;
|
|
8
|
+
while (path[start] === "/" || path[start] === "\\") start++;
|
|
9
|
+
return new URL(`/${path.slice(start)}`, "http://localhost");
|
|
10
|
+
}
|
|
11
|
+
}
|