nuxt-i18n-micro 3.26.0 → 3.27.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.
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.CJGrD2VA.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DBXvRcU1.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.BcWSpGYy.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/p3h4sA5-.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DbtvxgAm.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"24f3c578-f02b-41bc-953a-c7ff7b504d56",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1785527485918,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.CJGrD2VA.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DBXvRcU1.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.BcWSpGYy.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/p3h4sA5-.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DbtvxgAm.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"fcc4b5e8-a04c-49bd-9cb3-620fcda4d432",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1785932351898,false]</script></body></html>
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.CJGrD2VA.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DBXvRcU1.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.BcWSpGYy.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/p3h4sA5-.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DbtvxgAm.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"24f3c578-f02b-41bc-953a-c7ff7b504d56",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1785527485918,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.CJGrD2VA.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DBXvRcU1.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.BcWSpGYy.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/p3h4sA5-.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DbtvxgAm.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"fcc4b5e8-a04c-49bd-9cb3-620fcda4d432",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1785932351899,false]</script></body></html>
@@ -1 +1 @@
1
- {"id":"24f3c578-f02b-41bc-953a-c7ff7b504d56","timestamp":1785527483273}
1
+ {"id":"fcc4b5e8-a04c-49bd-9cb3-620fcda4d432","timestamp":1785932349529}
@@ -0,0 +1 @@
1
+ {"id":"fcc4b5e8-a04c-49bd-9cb3-620fcda4d432","timestamp":1785932349529,"prerendered":[]}
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.CJGrD2VA.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DBXvRcU1.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.BcWSpGYy.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/p3h4sA5-.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DbtvxgAm.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"24f3c578-f02b-41bc-953a-c7ff7b504d56",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1785527485918,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.CJGrD2VA.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DBXvRcU1.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/Bwak6g3b.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.BcWSpGYy.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/p3h4sA5-.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DbtvxgAm.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"fcc4b5e8-a04c-49bd-9cb3-620fcda4d432",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1785932351899,false]</script></body></html>
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
3
  "configKey": "i18n",
4
- "version": "3.26.0",
4
+ "version": "3.27.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import fs__default, { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
3
- import path, { resolve, join, dirname } from 'node:path';
3
+ import path, { dirname, resolve, join } from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { defaultPlural, isNoPrefixStrategy, withPrefixStrategy } from '@i18n-micro/core';
6
6
  import { generateHmrPlugin } from '@i18n-micro/hmr/generate-plugin';
@@ -22,12 +22,31 @@ import { parse } from '@vue/compiler-sfc';
22
22
  const DEVTOOLS_UI_PORT = 3030;
23
23
  const DEVTOOLS_UI_ROUTE = "/__nuxt-i18n-micro";
24
24
  const DEVTOOLS_RPC_NAMESPACE = "nuxt-i18n-micro";
