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.
Files changed (111) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/dist/client/200.html +1 -1
  4. package/dist/client/404.html +1 -1
  5. package/dist/client/_nuxt/BVna_KKx.js +3 -0
  6. package/dist/client/_nuxt/Bw81RhAv.js +1 -0
  7. package/dist/client/_nuxt/CPz_jhsD.js +1 -0
  8. package/dist/client/_nuxt/CmvObVGq.js +1 -0
  9. package/dist/client/_nuxt/DKUOBR6F.js +5 -0
  10. package/dist/client/_nuxt/builds/latest.json +1 -1
  11. package/dist/client/_nuxt/builds/meta/f0c8d4bf-40d2-4915-8b4a-ebe46bde61c8.json +1 -0
  12. package/dist/client/_nuxt/{entry.BM-mexAv.css → entry.PSWtHhay.css} +1 -1
  13. package/dist/client/_nuxt/error-404.xs3fxhVJ.css +1 -0
  14. package/dist/client/_nuxt/error-500.C2S-4iLg.css +1 -0
  15. package/dist/client/index.html +1 -1
  16. package/dist/module.d.mts +14 -5
  17. package/dist/module.json +4 -1
  18. package/dist/module.mjs +850 -475
  19. package/dist/runtime/components/i18n-group.d.vue.ts +17 -9
  20. package/dist/runtime/components/i18n-group.vue +2 -4
  21. package/dist/runtime/components/i18n-group.vue.d.ts +17 -9
  22. package/dist/runtime/components/i18n-link.d.vue.ts +9 -6
  23. package/dist/runtime/components/i18n-link.vue +7 -28
  24. package/dist/runtime/components/i18n-link.vue.d.ts +9 -6
  25. package/dist/runtime/components/i18n-switcher.d.vue.ts +128 -54
  26. package/dist/runtime/components/i18n-switcher.vue +56 -156
  27. package/dist/runtime/components/i18n-switcher.vue.d.ts +128 -54
  28. package/dist/runtime/components/i18n-t.d.vue.ts +59 -116
  29. package/dist/runtime/components/i18n-t.vue +24 -71
  30. package/dist/runtime/components/i18n-t.vue.d.ts +59 -116
  31. package/dist/runtime/composables/useI18n.js +2 -38
  32. package/dist/runtime/composables/useI18nHead.js +21 -6
  33. package/dist/runtime/composables/useI18nLocale.d.ts +4 -1
  34. package/dist/runtime/composables/useI18nLocale.js +20 -22
  35. package/dist/runtime/composables/useLocaleHead.d.ts +26 -31
  36. package/dist/runtime/composables/useLocaleHead.js +47 -167
  37. package/dist/runtime/devtools/bridge.d.ts +2 -2
  38. package/dist/runtime/devtools/vue-devtools.d.ts +2 -2
  39. package/dist/runtime/devtools/vue-devtools.js +5 -5
  40. package/dist/runtime/middleware/i18n-redirect.global.d.ts +3 -2
  41. package/dist/runtime/middleware/i18n-redirect.global.js +30 -41
  42. package/dist/runtime/plugins/01.plugin.d.ts +156 -34
  43. package/dist/runtime/plugins/01.plugin.js +184 -138
  44. package/dist/runtime/plugins/02.meta.js +36 -43
  45. package/dist/runtime/plugins/04.vue-devtools.js +1 -1
  46. package/dist/runtime/plugins/06.redirect.d.ts +3 -3
  47. package/dist/runtime/plugins/06.redirect.js +65 -91
  48. package/dist/runtime/server/{utils → internal}/dev-live-translations.js +4 -3
  49. package/dist/runtime/server/internal/server-translation-cache.d.ts +9 -0
  50. package/dist/runtime/server/internal/server-translation-cache.js +10 -0
  51. package/dist/runtime/server/internal/shared-cache.d.ts +24 -0
  52. package/dist/runtime/server/internal/shared-cache.js +71 -0
  53. package/dist/runtime/server/internal/translation-watcher.d.ts +79 -0
  54. package/dist/runtime/server/internal/translation-watcher.js +105 -0
  55. package/dist/runtime/server/middleware/i18n.global.js +24 -37
  56. package/dist/runtime/server/payload-source.assets.d.ts +1 -0
  57. package/dist/runtime/server/payload-source.assets.js +3 -8
  58. package/dist/runtime/server/payload-source.fs.d.ts +1 -0
  59. package/dist/runtime/server/payload-source.fs.js +18 -7
  60. package/dist/runtime/server/plugins/failed-read.d.ts +7 -0
  61. package/dist/runtime/server/plugins/failed-read.js +23 -0
  62. package/dist/runtime/server/plugins/payload-cache.d.ts +6 -0
  63. package/dist/runtime/server/plugins/payload-cache.js +16 -0
  64. package/dist/runtime/server/plugins/watcher.dev.js +15 -79
  65. package/dist/runtime/server/routes/i18n.js +13 -14
  66. package/dist/runtime/server/utils/locale-detector.d.ts +32 -2
  67. package/dist/runtime/server/utils/locale-detector.js +48 -27
  68. package/dist/runtime/server/utils/locale-server-middleware.js +14 -33
  69. package/dist/runtime/server/utils/payload-cache.d.ts +4 -0
  70. package/dist/runtime/server/utils/payload-cache.js +13 -0
  71. package/dist/runtime/server/utils/server-loader.d.ts +17 -6
  72. package/dist/runtime/server/utils/server-loader.js +105 -51
  73. package/dist/runtime/server/utils/translation-server-middleware.js +18 -35
  74. package/dist/runtime/utils/cache-keys.d.ts +6 -0
  75. package/dist/runtime/utils/cache-keys.js +2 -0
  76. package/dist/runtime/utils/i18n-api.d.ts +185 -0
  77. package/dist/runtime/utils/i18n-api.js +196 -0
  78. package/dist/runtime/utils/i18n-config.d.ts +18 -0
  79. package/dist/runtime/utils/i18n-config.js +47 -0
  80. package/dist/runtime/utils/i18n-navigation.d.ts +26 -0
  81. package/dist/runtime/utils/i18n-navigation.js +96 -0
  82. package/dist/runtime/utils/nuxt-i18n-server.d.ts +26 -0
  83. package/dist/runtime/utils/nuxt-i18n-server.js +83 -0
  84. package/dist/runtime/utils/nuxt-i18n.d.ts +81 -96
  85. package/dist/runtime/utils/nuxt-i18n.js +180 -252
  86. package/dist/runtime/utils/page-head.d.ts +12 -0
  87. package/dist/runtime/utils/page-head.js +6 -0
  88. package/dist/runtime/utils/request-url.d.ts +6 -0
  89. package/dist/runtime/utils/request-url.js +11 -0
  90. package/dist/runtime/utils/storage.d.ts +31 -19
  91. package/dist/runtime/utils/storage.js +100 -49
  92. package/dist/runtime/utils/translation-entry.d.ts +6 -0
  93. package/dist/runtime/utils/translation-entry.js +16 -0
  94. package/dist/types.d.mts +4 -6
  95. package/internals.d.mts +9 -2
  96. package/package.json +78 -79
  97. package/dist/client/_nuxt/BOxWElQP.js +0 -21
  98. package/dist/client/_nuxt/CUWB4CwN.js +0 -3
  99. package/dist/client/_nuxt/CmUbe-uv.js +0 -1
  100. package/dist/client/_nuxt/DAzShkZF.js +0 -1
  101. package/dist/client/_nuxt/MokPP5zi.js +0 -1
  102. package/dist/client/_nuxt/builds/meta/60974e48-d00e-4f9b-9467-9d4b322519da.json +0 -1
  103. package/dist/client/_nuxt/error-404.D9Svp6Hk.css +0 -1
  104. package/dist/client/_nuxt/error-500.B-mjI9_9.css +0 -1
  105. package/dist/runtime/plugins/03.define.d.ts +0 -7
  106. package/dist/runtime/plugins/03.define.js +0 -42
  107. package/dist/runtime/plugins/05.hooks.d.ts +0 -2
  108. package/dist/runtime/plugins/05.hooks.js +0 -47
  109. package/dist/runtime/server/utils/server-translation-cache.d.ts +0 -11
  110. package/dist/runtime/server/utils/server-translation-cache.js +0 -20
  111. /package/dist/runtime/server/{utils → internal}/dev-live-translations.d.ts +0 -0