25
- const distDir = resolve(fileURLToPath(import.meta.url), "..");
26
- const clientDir = resolve(distDir, "client");
25
+ const thisDir = dirname(fileURLToPath(import.meta.url));
26
+ const packageRoot = resolve(thisDir, "..");
27
+ const clientDir = resolveClientDir();
28
+ function resolveClientDir() {
29
+ const candidates = [
30
+ resolve(thisDir, "client"),
31
+ // dist/client next to dist/module.mjs
32
+ resolve(thisDir, "../dist/client"),
33
+ // src/devtools.ts → repo dist/client
34
+ resolve(thisDir, "../client/dist")
35
+ // client/dist symlink used in-repo
36
+ ];
37
+ return candidates.find((dir) => fs.existsSync(path.join(dir, "index.html"))) ?? candidates[0];
38
+ }
39
+ function resolveClientPackageDir() {
40
+ return resolve(packageRoot, "client");
41
+ }
42
+ function useLiveClientDevServer() {
43
+ return process.env.I18N_DEVTOOLS_CLIENT_DEV === "1" || process.env.I18N_DEVTOOLS_CLIENT_DEV === "true";
44
+ }
27
45
  function setupDevToolsUI(options, rootDirs) {
28
46
  const nuxt = useNuxt();
29
- const hasPrebuiltClient = fs.existsSync(clientDir);
30
- if (hasPrebuiltClient) {
47
+ const hasPrebuiltClient = fs.existsSync(path.join(clientDir, "index.html"));
48
+ const liveClient = useLiveClientDevServer();
49
+ if (hasPrebuiltClient && !liveClient) {
31
50
  addDevServerHandler({
32
51
  route: DEVTOOLS_UI_ROUTE,
33
52
  handler: fromNodeMiddleware(sirv(clientDir, { dev: true, single: true }))
@@ -37,7 +56,7 @@ function setupDevToolsUI(options, rootDirs) {
37
56
  {
38
57
  command: "npx",
39
58
  args: ["nuxi", "dev", "--port", String(DEVTOOLS_UI_PORT)],
40
- cwd: resolve(distDir, "../client")
59
+ cwd: resolveClientPackageDir()
41
60
  },
42
61
  { id: "nuxt-i18n-micro:client", name: "i18n Micro DevTools UI" }
43
62
  );
@@ -46,7 +65,9 @@ function setupDevToolsUI(options, rootDirs) {
46
65
  // `addDevServerHandler` strips the mount prefix, so `event.path` arrives as `/`
47
66
  // or `/_nuxt/...` and the route has to be put back on for the client dev server,
48
67
  // which serves itself under that same base.
49
- handler: defineEventHandler((event) => proxyRequest(event, `http://localhost:${DEVTOOLS_UI_PORT}${joinURL(DEVTOOLS_UI_ROUTE, event.path)}`))
68
+ handler: defineEventHandler(
69
+ (event) => proxyRequest(event, `http://localhost:${DEVTOOLS_UI_PORT}${joinURL(DEVTOOLS_UI_ROUTE, event.path || "/")}`)
70
+ )
50
71
  });
51
72
  }
52
73
  addCustomTab({
@@ -852,6 +873,14 @@ const module$1 = defineNuxtModule({
852
873
  order: 3
853
874
  });
854
875
  }
876
+ if (nuxt.options.dev && options.plugin !== false && options.vueDevtools !== false) {
877
+ addPlugin({
878
+ src: resolver.resolve("./runtime/plugins/04.vue-devtools"),
879
+ name: "i18n-vue-devtools",
880
+ mode: "client",
881
+ order: 5
882
+ });
883
+ }
855
884
  addPlugin({
856
885
  src: resolver.resolve("./runtime/plugins/06.redirect"),
857
886
  mode: "server",
@@ -0,0 +1,11 @@
1
+ import type { ModuleOptionsExtend } from '@i18n-micro/types';
2
+ import type { Ref } from 'vue';
3
+ import type { NuxtI18n } from '../utils/nuxt-i18n.js';
4
+ export declare const I18N_DEVTOOLS_BRIDGE_KEY = "i18n-micro-devtools-bridge";
5
+ export interface I18nDevtoolsBridge {
6
+ i18n: NuxtI18n;
7
+ i18nConfig: ModuleOptionsExtend;
8
+ localeState: Ref<string | null>;
9
+ }
10
+ export declare function setI18nDevtoolsBridge(value: I18nDevtoolsBridge | null): void;
11
+ export declare function getI18nDevtoolsBridge(): I18nDevtoolsBridge | null;
@@ -0,0 +1,8 @@
1
+ export const I18N_DEVTOOLS_BRIDGE_KEY = "i18n-micro-devtools-bridge";
2
+ let bridge = null;
3
+ export function setI18nDevtoolsBridge(value) {
4
+ bridge = value;
5
+ }
6
+ export function getI18nDevtoolsBridge() {
7
+ return bridge;
8
+ }
@@ -0,0 +1,18 @@
1
+ import type { ModuleOptionsExtend } from '@i18n-micro/types';
2
+ import type { App } from 'vue';
3
+ import type { I18nContextChangeReason, NuxtI18n } from '../utils/nuxt-i18n.js';
4
+ export interface VueI18nDevtoolsContext {
5
+ app: App;
6
+ i18n: NuxtI18n;
7
+ i18nConfig: ModuleOptionsExtend;
8
+ onContextChange: (listener: (reason: I18nContextChangeReason) => void) => () => void;
9
+ onMissingKey: (listener: (locale: string, key: string, routeName: string) => void) => () => void;
10
+ }
11
+ export interface VueI18nDevtoolsNotifier {
12
+ notifyLocaleSwitch: (from: string, to: string, routeName: string) => void;
13
+ notifyLoad: (locale: string, routeName: string, keyCount: number) => void;
14
+ notifyTranslate: (key: string, result: unknown) => void;
15
+ refreshInspector: () => void;
16
+ }
17
+ export declare function setupVueI18nDevtools(ctx: VueI18nDevtoolsContext): Promise<VueI18nDevtoolsNotifier | null>;
18
+ export declare function countLoadedTranslationKeys(i18n: NuxtI18n, locale: string, routeName: string): number;
@@ -0,0 +1,261 @@
1
+ import {
2
+ buildInspectorState,
3
+ buildInspectorTree,
4
+ countTranslationKeys,
5
+ getByInspectorPath,
6
+ parseInspectorNodeId
7
+ } from "@i18n-micro/core/devtools";
8
+ const INSPECTOR_ID = "i18n-micro-inspector";
9
+ const PLUGIN_ID = "nuxt-i18n-micro";
10
+ const TIMELINE_LAYER_ID = "i18n-micro";
11
+ const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
+ function toInspectorState(snapshot) {
13
+ const groups = buildInspectorState(snapshot);
14
+ const state = {};
15
+ for (const [group, entries] of Object.entries(groups)) {
16
+ state[group] = entries.map((entry) => ({
17
+ key: entry.key,
18
+ value: entry.value
19
+ }));
20
+ }
21
+ return state;
22
+ }
23
+ function createSnapshot(i18n, i18nConfig) {
24
+ return {
25
+ locale: i18n.getCurrentLocale(),
26
+ routeName: i18n.getCurrentRouteName() || "index",
27
+ strategy: i18nConfig.strategy,
28
+ defaultLocale: i18nConfig.defaultLocale,
29
+ fallbackLocale: i18nConfig.fallbackLocale,
30
+ cachedChunks: i18n.storage.translations.size,
31
+ locales: (i18nConfig.locales ?? []).map((locale) => ({
32
+ code: locale.code,
33
+ displayName: locale.displayName,
34
+ disabled: locale.disabled
35
+ }))
36
+ };
37
+ }
38
+ function buildNodeState(i18n, i18nConfig, nodeId) {
39
+ const parsed = parseInspectorNodeId(nodeId);
40
+ if (parsed.kind === "active") {
41
+ return {
42
+ Translations: objectToStateEntries(i18n.resolveTranslations())
43
+ };
44
+ }
45
+ if (parsed.kind === "locale" && parsed.locale) {
46
+ const locale = parsed.locale;
47
+ const configured = (i18nConfig.locales ?? []).find((item) => item.code === locale);
48
+ const prefix = `${locale}:`;
49
+ const chunks = [];
50
+ for (const [cacheKey, data] of i18n.storage.translations) {
51
+ if (!cacheKey.startsWith(prefix)) continue;
52
+ const routeName = cacheKey.slice(prefix.length) || "index";
53
+ chunks.push({
54
+ key: routeName,
55
+ value: `${countTranslationKeys(data)} keys`
56
+ });
57
+ }
58
+ chunks.sort((a, b) => a.key.localeCompare(b.key));
59
+ return {
60
+ Locale: [
61
+ { key: "code", value: locale },
62
+ { key: "displayName", value: configured?.displayName ?? locale },
63
+ { key: "disabled", value: Boolean(configured?.disabled) },
64
+ { key: "isActive", value: i18n.getCurrentLocale() === locale },
65
+ { key: "loadedChunks", value: chunks.length }
66
+ ],
67
+ Chunks: chunks.length ? chunks : [{ key: "\u2014", value: "No translation chunks loaded for this locale yet" }]
68
+ };
69
+ }
70
+ if (parsed.kind === "chunk" && parsed.locale) {
71
+ const routeName = parsed.routeName || "index";
72
+ const cacheKey = `${parsed.locale}:${routeName}`;
73
+ const chunk = i18n.storage.translations.get(cacheKey);
74
+ if (!chunk) {
75
+ return {
76
+ Chunk: [
77
+ { key: "cacheKey", value: cacheKey },
78
+ { key: "status", value: "not loaded" }
79
+ ]
80
+ };
81
+ }
82
+ const entries = objectToStateEntries(chunk);
83
+ return {
84
+ Chunk: [
85
+ { key: "cacheKey", value: cacheKey },
86
+ { key: "keys", value: countTranslationKeys(chunk) },
87
+ {
88
+ key: "isActive",
89
+ value: i18n.getCurrentLocale() === parsed.locale && (i18n.getCurrentRouteName() || "index") === routeName
90
+ }
91
+ ],
92
+ Translations: entries.length ? entries : [{ key: "\u2014", value: "Chunk is loaded but empty" }]
93
+ };
94
+ }
95
+ if (parsed.kind === "key" && parsed.locale) {
96
+ const routeName = parsed.routeName || "index";
97
+ const segments = parsed.segments ?? [];
98
+ const chunk = i18n.storage.translations.get(`${parsed.locale}:${routeName}`);
99
+ const isActive = i18n.getCurrentLocale() === parsed.locale && (i18n.getCurrentRouteName() || "index") === routeName;
100
+ const source = isActive ? i18n.resolveTranslations() : chunk ?? {};
101
+ const value = segments.length ? getByInspectorPath(source, segments) : source;
102
+ if (isPlainObject(value)) {
103
+ return { [segments.join(".") || "root"]: objectToStateEntries(value) };
104
+ }
105
+ return {
106
+ Value: [{ key: segments.join(".") || "value", value }]
107
+ };
108
+ }
109
+ return toInspectorState(createSnapshot(i18n, i18nConfig));
110
+ }
111
+ function objectToStateEntries(obj, limit = 500) {
112
+ const entries = [];
113
+ let truncated = false;
114
+ const queue = [{ node: obj, prefix: "" }];
115
+ while (queue.length > 0) {
116
+ const { node, prefix } = queue.shift();
117
+ const nested = [];
118
+ for (const key of Object.keys(node)) {
119
+ if (key === "__proto__") continue;
120
+ if (entries.length >= limit) {
121
+ truncated = true;
122
+ break;
123
+ }
124
+ const path = prefix ? `${prefix}.${key}` : key;
125
+ const value = node[key];
126
+ if (isPlainObject(value)) {
127
+ nested.push({ node: value, prefix: path });
128
+ continue;
129
+ }
130
+ entries.push({ key: path, value });
131
+ }
132
+ if (truncated) break;
133
+ queue.push(...nested);
134
+ }
135
+ entries.sort((a, b) => a.key.localeCompare(b.key));
136
+ if (truncated) {
137
+ entries.push({
138
+ key: "\u2026",
139
+ value: `Showing first ${limit} leaves (nested keys flattened)`
140
+ });
141
+ }
142
+ return entries;
143
+ }
144
+ export async function setupVueI18nDevtools(ctx) {
145
+ if (!import.meta.dev || import.meta.server) {
146
+ return null;
147
+ }
148
+ let notifier = null;
149
+ try {
150
+ const { setupDevtoolsPlugin } = await import("@vue/devtools-api");
151
+ setupDevtoolsPlugin(
152
+ {
153
+ id: PLUGIN_ID,
154
+ label: "i18n Micro",
155
+ packageName: "nuxt-i18n-micro",
156
+ homepage: "https://s00d.github.io/nuxt-i18n-micro/",
157
+ logo: "https://s00d.github.io/nuxt-i18n-micro/favicon.svg",
158
+ app: ctx.app,
159
+ enableEarlyProxy: true
160
+ },
161
+ (api) => {
162
+ const refreshInspector = () => {
163
+ api.sendInspectorTree(INSPECTOR_ID);
164
+ api.sendInspectorState(INSPECTOR_ID);
165
+ };
166
+ api.addInspector({
167
+ id: INSPECTOR_ID,
168
+ label: "i18n Micro",
169
+ icon: "carbon:language",
170
+ actions: [
171
+ {
172
+ icon: "refresh",
173
+ tooltip: "Refresh inspector",
174
+ action: refreshInspector
175
+ }
176
+ ]
177
+ });
178
+ api.addTimelineLayer({
179
+ id: TIMELINE_LAYER_ID,
180
+ label: "i18n Micro",
181
+ color: 4372611
182
+ });
183
+ api.on.getInspectorTree((payload) => {
184
+ if (payload.inspectorId !== INSPECTOR_ID) return;
185
+ payload.rootNodes = buildInspectorTree({
186
+ nodeId: "root",
187
+ storage: ctx.i18n.storage,
188
+ configuredLocales: ctx.i18nConfig.locales ?? [],
189
+ activeLocale: ctx.i18n.getCurrentLocale(),
190
+ activeRouteName: ctx.i18n.getCurrentRouteName() || "index",
191
+ activeTranslations: ctx.i18n.resolveTranslations()
192
+ });
193
+ });
194
+ api.on.getInspectorState((payload) => {
195
+ if (payload.inspectorId !== INSPECTOR_ID) return;
196
+ payload.state = payload.nodeId ? buildNodeState(ctx.i18n, ctx.i18nConfig, payload.nodeId) : toInspectorState(createSnapshot(ctx.i18n, ctx.i18nConfig));
197
+ });
198
+ ctx.onContextChange(() => {
199
+ refreshInspector();
200
+ });
201
+ ctx.onMissingKey((locale, key, routeName) => {
202
+ api.addTimelineEvent({
203
+ layerId: TIMELINE_LAYER_ID,
204
+ event: {
205
+ time: api.now(),
206
+ title: "Missing translation",
207
+ subtitle: key,
208
+ data: { locale, key, routeName }
209
+ }
210
+ });
211
+ });
212
+ notifier = {
213
+ notifyLocaleSwitch(from, to, routeName) {
214
+ api.addTimelineEvent({
215
+ layerId: TIMELINE_LAYER_ID,
216
+ event: {
217
+ time: api.now(),
218
+ title: "Locale switch",
219
+ subtitle: `${from || "\u2014"} \u2192 ${to}`,
220
+ data: { from, to, routeName }
221
+ }
222
+ });
223
+ refreshInspector();
224
+ },
225
+ notifyLoad(locale, routeName, keyCount) {
226
+ api.addTimelineEvent({
227
+ layerId: TIMELINE_LAYER_ID,
228
+ event: {
229
+ time: api.now(),
230
+ title: "Translations loaded",
231
+ subtitle: `${locale}:${routeName}`,
232
+ data: { locale, routeName, keyCount }
233
+ }
234
+ });
235
+ refreshInspector();
236
+ },
237
+ notifyTranslate(key, result) {
238
+ api.addTimelineEvent({
239
+ layerId: TIMELINE_LAYER_ID,
240
+ event: {
241
+ time: api.now(),
242
+ title: "$t",
243
+ subtitle: key,
244
+ data: { key, result }
245
+ }
246
+ });
247
+ },
248
+ refreshInspector
249
+ };
250
+ refreshInspector();
251
+ }
252
+ );
253
+ } catch {
254
+ return null;
255
+ }
256
+ return notifier;
257
+ }
258
+ export function countLoadedTranslationKeys(i18n, locale, routeName) {
259
+ const chunk = i18n.storage.translations.get(i18n.getCacheKey(locale, routeName));
260
+ return countTranslationKeys(chunk);
261
+ }
@@ -7,6 +7,7 @@ import { createError, defineNuxtPlugin, navigateTo, useRouter, useRuntimeConfig
7
7
  import { useI18nHead } from "../composables/useI18nHead.js";
8
8
  import { useI18nLocale } from "../composables/useI18nLocale.js";
9
9
  import { createNuxtI18nPluginApi, NuxtI18n, NuxtTranslationLoader } from "../utils/nuxt-i18n.js";
10
+ import { setI18nDevtoolsBridge } from "../devtools/bridge.js";
10
11
  import { translationStorage } from "../utils/storage.js";
11
12
  const isDev = process.env.NODE_ENV !== "production";
12
13
  export default defineNuxtPlugin(async (nuxtApp) => {
@@ -102,6 +103,13 @@ export default defineNuxtPlugin(async (nuxtApp) => {
102
103
  customMissingHandler.value = handler;
103
104
  }
104
105
  });
106
+ if (import.meta.dev && import.meta.client) {
107
+ setI18nDevtoolsBridge({
108
+ i18n,
109
+ i18nConfig,
110
+ localeState
111
+ });
112
+ }
105
113
  router.beforeEach(async (to, from) => {
106
114
  if (to.name !== from.name) {
107
115
  i18nRouteParams.value = {};
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,60 @@
1
+ import { defineNuxtPlugin, useNuxtApp } from "#imports";
2
+ import { watch } from "vue";
3
+ import { getI18nDevtoolsBridge } from "../devtools/bridge.js";
4
+ import { countLoadedTranslationKeys, setupVueI18nDevtools } from "../devtools/vue-devtools.js";
5
+ export default defineNuxtPlugin({
6
+ name: "i18n-vue-devtools",
7
+ dependsOn: ["i18n-plugin-loader"],
8
+ async setup() {
9
+ if (!import.meta.dev || import.meta.server) {
10
+ return;
11
+ }
12
+ const nuxtApp = useNuxtApp();
13
+ const bridge = getI18nDevtoolsBridge();
14
+ if (!bridge) {
15
+ return;
16
+ }
17
+ const notifier = await setupVueI18nDevtools({
18
+ app: nuxtApp.vueApp,
19
+ i18n: bridge.i18n,
20
+ i18nConfig: bridge.i18nConfig,
21
+ onContextChange: (listener) => bridge.i18n.onContextChange(listener),
22
+ onMissingKey: (listener) => bridge.i18n.onMissingKey(listener)
23
+ });
24
+ if (!notifier) {
25
+ return;
26
+ }
27
+ const notifyCurrentLoad = () => {
28
+ const locale = bridge.i18n.getCurrentLocale();
29
+ const routeName = bridge.i18n.getCurrentRouteName() || "index";
30
+ notifier.notifyLoad(locale, routeName, countLoadedTranslationKeys(bridge.i18n, locale, routeName));
31
+ };
32
+ notifyCurrentLoad();
33
+ watch(
34
+ () => bridge.localeState.value,
35
+ (to, from) => {
36
+ if (to && from && to !== from) {
37
+ notifier.notifyLocaleSwitch(from, to, bridge.i18n.getCurrentRouteName() || "index");
38
+ }
39
+ }
40
+ );
41
+ bridge.i18n.onContextChange((reason) => {
42
+ if (reason === "load") {
43
+ notifyCurrentLoad();
44
+ return;
45
+ }
46
+ notifier.refreshInspector();
47
+ });
48
+ if (bridge.i18nConfig.debug) {
49
+ const wrapTranslate = (translate) => ((key, ...args) => {
50
+ const result = translate(key, ...args);
51
+ notifier.notifyTranslate(key, result);
52
+ return result;
53
+ });
54
+ const app = nuxtApp;
55
+ app.$t = wrapTranslate(app.$t.bind(app));
56
+ app.$ts = wrapTranslate(app.$ts.bind(app));
57
+ app.$tc = wrapTranslate(app.$tc.bind(app));
58
+ }
59
+ }
60
+ });
@@ -10,6 +10,8 @@ export interface RouteTranslationContext {
10
10
  }
11
11
  export type GetRouteTranslationContext = (route?: unknown) => RouteTranslationContext;
12
12
  export type LoadTranslationsChunk = (locale: string, routeName?: string) => Promise<Record<string, unknown>>;
13
+ /** Why the active translation context changed — used by Vue DevTools timeline. */
14
+ export type I18nContextChangeReason = 'load' | 'refresh';
13
15
  export interface NuxtI18nOptions extends BaseI18nOptions {
14
16
  }
15
17
  export declare class NuxtI18n extends BaseI18n {
@@ -26,6 +28,8 @@ export declare class NuxtI18n extends BaseI18n {
26
28
  private currentLocale;
27
29
  private currentRouteName;
28
30
  private resolveRouteContext?;
31
+ private readonly contextListeners;
32
+ private missingKeyListener?;
29
33
  constructor(options?: NuxtI18nOptions);
30
34
  setRouteContextResolver(resolver: GetRouteTranslationContext): void;
31
35
  getCacheKey(locale: string, routeName?: string): string;
@@ -48,6 +52,9 @@ export declare class NuxtI18n extends BaseI18n {
48
52
  getRoute(): string;
49
53
  getCurrentLocale(): string;
50
54
  getCurrentRouteName(): string;
55
+ onContextChange(listener: (reason: I18nContextChangeReason) => void): () => void;
56
+ onMissingKey(listener: (locale: string, key: string, routeName: string) => void): () => void;
57
+ private notifyContextChange;
51
58
  /**
52
59
  * Hot-reload the active dictionary for the next route — replace, don't merge.
53
60
  *
@@ -82,11 +89,11 @@ export declare class NuxtI18n extends BaseI18n {
82
89
  * first makes the change vanish on the next navigation.
83
90
  */
84
91
  setTranslation(key: TranslationKey, value: unknown): void;
85
- protected getMissingContext(_routeContext?: unknown): {
92
+ protected getMissingContext(routeContext?: unknown): {
86
93
  locale: string;
87
94
  routeName: string;
88
95
  };
89
- protected warnMissing(key: TranslationKey): void;
96
+ protected warnMissing(key: TranslationKey, routeContext?: unknown): void;
90
97
  clearCache(): void;
91
98
  }
92
99
  export interface NuxtTranslationLoaderOptions {
@@ -44,6 +44,8 @@ export class NuxtI18n extends BaseI18n {
44
44
  currentLocale = "";
45
45
  currentRouteName = "";
46
46
  resolveRouteContext;
47
+ contextListeners = /* @__PURE__ */ new Set();
48
+ missingKeyListener;
47
49
  constructor(options = {}) {
48
50
  const storage = {
49
51
  translations: /* @__PURE__ */ new Map()
@@ -105,6 +107,25 @@ export class NuxtI18n extends BaseI18n {
105
107
  getCurrentRouteName() {
106
108
  return this.currentRouteName;
107
109
  }
110
+ onContextChange(listener) {
111
+ this.contextListeners.add(listener);
112
+ return () => {
113
+ this.contextListeners.delete(listener);
114
+ };
115
+ }
116
+ onMissingKey(listener) {
117
+ this.missingKeyListener = listener;
118
+ return () => {
119
+ if (this.missingKeyListener === listener) {
120
+ this.missingKeyListener = void 0;
121
+ }
122
+ };
123
+ }
124
+ notifyContextChange(reason) {
125
+ for (const listener of this.contextListeners) {
126
+ listener(reason);
127
+ }
128
+ }
108
129
  /**
109
130
  * Hot-reload the active dictionary for the next route — replace, don't merge.
110
131
  *
@@ -122,23 +143,27 @@ export class NuxtI18n extends BaseI18n {
122
143
  this.currentLocale = locale;
123
144
  this.currentRouteName = routeName || "";
124
145
  triggerRef(this.contextSignal);
146
+ this.notifyContextChange("load");
125
147
  }
126
148
  /** The outgoing page has unmounted, so its keys must stop resolving. */
127
149
  finishTransition() {
128
150
  if (this.outgoingTranslations === null) return;
129
151
  this.outgoingTranslations = null;
130
152
  triggerRef(this.contextSignal);
153
+ this.notifyContextChange("refresh");
131
154
  }
132
155
  mergeTranslations(newTranslations) {
133
156
  this.mergeChunk(this.currentLocale, this.currentRouteName, newTranslations);
134
157
  this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations);
135
158
  triggerRef(this.contextSignal);
159
+ this.notifyContextChange("load");
136
160
  }
137
161
  async loadPageTranslations(locale, routeName, translations) {
138
162
  this.mergeChunk(locale, routeName, translations);
139
163
  if (locale === this.currentLocale && routeName === this.currentRouteName) {
140
164
  this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, translations);
141
165
  triggerRef(this.contextSignal);
166
+ this.notifyContextChange("load");
142
167
  }
143
168
  }
144
169
  async mergeTranslationAsync(locale, routeName, newTranslations, loadIfMissing) {
@@ -150,6 +175,7 @@ export class NuxtI18n extends BaseI18n {
150
175
  if (locale === this.currentLocale && routeName === this.currentRouteName) {
151
176
  this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations);
152
177
  triggerRef(this.contextSignal);
178
+ this.notifyContextChange("load");
153
179
  }
154
180
  }
155
181
  tForRoute(route) {
@@ -201,18 +227,24 @@ export class NuxtI18n extends BaseI18n {
201
227
  setTranslationAtKey(this.getChunk(this.currentLocale, this.currentRouteName), String(key), value)
202
228
  );
203
229
  triggerRef(this.contextSignal);
230
+ this.notifyContextChange("refresh");
204
231
  }
205
- getMissingContext(_routeContext) {
232
+ getMissingContext(routeContext) {
233
+ if (routeContext && this.resolveRouteContext) {
234
+ return this.resolveRouteContext(routeContext);
235
+ }
206
236
  return { locale: this.currentLocale, routeName: this.currentRouteName };
207
237
  }
208
- warnMissing(key) {
238
+ warnMissing(key, routeContext) {
239
+ const { locale, routeName } = this.getMissingContext(routeContext);
240
+ this.missingKeyListener?.(locale, String(key), routeName);
209
241
  const customHandler = this.getCustomMissingHandler?.();
210
242
  if (customHandler) {
211
- customHandler(this.currentLocale, String(key), this.currentRouteName);
243
+ customHandler(locale, String(key), routeName);
212
244
  return;
213
245
  }
214
246
  if (this.missingWarn && process.env.NODE_ENV !== "production" && import.meta.client) {
215
- console.warn(`[i18n] Missing key '${key}' in '${this.currentLocale}' for route '${this.currentRouteName}'`);
247
+ console.warn(`[i18n] Missing key '${key}' in '${locale}' for route '${routeName}'`);
216
248
  }
217
249
  }
218
250
  clearCache() {
@@ -220,6 +252,7 @@ export class NuxtI18n extends BaseI18n {
220
252
  this.cachedTranslations = {};
221
253
  this.outgoingTranslations = null;
222
254
  triggerRef(this.contextSignal);
255
+ this.notifyContextChange("refresh");
223
256
  }
224
257
  }
225
258
  export class NuxtTranslationLoader {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
- "version": "3.26.0",
4
- "description": "Nuxt I18n Micro is a lightweight, high-performance internationalization module for Nuxt, designed to handle multi-language support with minimal overhead, fast build times, and efficient runtime performance.",
3
+ "version": "3.27.0",
4
+ "description": "Fast, lightweight i18n for Nuxt with strategy-based routing and minimal overhead.",
5
5
  "keywords": [
6
6
  "i18n",
7
7
  "internationalization",
@@ -14,7 +14,7 @@
14
14
  "seo",
15
15
  "translation"
16
16
  ],
17
- "homepage": "https://github.com/s00d/nuxt-i18n-micro",
17
+ "homepage": "https://s00d.github.io/nuxt-i18n-micro/",
18
18
  "bugs": {
19
19
  "url": "https://github.com/s00d/nuxt-i18n-micro/issues"
20
20
  },
@@ -27,6 +27,7 @@
27
27
  "repository": "s00d/nuxt-i18n-micro",
28
28
  "workspaces": [
29
29
  "client",
30
+ "docs",
30
31
  "packages/**",
31
32
  "test/fixtures/**/*"
32
33
  ],
@@ -81,38 +82,38 @@
81
82
  "sirv": "^2.0.3",
82
83
  "ufo": "^1.5.4",
83
84
  "unplugin": "^1.16.1 || ^2.0.0 || ^3.3.0",
84
- "@i18n-micro/path-strategy": "^1.3.9",
85
- "@i18n-micro/test-utils": "^1.2.8",
86
- "@i18n-micro/core": "^1.3.11",
87
- "@i18n-micro/hmr": "^1.0.6",
88
- "@i18n-micro/types": "^1.2.10",
89
- "@i18n-micro/utils": "^1.0.13",
90
- "@i18n-micro/route-strategy": "^1.1.14"
85
+ "@i18n-micro/core": "^1.3.13",
86
+ "@i18n-micro/path-strategy": "^1.3.10",
87
+ "@i18n-micro/route-strategy": "^1.1.15",
88
+ "@i18n-micro/types": "^1.2.11",
89
+ "@i18n-micro/utils": "^1.0.14",
90
+ "@i18n-micro/test-utils": "^1.2.9",
91
+ "@i18n-micro/hmr": "^1.0.7"
91
92
  },
92
93
  "devDependencies": {
93
94
  "@nuxt/devtools": "^3.3.1",
94
95
  "@nuxt/devtools-ui-kit": "^3.3.1",
96
+ "@vue/devtools-api": "^8.1.5",
95
97
  "@nuxt/module-builder": "^1.0.0",
96
98
  "@nuxt/schema": "^3.18.1 || ^4.0.0",
97
99
  "@nuxt/test-utils": "^4.0.3",
98
100
  "@playwright/test": "^1.55.0",
99
101
  "@types/node": "^20.19.26",
100
102
  "changelogen": "^0.5.7",
101
- "chart.js": "^4.5.1",
102
- "mermaid": "^11.12.2",
103
103
  "oxfmt": "^0.43.0",
104
104
  "oxlint": "^1.58.0",
105
105
  "typescript": "^5.9.3",
106
- "vitepress": "^2.0.0-alpha.18",
107
- "vitepress-plugin-chartjs": "^1.2.1",
108
- "vitepress-plugin-folder-tree": "^1.2.1",
109
- "vitepress-plugin-llms": "^1.13.3",
110
- "vitepress-plugin-mermaid": "^2.0.17",
111
106
  "vitest": "^4.1.10",
112
107
  "vue-tsc": "^2.0.21"
113
108
  },
114
109
  "peerDependencies": {
115
- "vue-router": "^4.6.3 || ^5.0.0"
110
+ "vue-router": "^4.6.3 || ^5.0.0",
111
+ "@vue/devtools-api": "^8.1.5"
112
+ },
113
+ "peerDependenciesMeta": {
114
+ "@vue/devtools-api": {
115
+ "optional": true
116
+ }
116
117
  },
117
118
  "optionalDependencies": {
118
119
  "@rollup/rollup-linux-x64-gnu": "^4.28.1",
@@ -161,11 +162,11 @@
161
162
  "test:dist:packages": "pnpm --filter \"@i18n-micro/core\" --filter \"@i18n-micro/utils\" --filter \"@i18n-micro/hmr\" --filter \"@i18n-micro/vue\" --filter \"@i18n-micro/path-strategy\" --filter \"@i18n-micro/route-strategy\" --filter \"@i18n-micro/devtools-ui\" --filter \"@i18n-micro/astro\" run test:dist",
162
163
  "typecheck": "tsc --noEmit",
163
164
  "typecheck:nuxt": "nuxt typecheck --no-emit",
164
- "docs:dev": "pnpm run docs:generate && vitepress dev docs",
165
- "docs:build": "pnpm run docs:generate && vitepress build docs",
166
- "docs:serve": "vitepress serve docs",
167
- "docs:clean-dev": "pnpm run docs:generate && vitepress dev docs --clean-cache",
168
- "docs:preview": "vitepress preview docs",
165
+ "docs:dev": "pnpm --filter @i18n-micro/docs dev",
166
+ "docs:build": "pnpm --filter @i18n-micro/docs build",
167
+ "docs:serve": "pnpm --filter @i18n-micro/docs preview",
168
+ "docs:clean-dev": "pnpm --filter @i18n-micro/docs clean-dev",
169
+ "docs:preview": "pnpm --filter @i18n-micro/docs preview",
169
170
  "smoke:pack": "pnpm -C scripts cli smoke-pack",
170
171
  "smoke:verify": "pnpm -C scripts cli smoke-verify",
171
172
  "smoke:browser": "pnpm -C scripts cli smoke-browser",
@@ -1 +0,0 @@
1
- {"id":"24f3c578-f02b-41bc-953a-c7ff7b504d56","timestamp":1785527483273,"prerendered":[]}