@@ -1,35 +1,19 @@
1
1
  import {
2
2
  BaseI18n,
3
- isNoPrefixStrategy,
3
+ FormatService,
4
4
  mergeTranslationChunk,
5
- resolveTranslation,
5
+ resolveCachedTranslation,
6
6
  setTranslationAtKey,
7
7
  translationCacheKey
8
8
  } from "@i18n-micro/core";
9
- import { deepMergeTranslationsRecursive } from "@i18n-micro/utils/deep-merge";
10
- import { shallowRef, triggerRef, unref } from "vue";
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
- import { applyTrailingSlash } from "./trailing-slash.js";
13
- function toRouteLocationResolved(result) {
14
- const fullPath = result.fullPath ?? result.path ?? "";
15
- const path = result.path ?? fullPath.split("?")[0]?.split("#")[0] ?? fullPath;
16
- const out = {
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
- setChunk(locale, routeName, data) {
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.set(key, data);
73
- this.evictChunks(key);
61
+ return this.storage.translations.has(key) && !this.fragments.has(key);
74
62
  }
75
63
  /**
76
- * Install SSR chunks from the payload into the view layer.
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
- seedChunks(chunks) {
85
- let lastKey = "";
86
- for (const [cacheKey, data] of Object.entries(chunks)) {
87
- if (!this.storage.translations.has(cacheKey)) {
88
- this.storage.translations.set(cacheKey, data);
89
- lastKey = cacheKey;
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 cacheKey = this.getCacheKey(locale, routeName);
99
- const existing = this.storage.translations.get(cacheKey) ?? {};
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
- for (const key of this.storage.translations.keys()) {
117
- if (this.storage.translations.size <= max) break;
118
- if (key === keep || key === current) continue;
119
- this.storage.translations.delete(key);
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(reason);
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.cachedTranslations = data;
154
+ this.outgoingServed = false;
155
+ this.cachedTranslations = data ?? {};
175
156
  this.currentLocale = locale;
176
157
  this.currentRouteName = routeName || "";
177
- this.setChunk(locale, routeName, data);
178
- triggerRef(this.contextSignal);
179
- this.notifyContextChange("load");
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
- /** The outgoing page has unmounted, so its keys must stop resolving. */
182
- finishTransition() {
183
- if (this.outgoingTranslations === null) return;
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
- mergeTranslations(newTranslations) {
189
- this.mergeChunk(this.currentLocale, this.currentRouteName, newTranslations);
190
- this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations);
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.mergeChunk(locale, routeName, translations);
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.setChunk(locale, routeName, loaded);
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 translations = routeContext && this.resolveRouteContext ? (() => {
225
- const { locale, routeName } = this.resolveRouteContext(routeContext);
226
- return this.getChunk(locale, routeName);
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
- return this.outgoingTranslations === null ? null : resolveTranslation(this.outgoingTranslations, String(key));
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.setChunk(
258
- this.currentLocale,
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` cannot overwrite a newer one.
298
- * Vue Router aborts a superseded navigation but does not cancel the guard's await.
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 — including when the
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
- if (cached) {
316
- i18n.setChunk(locale, routeName, cached.data);
317
- return cached.data;
318
- }
319
- return null;
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
- loadAsync(locale, routeName) {
322
- const { i18n, loadOptions, isDev } = this.options;
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
- const result = await translationStorage.load(locale, routeName, loadOptions);
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 (isDev) console.error("[i18n] Load error:", e);
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
- return {};
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
- * Load and apply translations for a locale/route. Returns `false` when a newer
346
- * navigation superseded this switch — callers must skip post-await side effects
347
- * (e.g. `setLocale`) for the abandoned navigation.
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
- let data = this.loadFromCacheSync(locale, routeName);
352
- if (data === null) {
353
- data = await this.loadAsync(locale, routeName);
354
- }
355
- if (generation !== this.switchGeneration) return false;
356
- this.options.i18n.applySwitchContext(locale, routeName, data);
357
- return true;
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
+ }