From 79dad8aacc4a610727dbee80e3308dc3b7091c1b Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:19:47 +0500 Subject: [PATCH 1/8] chore(docs): extract VitePress into workspace package Move docs deps out of root so the module package stays lean. Co-authored-by: Cursor --- docs/package.json | 24 ++++++ package.json | 31 +++---- pnpm-lock.yaml | 203 ++++++++++++++++++-------------------------- pnpm-workspace.yaml | 3 +- 4 files changed, 125 insertions(+), 136 deletions(-) create mode 100644 docs/package.json diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..a91c4cc7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,24 @@ +{ + "name": "@i18n-micro/docs", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "VitePress documentation site for nuxt-i18n-micro.", + "scripts": { + "generate": "pnpm -C ../scripts cli docs-generate", + "dev": "pnpm run generate && vitepress dev .", + "build": "pnpm run generate && vitepress build .", + "serve": "vitepress serve .", + "clean-dev": "pnpm run generate && vitepress dev . --clean-cache", + "preview": "vitepress preview ." + }, + "devDependencies": { + "chart.js": "catalog:", + "mermaid": "catalog:", + "vitepress": "catalog:", + "vitepress-plugin-chartjs": "catalog:", + "vitepress-plugin-folder-tree": "catalog:", + "vitepress-plugin-llms": "catalog:", + "vitepress-plugin-mermaid": "catalog:" + } +} diff --git a/package.json b/package.json index 1ac2011c..a9fdcc72 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nuxt-i18n-micro", "version": "3.26.0", - "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.", + "description": "Fast, lightweight i18n for Nuxt with strategy-based routing and minimal overhead.", "keywords": [ "i18n", "internationalization", @@ -14,7 +14,7 @@ "seo", "translation" ], - "homepage": "https://github.com/s00d/nuxt-i18n-micro", + "homepage": "https://s00d.github.io/nuxt-i18n-micro/", "bugs": { "url": "https://github.com/s00d/nuxt-i18n-micro/issues" }, @@ -27,6 +27,7 @@ "repository": "s00d/nuxt-i18n-micro", "workspaces": [ "client", + "docs", "packages/**", "test/fixtures/**/*" ], @@ -109,11 +110,11 @@ "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", "typecheck": "tsc --noEmit", "typecheck:nuxt": "nuxt typecheck --no-emit", - "docs:dev": "pnpm run docs:generate && vitepress dev docs", - "docs:build": "pnpm run docs:generate && vitepress build docs", - "docs:serve": "vitepress serve docs", - "docs:clean-dev": "pnpm run docs:generate && vitepress dev docs --clean-cache", - "docs:preview": "vitepress preview docs", + "docs:dev": "pnpm --filter @i18n-micro/docs dev", + "docs:build": "pnpm --filter @i18n-micro/docs build", + "docs:serve": "pnpm --filter @i18n-micro/docs serve", + "docs:clean-dev": "pnpm --filter @i18n-micro/docs clean-dev", + "docs:preview": "pnpm --filter @i18n-micro/docs preview", "smoke:pack": "pnpm -C scripts cli smoke-pack", "smoke:verify": "pnpm -C scripts cli smoke-verify", "smoke:browser": "pnpm -C scripts cli smoke-browser", @@ -149,27 +150,27 @@ "devDependencies": { "@nuxt/devtools": "catalog:", "@nuxt/devtools-ui-kit": "catalog:", + "@vue/devtools-api": "catalog:", "@nuxt/module-builder": "catalog:", "@nuxt/schema": "catalog:", "@nuxt/test-utils": "catalog:", "@playwright/test": "catalog:", "@types/node": "catalog:", "changelogen": "catalog:", - "chart.js": "catalog:", - "mermaid": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", "typescript": "catalog:", - "vitepress": "catalog:", - "vitepress-plugin-chartjs": "catalog:", - "vitepress-plugin-folder-tree": "catalog:", - "vitepress-plugin-llms": "catalog:", - "vitepress-plugin-mermaid": "catalog:", "vitest": "catalog:", "vue-tsc": "catalog:" }, "peerDependencies": { - "vue-router": "catalog:" + "vue-router": "catalog:", + "@vue/devtools-api": "^8.1.5" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": true + } }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11fbd9b6..5179d00f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,8 +100,8 @@ catalogs: specifier: ^3.5.25 version: 3.5.27 '@vue/devtools-api': - specifier: ^7.0.0 - version: 7.7.2 + specifier: ^8.1.5 + version: 8.2.1 '@vue/server-renderer': specifier: ^3.3.0 version: 3.5.25 @@ -345,22 +345,19 @@ importers: version: 4.5.0 '@nuxt/test-utils': specifier: 'catalog:' - version: 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) + version: 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) '@playwright/test': specifier: 'catalog:' version: 1.55.0 '@types/node': specifier: 'catalog:' version: 20.19.26 + '@vue/devtools-api': + specifier: 'catalog:' + version: 8.2.1 changelogen: specifier: 'catalog:' version: 0.5.7(magicast@0.3.5) - chart.js: - specifier: 'catalog:' - version: 4.5.1 - mermaid: - specifier: 'catalog:' - version: 11.12.2 oxfmt: specifier: 'catalog:' version: 0.43.0 @@ -370,24 +367,9 @@ importers: typescript: specifier: 'catalog:' version: 5.9.3 - vitepress: - specifier: 'catalog:' - version: 2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) - vitepress-plugin-chartjs: - specifier: 'catalog:' - version: 1.2.1(chart.js@4.5.1)(vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) - vitepress-plugin-folder-tree: - specifier: 'catalog:' - version: 1.2.1(vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) - vitepress-plugin-llms: - specifier: 'catalog:' - version: 1.13.3 - vitepress-plugin-mermaid: - specifier: 'catalog:' - version: 2.0.17(mermaid@11.12.2)(vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vue-tsc: specifier: 'catalog:' version: 2.2.12(typescript@5.9.3) @@ -405,6 +387,30 @@ importers: specifier: 'catalog:' version: 0.18.1(vue@3.5.40(typescript@5.9.3)) + docs: + devDependencies: + chart.js: + specifier: 'catalog:' + version: 4.5.1 + mermaid: + specifier: 'catalog:' + version: 11.12.2 + vitepress: + specifier: 'catalog:' + version: 2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + vitepress-plugin-chartjs: + specifier: 'catalog:' + version: 1.2.1(chart.js@4.5.1)(vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + vitepress-plugin-folder-tree: + specifier: 'catalog:' + version: 1.2.1(vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + vitepress-plugin-llms: + specifier: 'catalog:' + version: 1.13.3 + vitepress-plugin-mermaid: + specifier: 'catalog:' + version: 2.0.17(mermaid@11.12.2)(vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)) + packages/astro: dependencies: '@i18n-micro/core': @@ -443,7 +449,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vue: specifier: 'catalog:' version: 3.5.25(typescript@5.9.3) @@ -511,7 +517,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/devtools-ui: dependencies: @@ -548,7 +554,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vue: specifier: 'catalog:' version: 3.5.25(typescript@5.9.3) @@ -576,7 +582,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/node: dependencies: @@ -604,7 +610,7 @@ importers: version: 4.5.4(@types/node@20.19.26)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/node/playground: dependencies: @@ -648,7 +654,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/preact: dependencies: @@ -682,7 +688,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/preact/playground: dependencies: @@ -759,7 +765,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/react/playground: dependencies: @@ -827,7 +833,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/solid: dependencies: @@ -861,7 +867,7 @@ importers: version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/solid/playground: dependencies: @@ -911,7 +917,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/types: devDependencies: @@ -926,7 +932,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/types-generator: dependencies: @@ -960,7 +966,7 @@ importers: version: 4.5.4(@types/node@20.19.26)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/utils: dependencies: @@ -985,7 +991,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) packages/vue: dependencies: @@ -1016,7 +1022,7 @@ importers: version: 4.5.4(@types/node@26.1.2)(rollup@4.62.3)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vue: specifier: 'catalog:' version: 3.5.25(typescript@5.9.3) @@ -1059,7 +1065,7 @@ importers: version: 6.0.3(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.25(typescript@5.9.3)) '@vue/devtools-api': specifier: 'catalog:' - version: 7.7.2 + version: 8.2.1 '@vue/server-renderer': specifier: 'catalog:' version: 3.5.25(vue@3.5.25(typescript@5.9.3)) @@ -1098,7 +1104,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) vue-docgen-api: specifier: 'catalog:' version: 4.79.2(vue@3.5.40(typescript@5.9.3)) @@ -1110,7 +1116,7 @@ importers: dependencies: '@nuxt/test-utils': specifier: 'catalog:' - version: 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) + version: 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) nuxt: specifier: 'catalog:' version: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@netlify/blobs@9.1.2)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.40)(better-sqlite3@12.2.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.2.0))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.141.0)(oxlint@1.69.0(oxlint-tsgolint@0.24.0))(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.62.3))(rollup@4.62.3)(sass@1.94.2)(srvx@0.12.5)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vue-tsc@3.0.6(typescript@5.9.3))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25))(yaml@2.9.0) @@ -3627,6 +3633,10 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -6154,9 +6164,6 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-api@7.7.2': - resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==} - '@vue/devtools-api@8.2.1': resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} @@ -6165,15 +6172,9 @@ packages: peerDependencies: vue: ^3.0.0 - '@vue/devtools-kit@7.7.9': - resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - '@vue/devtools-kit@8.2.1': resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} - '@vue/devtools-shared@7.7.9': - resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - '@vue/devtools-shared@8.2.1': resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} @@ -7129,10 +7130,6 @@ packages: resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} engines: {node: '>=22'} - copy-anything@3.0.5: - resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} - engines: {node: '>=12.13'} - core-js-pure@3.47.0: resolution: {integrity: sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==} @@ -11017,9 +11014,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -11361,10 +11355,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - speakingurl@14.0.1: - resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} - engines: {node: '>=0.10.0'} - splitpanes@4.1.2: resolution: {integrity: sha512-frNchoCv7w0nMQEuaDGgMGMU6jv3NhN6bBGQVfCNgwJdVcYaRmi1dwYDjp7SifAoUFdiQjBRB254LJNidzo15Q==} peerDependencies: @@ -11508,10 +11498,6 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - superjson@2.2.2: - resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} - engines: {node: '>=16'} - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -17103,7 +17089,7 @@ snapshots: rc9: 3.0.1 std-env: 4.2.0 - '@nuxt/test-utils@4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25))': + '@nuxt/test-utils@4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25))': dependencies: '@clack/prompts': 1.2.0 '@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -17132,14 +17118,14 @@ snapshots: tinyexec: 1.2.4 ufo: 1.6.4 unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.1)(rollup@4.62.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) - vitest-environment-nuxt: 2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) + vitest-environment-nuxt: 2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) vue: 3.5.40(typescript@5.9.3) optionalDependencies: '@playwright/test': 1.55.0 '@vue/test-utils': 2.4.6 jsdom: 29.1.1 playwright-core: 1.55.0 - vitest: 4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -17154,7 +17140,7 @@ snapshots: - vite - webpack - '@nuxt/test-utils@4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25))': + '@nuxt/test-utils@4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25))': dependencies: '@clack/prompts': 1.2.0 '@nuxt/devtools-kit': 2.7.0(magicast@0.5.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -17183,14 +17169,14 @@ snapshots: tinyexec: 1.2.4 ufo: 1.6.4 unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.1)(rollup@4.62.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) - vitest-environment-nuxt: 2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) + vitest-environment-nuxt: 2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) vue: 3.5.40(typescript@5.9.3) optionalDependencies: '@playwright/test': 1.55.0 '@vue/test-utils': 2.4.6 jsdom: 29.1.1 playwright-core: 1.55.0 - vitest: 4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -18143,6 +18129,9 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@opentelemetry/api@1.9.0': + optional: true + '@oslojs/encoding@1.1.0': {} '@oxc-parser/binding-android-arm-eabi@0.131.0': @@ -20807,10 +20796,6 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/devtools-api@7.7.2': - dependencies: - '@vue/devtools-kit': 7.7.9 - '@vue/devtools-api@8.2.1': dependencies: '@vue/devtools-kit': 8.2.1 @@ -20827,16 +20812,6 @@ snapshots: '@vue/devtools-shared': 8.2.1 vue: 3.5.40(typescript@5.9.3) - '@vue/devtools-kit@7.7.9': - dependencies: - '@vue/devtools-shared': 7.7.9 - birpc: 2.9.0 - hookable: 5.5.3 - mitt: 3.0.1 - perfect-debounce: 1.0.0 - speakingurl: 14.0.1 - superjson: 2.2.2 - '@vue/devtools-kit@8.2.1': dependencies: '@vue/devtools-shared': 8.2.1 @@ -20844,10 +20819,6 @@ snapshots: hookable: 5.5.3 perfect-debounce: 2.1.0 - '@vue/devtools-shared@7.7.9': - dependencies: - rfdc: 1.4.1 - '@vue/devtools-shared@8.2.1': {} '@vue/language-core@2.2.0(typescript@5.9.3)': @@ -22079,10 +22050,6 @@ snapshots: cookie@2.0.1: {} - copy-anything@3.0.5: - dependencies: - is-what: 4.1.16 - core-js-pure@3.47.0: {} core-util-is@1.0.3: {} @@ -28924,8 +28891,6 @@ snapshots: reusify@1.0.4: {} - rfdc@1.4.1: {} - robust-predicates@3.0.2: {} rolldown-string@0.3.1(rolldown@1.2.1): @@ -29507,8 +29472,6 @@ snapshots: space-separated-tokens@2.0.2: {} - speakingurl@14.0.1: {} - splitpanes@4.1.2(vue@3.5.40(typescript@5.9.3)): dependencies: vue: 3.5.40(typescript@5.9.3) @@ -29642,10 +29605,6 @@ snapshots: stylis@4.3.6: {} - superjson@2.2.2: - dependencies: - copy-anything: 3.0.5 - supports-color@10.2.2: {} supports-color@7.2.0: @@ -31275,16 +31234,16 @@ snapshots: optionalDependencies: vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) - vitepress-plugin-chartjs@1.2.1(chart.js@4.5.1)(vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + vitepress-plugin-chartjs@1.2.1(chart.js@4.5.1)(vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): dependencies: chart.js: 4.5.1 - vitepress: 2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + vitepress: 2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) yaml: 2.9.0 zod: 4.4.3 - vitepress-plugin-folder-tree@1.2.1(vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + vitepress-plugin-folder-tree@1.2.1(vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): dependencies: - vitepress: 2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + vitepress: 2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) yaml: 2.9.0 zod: 4.4.3 @@ -31307,14 +31266,14 @@ snapshots: transitivePeerDependencies: - supports-color - vitepress-plugin-mermaid@2.0.17(mermaid@11.12.2)(vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): + vitepress-plugin-mermaid@2.0.17(mermaid@11.12.2)(vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)): dependencies: mermaid: 11.12.2 - vitepress: 2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + vitepress: 2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) optionalDependencies: '@mermaid-js/mermaid-mindmap': 9.3.0 - vitepress@2.0.0-alpha.18(@types/node@20.19.26)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): + vitepress@2.0.0-alpha.18(@types/node@26.1.2)(axios@1.13.2)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(jiti@2.7.0)(jwt-decode@4.0.0)(postcss@8.5.25)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): dependencies: '@docsearch/css': 4.6.3 '@docsearch/js': 4.6.3 @@ -31324,7 +31283,7 @@ snapshots: '@shikijs/transformers': 4.3.1 '@shikijs/types': 4.3.1 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 6.0.8(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.8(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.9.3)) '@vue/devtools-api': 8.2.1 '@vue/shared': 3.5.40 '@vueuse/core': 14.3.0(vue@3.5.40(typescript@5.9.3)) @@ -31333,7 +31292,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.2.0 shiki: 4.3.0 - vite: 8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) vue: 3.5.40(typescript@5.9.3) optionalDependencies: postcss: 8.5.25 @@ -31363,9 +31322,9 @@ snapshots: - universal-cookie - yaml - vitest-environment-nuxt@2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)): + vitest-environment-nuxt@2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)): dependencies: - '@nuxt/test-utils': 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) + '@nuxt/test-utils': 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.3.5)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) transitivePeerDependencies: - '@cucumber/cucumber' - '@farmfe/core' @@ -31390,9 +31349,9 @@ snapshots: - vitest - webpack - vitest-environment-nuxt@2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)): + vitest-environment-nuxt@2.0.0(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)): dependencies: - '@nuxt/test-utils': 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) + '@nuxt/test-utils': 4.0.3(@playwright/test@1.55.0)(@vue/test-utils@2.4.6)(crossws@0.4.10(srvx@0.12.5))(esbuild@0.28.1)(jsdom@29.1.1)(magicast@0.5.3)(playwright-core@1.55.0)(rolldown@1.2.1)(rollup@4.62.3)(typescript@5.9.3)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)))(webpack@5.98.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.25)) transitivePeerDependencies: - '@cucumber/cucumber' - '@farmfe/core' @@ -31417,7 +31376,7 @@ snapshots: - vitest - webpack - vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -31440,12 +31399,13 @@ snapshots: vite: 7.3.6(@types/node@20.19.26)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 20.19.26 jsdom: 29.1.1 transitivePeerDependencies: - msw - vitest@4.1.10(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.26)(jsdom@29.1.1)(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -31468,12 +31428,13 @@ snapshots: vite: 8.2.0(@types/node@20.19.26)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 20.19.26 jsdom: 29.1.1 transitivePeerDependencies: - msw - vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -31496,12 +31457,13 @@ snapshots: vite: 7.3.6(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 26.1.2 jsdom: 29.1.1 transitivePeerDependencies: - msw - vitest@4.1.10(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@26.1.2)(jsdom@29.1.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -31524,6 +31486,7 @@ snapshots: vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.94.2)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 26.1.2 jsdom: 29.1.1 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9750621e..e65399fe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - 'packages/*/playground' - 'packages/*/playground-*' - 'client' + - 'docs' - 'scripts' - 'playground' - 'test/fixtures/**/*' @@ -41,7 +42,7 @@ catalog: vue-router: ^4.6.3 || ^5.0.0 vue-tsc: ^2.0.21 '@vue/test-utils': ^2.4.6 - '@vue/devtools-api': ^7.0.0 + '@vue/devtools-api': ^8.1.5 '@vue/server-renderer': ^3.3.0 # React (один диапазон для deps и peer) react: "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" From 2e9f78f88c906bdf7fa42a5a034b569a9510e5d0 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:20:01 +0500 Subject: [PATCH 2/8] feat(docs): add Mintlify Ask AI widget Official embed with branded trigger; hide closed Shadow DOM button. Co-authored-by: Cursor --- branding/mintlify/favicon.svg | 38 +++++++++++++++ branding/mintlify/logo-dark.svg | 45 +++++++++++++++++ branding/mintlify/logo-light.svg | 45 +++++++++++++++++ docs/.vitepress/config.mts | 83 ++++++++++++++++++++++++++++++++ docs/.vitepress/theme/custom.css | 70 +++++++++++++++++++++++++++ 5 files changed, 281 insertions(+) create mode 100644 branding/mintlify/favicon.svg create mode 100644 branding/mintlify/logo-dark.svg create mode 100644 branding/mintlify/logo-light.svg diff --git a/branding/mintlify/favicon.svg b/branding/mintlify/favicon.svg new file mode 100644 index 00000000..4b2acad3 --- /dev/null +++ b/branding/mintlify/favicon.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/branding/mintlify/logo-dark.svg b/branding/mintlify/logo-dark.svg new file mode 100644 index 00000000..c6b2cda0 --- /dev/null +++ b/branding/mintlify/logo-dark.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + Nuxt + I18n Micro + diff --git a/branding/mintlify/logo-light.svg b/branding/mintlify/logo-light.svg new file mode 100644 index 00000000..b597cb7f --- /dev/null +++ b/branding/mintlify/logo-light.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + Nuxt + I18n Micro + diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c00acc99..192189c3 100755 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -72,6 +72,89 @@ export default withFolderTree( ['meta', { name: 'application-name', content: SITE.name }], ['meta', { name: 'keywords', content: 'nuxt,i18n,internationalization,nuxt-module,localization,vue,ssr,seo' }], ['link', { rel: 'dns-prefetch', href: 'https://github.com' }], + ['link', { rel: 'dns-prefetch', href: 'https://widget.mintlify.com' }], + ['script', { type: 'module', src: 'https://widget.mintlify.com/v1/embed.js' }], + [ + 'script', + { type: 'module' }, + `const isDark = () => document.documentElement.classList.contains('dark') +const theme = () => (isDark() ? 'dark' : 'light') + +const trigger = document.createElement('button') +trigger.type = 'button' +trigger.className = 'ni18n-ask-ai' +trigger.setAttribute('aria-label', 'Ask AI') +trigger.innerHTML = 'Ask AI' +trigger.hidden = true +document.body.appendChild(trigger) + +const setTriggerVisible = (visible) => { + trigger.hidden = !visible +} + +await window.MintlifyAssistant.init({ + id: "mint_widget_fe4a84f3-0f6c-4dc0-94d6-3941d70125c2", + appearance: { + variant: "widget", + theme: theme(), + accent: "#32ba8c", + radius: "16px", + side: "bottom", + align: "end", + logo: "${SITE.url}/favicon.svg", + zIndex: 40, + }, + labels: { + title: "Ask AI", + trigger: "Ask AI", + placeholder: "Ask about Nuxt I18n Micro…", + suggestions: "Try asking", + }, + starterQuestions: [ + "How do I install nuxt-i18n-micro?", + "Which routing strategy should I use?", + "How do page-level translations work?", + ], + hooks: { + event(event) { + if (event.type === 'open') setTriggerVisible(false) + if (event.type === 'close' || event.type === 'init') setTriggerVisible(true) + }, + }, +}) + +setTriggerVisible(true) + +trigger.addEventListener('click', () => { + void window.MintlifyAssistant.open({ source: 'custom-trigger', focus: true }) +}) + +// Built-in trigger is closed Shadow DOM and stays white — cover it with our button. +const hideBuiltInTrigger = () => { + const host = document.querySelector('mintlify-assistant') + if (!host) return + const root = host.shadowRoot + if (!root) return + for (const el of root.querySelectorAll('button, [role="button"]')) { + const text = (el.textContent || '').trim() + if (text.includes('Ask AI') || el.getAttribute('aria-label')?.includes('Ask')) { + el.style.setProperty('visibility', 'hidden', 'important') + el.style.setProperty('pointer-events', 'none', 'important') + } + } +} +hideBuiltInTrigger() +new MutationObserver(hideBuiltInTrigger).observe(document.body, { childList: true, subtree: true }) + +new MutationObserver(() => { + void window.MintlifyAssistant.update({ + appearance: { theme: theme() }, + }) +}).observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], +})`, + ], ], themeConfig: { diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 399e9647..0377af4f 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -11,3 +11,73 @@ border: none; box-shadow: none; } + +/* Custom Ask AI trigger — Mintlify built-in button stays white (closed Shadow DOM) */ +.ni18n-ask-ai { + position: fixed; + right: 24px; + bottom: 24px; + z-index: 50; + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 12px 18px; + border: none; + border-radius: 999px; + background: #32ba8c; + color: #fff; + font: 600 14px/1 Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; + letter-spacing: -0.01em; + cursor: pointer; + box-shadow: + 0 10px 28px rgba(26, 80, 81, 0.28), + 0 2px 8px rgba(50, 186, 140, 0.35); + transition: + transform 0.15s ease, + background-color 0.15s ease, + box-shadow 0.15s ease; +} + +.ni18n-ask-ai:hover { + background: #2aa67c; + transform: translateY(-1px); + box-shadow: + 0 14px 32px rgba(26, 80, 81, 0.32), + 0 4px 12px rgba(50, 186, 140, 0.4); +} + +.ni18n-ask-ai:active { + transform: translateY(0); +} + +.ni18n-ask-ai:focus-visible { + outline: 2px solid #1a5051; + outline-offset: 3px; +} + +.ni18n-ask-ai__icon { + font-size: 15px; + line-height: 1; +} + +.dark .ni18n-ask-ai { + background: #32ba8c; + color: #0b1f1c; + box-shadow: + 0 10px 28px rgba(0, 0, 0, 0.45), + 0 0 0 1px rgba(95, 224, 174, 0.25); +} + +.dark .ni18n-ask-ai:hover { + background: #5fe0ae; +} + +@media (max-width: 640px) { + .ni18n-ask-ai { + right: 16px; + bottom: 16px; + padding: 11px 16px; + } +} + From 21a4ee0e105a9f766d504c5007c9cf229c186437 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:20:01 +0500 Subject: [PATCH 3/8] feat: add Vue DevTools live inspector Optional inspector/timeline for locale, chunks, and missing keys. Co-authored-by: Cursor --- packages/core/package.json | 17 +- packages/core/src/devtools-entry.ts | 10 + packages/core/src/devtools.ts | 258 +++++++++++++++++++ packages/core/tests/devtools.test.ts | 105 ++++++++ packages/core/vite.config.mts | 3 +- packages/types/package.json | 2 +- packages/types/src/index.ts | 7 + src/devtools.ts | 64 ++++- src/module.ts | 9 + src/runtime/devtools/bridge.ts | 11 + src/runtime/devtools/vue-devtools.ts | 337 +++++++++++++++++++++++++ src/runtime/plugins/01.plugin.ts | 8 + src/runtime/plugins/04.vue-devtools.ts | 74 ++++++ src/runtime/utils/nuxt-i18n.ts | 30 +++ 14 files changed, 918 insertions(+), 17 deletions(-) create mode 100644 packages/core/src/devtools-entry.ts create mode 100644 packages/core/src/devtools.ts create mode 100644 packages/core/tests/devtools.test.ts create mode 100644 src/runtime/devtools/bridge.ts create mode 100644 src/runtime/devtools/vue-devtools.ts create mode 100644 src/runtime/plugins/04.vue-devtools.ts diff --git a/packages/core/package.json b/packages/core/package.json index 80208754..a27a3aee 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/core", - "version": "1.3.11", + "version": "1.3.13", "description": "Core utilities for translations, formatting, and locale routing in Nuxt I18n Micro.", "keywords": [ "formatting", @@ -64,6 +64,21 @@ }, "default": "./dist/helpers.mjs" }, + "./devtools": { + "production": { + "types": "./dist/devtools.d.ts", + "default": "./dist/devtools.mjs" + }, + "import": { + "types": "./dist/devtools.d.ts", + "default": "./dist/devtools.mjs" + }, + "require": { + "types": "./dist/devtools.d.cts", + "default": "./dist/devtools.cjs" + }, + "default": "./dist/devtools.mjs" + }, "./package.json": "./package.json" }, "publishConfig": { diff --git a/packages/core/src/devtools-entry.ts b/packages/core/src/devtools-entry.ts new file mode 100644 index 00000000..0b74eedb --- /dev/null +++ b/packages/core/src/devtools-entry.ts @@ -0,0 +1,10 @@ +export { + buildInspectorState, + buildInspectorTree, + countTranslationKeys, + flattenTranslationNode, + parseInspectorNodeId, + type BuildInspectorTreeOptions, + type I18nDevtoolsInspectorNode, + type I18nDevtoolsStateSnapshot, +} from './devtools' diff --git a/packages/core/src/devtools.ts b/packages/core/src/devtools.ts new file mode 100644 index 00000000..2438be7b --- /dev/null +++ b/packages/core/src/devtools.ts @@ -0,0 +1,258 @@ +import type { Locale, Translations } from '@i18n-micro/types' +import { getByPath } from './helpers' +import type { TranslationStorage } from './translation' + +export interface I18nDevtoolsInspectorNode { + id: string + label: string + children?: I18nDevtoolsInspectorNode[] + tags?: Array<{ + label: string + textColor: number + backgroundColor: number + tooltip?: string + }> +} + +export interface I18nDevtoolsStateSnapshot { + locale: string + routeName: string + strategy?: string + defaultLocale?: string + fallbackLocale?: string + cachedChunks: number + locales: Array<{ code: string; displayName?: string; disabled?: boolean }> +} + +export interface BuildInspectorTreeOptions { + nodeId: string + storage: TranslationStorage + configuredLocales: Locale[] + activeLocale: string + activeRouteName: string + activeTranslations?: Record +} + +const ACTIVE_TAG = { + label: 'Active', + textColor: 0xffffff, + backgroundColor: 0x42b883, + tooltip: 'Current locale and route context', +} + +const NODE_ROOT = 'root' +const NODE_ACTIVE = 'active' +const PREFIX_LOCALE = 'locale|' +const PREFIX_CHUNK = 'chunk|' +const PREFIX_KEY = 'key|' + +const isPlainObject = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value) + +function formatInspectorValue(value: unknown): string { + if (typeof value === 'string') return JSON.stringify(value) + if (value === null) return 'null' + if (value === undefined) return 'undefined' + if (typeof value === 'object') return Array.isArray(value) ? `[Array(${value.length})]` : '{…}' + return String(value) +} + +function activeChunkTag(locale: string, routeName: string, activeLocale: string, activeRouteName: string) { + return locale === activeLocale && routeName === activeRouteName ? [ACTIVE_TAG] : undefined +} + +function collectRouteNamesForLocale(storage: TranslationStorage, locale: string): string[] { + const prefix = `${locale}:` + const routes = new Set() + + for (const key of storage.translations.keys()) { + if (!key.startsWith(prefix)) continue + routes.add(key.slice(prefix.length) || 'index') + } + + return [...routes].sort() +} + +function collectLocaleCodes(storage: TranslationStorage, configuredLocales: Locale[]): string[] { + const codes = new Set(configuredLocales.map((locale) => locale.code)) + + for (const key of storage.translations.keys()) { + const locale = key.split(':')[0] + if (locale) codes.add(locale) + } + + return [...codes].sort() +} + +export function parseInspectorNodeId(nodeId: string): { + kind: 'root' | 'active' | 'locale' | 'chunk' | 'key' + locale?: string + routeName?: string + path?: string +} { + if (!nodeId || nodeId === NODE_ROOT) return { kind: 'root' } + if (nodeId === NODE_ACTIVE) return { kind: 'active' } + + if (nodeId.startsWith(PREFIX_LOCALE)) { + return { kind: 'locale', locale: nodeId.slice(PREFIX_LOCALE.length) } + } + + if (nodeId.startsWith(PREFIX_CHUNK)) { + const [locale = '', routeName = 'index'] = nodeId.slice(PREFIX_CHUNK.length).split('|') + return { kind: 'chunk', locale, routeName } + } + + if (nodeId.startsWith(PREFIX_KEY)) { + const parts = nodeId.slice(PREFIX_KEY.length).split('|') + const locale = parts[0] ?? '' + const routeName = parts[1] ?? 'index' + const path = parts.slice(2).join('|') + return { kind: 'key', locale, routeName, path } + } + + return { kind: 'root' } +} + +export function flattenTranslationNode(obj: Record, path = ''): I18nDevtoolsInspectorNode[] { + const target = path ? (getByPath(obj, path) as Record | undefined) : obj + if (!isPlainObject(target)) return [] + + return Object.keys(target) + .filter((key) => key !== '__proto__') + .map((key) => { + const childPath = path ? `${path}.${key}` : key + const value = target[key] + const nested = isPlainObject(value) + + return { + id: `${PREFIX_KEY}${childPath}`, + label: nested ? key : `${key}: ${formatInspectorValue(value)}`, + children: nested ? [] : undefined, + } + }) + .sort((a, b) => a.label.localeCompare(b.label)) +} + +export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevtoolsInspectorNode[] { + const parsed = parseInspectorNodeId(options.nodeId) + + if (parsed.kind === 'root') { + const locales = collectLocaleCodes(options.storage, options.configuredLocales) + const nodes: I18nDevtoolsInspectorNode[] = [ + { + id: NODE_ACTIVE, + label: `Active (${options.activeLocale}:${options.activeRouteName || 'index'})`, + tags: [ACTIVE_TAG], + }, + ] + + for (const code of locales) { + const configured = options.configuredLocales.find((locale) => locale.code === code) + const routes = collectRouteNamesForLocale(options.storage, code) + + nodes.push({ + id: `${PREFIX_LOCALE}${code}`, + label: configured?.displayName ? `${code} (${configured.displayName})` : code, + tags: code === options.activeLocale ? [{ ...ACTIVE_TAG, tooltip: 'Active locale' }] : undefined, + }) + + // Flat chunk nodes (not nested): Vue DevTools selection is unreliable for nested + // custom-inspector children, so route chunks sit at the root next to their locale. + for (const routeName of routes) { + nodes.push({ + id: `${PREFIX_CHUNK}${code}|${routeName}`, + label: `${code}:${routeName}`, + tags: activeChunkTag(code, routeName, options.activeLocale, options.activeRouteName), + }) + } + } + + return nodes + } + + if (parsed.kind === 'active') { + return flattenTranslationNode(options.activeTranslations ?? {}).map((node) => ({ + ...node, + id: `${PREFIX_KEY}${options.activeLocale}|${options.activeRouteName || 'index'}|${node.id.slice(PREFIX_KEY.length)}`, + })) + } + + if (parsed.kind === 'locale' && parsed.locale) { + const locale = parsed.locale + return collectRouteNamesForLocale(options.storage, locale).map((routeName) => ({ + id: `${PREFIX_CHUNK}${locale}|${routeName}`, + label: routeName, + children: [], + tags: activeChunkTag(locale, routeName, options.activeLocale, options.activeRouteName), + })) + } + + if (parsed.kind === 'chunk' && parsed.locale) { + const locale = parsed.locale + const routeName = parsed.routeName || 'index' + const chunk = options.storage.translations.get(`${locale}:${routeName}`) as Record | undefined + return flattenTranslationNode(chunk ?? {}).map((node) => ({ + ...node, + id: `${PREFIX_KEY}${locale}|${routeName}|${node.id.slice(PREFIX_KEY.length)}`, + })) + } + + if (parsed.kind === 'key' && parsed.locale) { + const locale = parsed.locale + const routeName = parsed.routeName || 'index' + const chunk = options.storage.translations.get(`${locale}:${routeName}`) as Record | undefined + const path = parsed.path ?? '' + + if (path && options.activeLocale === locale && options.activeRouteName === routeName) { + const activeValue = getByPath(options.activeTranslations ?? {}, path) + if (isPlainObject(activeValue)) { + return flattenTranslationNode(activeValue, '').map((node) => ({ + ...node, + id: `${PREFIX_KEY}${locale}|${routeName}|${path}.${node.id.slice(PREFIX_KEY.length)}`, + })) + } + } + + return flattenTranslationNode(chunk ?? {}, path).map((node) => ({ + ...node, + id: `${PREFIX_KEY}${locale}|${routeName}|${path ? `${path}.${node.id.slice(PREFIX_KEY.length)}` : node.id.slice(PREFIX_KEY.length)}`, + })) + } + + return [] +} + +export function buildInspectorState(snapshot: I18nDevtoolsStateSnapshot): Record> { + return { + Context: [ + { key: 'locale', value: snapshot.locale }, + { key: 'routeName', value: snapshot.routeName }, + { key: 'strategy', value: snapshot.strategy ?? '—' }, + { key: 'defaultLocale', value: snapshot.defaultLocale ?? '—' }, + { key: 'fallbackLocale', value: snapshot.fallbackLocale ?? '—' }, + { key: 'cachedChunks', value: snapshot.cachedChunks }, + ], + Locales: snapshot.locales.map((locale) => ({ + key: locale.code, + value: locale.displayName ?? locale.code, + editable: false, + })), + } +} + +export function countTranslationKeys(translations: Translations | Record | undefined): number { + if (!translations || typeof translations !== 'object') return 0 + + const paths = new Set() + const walk = (obj: Record, prefix = '') => { + for (const key of Object.keys(obj)) { + if (key === '__proto__') continue + const path = prefix ? `${prefix}.${key}` : key + paths.add(path) + const value = obj[key] + if (isPlainObject(value)) walk(value, path) + } + } + + walk(translations as Record) + return paths.size +} diff --git a/packages/core/tests/devtools.test.ts b/packages/core/tests/devtools.test.ts new file mode 100644 index 00000000..d85be4e6 --- /dev/null +++ b/packages/core/tests/devtools.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, test } from 'vitest' +import { buildInspectorState, buildInspectorTree, countTranslationKeys, flattenTranslationNode, parseInspectorNodeId } from '../src/devtools' +import type { TranslationStorage } from '../src/translation' + +function createStorage(entries: Record>): TranslationStorage { + return { + translations: new Map(Object.entries(entries)), + } +} + +describe('devtools helpers', () => { + test('parseInspectorNodeId handles root, locale, chunk, and key nodes', () => { + expect(parseInspectorNodeId('root')).toEqual({ kind: 'root' }) + expect(parseInspectorNodeId('active')).toEqual({ kind: 'active' }) + expect(parseInspectorNodeId('locale|en')).toEqual({ kind: 'locale', locale: 'en' }) + expect(parseInspectorNodeId('chunk|en|about')).toEqual({ kind: 'chunk', locale: 'en', routeName: 'about' }) + expect(parseInspectorNodeId('key|en|index|nav.home')).toEqual({ + kind: 'key', + locale: 'en', + routeName: 'index', + path: 'nav.home', + }) + }) + + test('buildInspectorTree lists locale chunks as flat selectable nodes', () => { + const storage = createStorage({ + 'en:index': { hello: 'Hello', nav: { home: 'Home' } }, + 'en:about': { title: 'About' }, + 'de:index': { hello: 'Hallo' }, + }) + + const root = buildInspectorTree({ + nodeId: 'root', + storage, + configuredLocales: [{ code: 'en', displayName: 'English' }, { code: 'de' }], + activeLocale: 'en', + activeRouteName: 'index', + }) + + expect(root.map((node) => node.id)).toEqual(['active', 'locale|de', 'chunk|de|index', 'locale|en', 'chunk|en|about', 'chunk|en|index']) + expect(root.find((node) => node.id === 'active')?.children).toBeUndefined() + expect(root.find((node) => node.id === 'locale|en')?.label).toBe('en (English)') + expect(root.find((node) => node.id === 'locale|en')?.children).toBeUndefined() + expect(root.find((node) => node.id === 'chunk|en|about')?.label).toBe('en:about') + expect(root.find((node) => node.id === 'chunk|en|index')?.tags?.[0]?.label).toBe('Active') + + const routes = buildInspectorTree({ + nodeId: 'locale|en', + storage, + configuredLocales: [{ code: 'en' }], + activeLocale: 'en', + activeRouteName: 'index', + }) + + expect(routes.map((node) => node.label)).toEqual(['about', 'index']) + expect(routes.find((node) => node.label === 'index')?.tags?.[0]?.label).toBe('Active') + }) + + test('flattenTranslationNode lazily exposes nested keys', () => { + const nodes = flattenTranslationNode({ + nav: { home: 'Home', about: 'About' }, + title: 'Welcome', + }) + + expect(nodes.map((node) => node.label)).toEqual(['nav', 'title: "Welcome"']) + expect(nodes.find((node) => node.label === 'nav')?.children).toEqual([]) + }) + + test('buildInspectorTree exposes active merged translations', () => { + const nodes = buildInspectorTree({ + nodeId: 'active', + storage: createStorage({}), + configuredLocales: [{ code: 'en' }], + activeLocale: 'en', + activeRouteName: 'index', + activeTranslations: { hello: 'Hello', nested: { key: 'Value' } }, + }) + + expect(nodes.map((node) => node.label)).toEqual(['hello: "Hello"', 'nested']) + }) + + test('buildInspectorState returns context and locales', () => { + const state = buildInspectorState({ + locale: 'en', + routeName: 'index', + strategy: 'prefix', + defaultLocale: 'en', + fallbackLocale: 'en', + cachedChunks: 2, + locales: [{ code: 'en', displayName: 'English' }, { code: 'de' }], + }) + + expect(state.Context?.map((entry) => entry.key)).toEqual(['locale', 'routeName', 'strategy', 'defaultLocale', 'fallbackLocale', 'cachedChunks']) + expect(state.Locales).toHaveLength(2) + }) + + test('countTranslationKeys counts nested paths', () => { + expect( + countTranslationKeys({ + hello: 'Hello', + nav: { home: 'Home', about: 'About' }, + }), + ).toBe(4) + }) +}) diff --git a/packages/core/vite.config.mts b/packages/core/vite.config.mts index d74595e1..b61cb483 100644 --- a/packages/core/vite.config.mts +++ b/packages/core/vite.config.mts @@ -9,7 +9,7 @@ function dualPackageBeforeWriteFile(filePath, content) { .split('/') .pop() ?.replace(/\.d\.ts$/, '') - if (base !== 'index' && base !== 'helpers') { + if (base !== 'index' && base !== 'helpers' && base !== 'devtools') { return { filePath, content } } const ctsPath = filePath.replace(/\.d\.ts$/, '.d.cts') @@ -24,6 +24,7 @@ export default defineConfig({ entry: { index: resolve(__dirname, 'src/index.ts'), helpers: resolve(__dirname, 'src/helpers-entry.ts'), + devtools: resolve(__dirname, 'src/devtools-entry.ts'), }, formats: ['cjs', 'es'], fileName: (format, entryName) => `${entryName}.${format === 'cjs' ? 'cjs' : 'mjs'}`, diff --git a/packages/types/package.json b/packages/types/package.json index 13559526..ad2f808c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/types", - "version": "1.2.10", + "version": "1.2.11", "description": "TypeScript definitions for the Nuxt I18n Micro ecosystem.", "keywords": [ "i18n", diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2921b0f5..26aa7394 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -384,6 +384,13 @@ export interface ModuleOptions { */ debug?: boolean + /** + * Register Vue DevTools inspector and timeline in dev (browser extension). + * Separate from the Nuxt DevTools custom tab. + * @default true + */ + vueDevtools?: boolean + /** * Global route-level locale configuration. * Allows restricting or customizing locale routes for specific pages without diff --git a/src/devtools.ts b/src/devtools.ts index e3361559..dd4f5a52 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs' import sirv from 'sirv' -import path, { resolve } from 'node:path' +import path, { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import type { ModuleOptions } from '@i18n-micro/types' import { addCustomTab, extendServerRpc, onDevToolsInitialized, startSubprocess } from '@nuxt/devtools-kit' @@ -13,9 +13,42 @@ export const DEVTOOLS_UI_PORT = 3030 export const DEVTOOLS_UI_ROUTE = '/__nuxt-i18n-micro' export const DEVTOOLS_RPC_NAMESPACE = 'nuxt-i18n-micro' -export const distDir = resolve(fileURLToPath(import.meta.url), '..') -/** Prebuilt client, shipped inside the package. Absent only when working on this repo. */ -export const clientDir = resolve(distDir, 'client') +const thisDir = dirname(fileURLToPath(import.meta.url)) +/** Repo / package root whether this file lives in `src/` or `dist/`. */ +const packageRoot = resolve(thisDir, '..') + +/** + * Directory that contains this file after resolve: + * - published / `nuxt-module-build`: `dist/` + * - playground (`modules: ['../src/module']`): `src/` + */ +export const distDir = thisDir + +/** + * Prebuilt DevTools UI (`index.html` + `_nuxt/`). + * + * Must work for both layouts above — `src/client` does not exist when the playground + * loads the module from source, so we also check `../dist/client`. + */ +export const clientDir = resolveClientDir() + +function resolveClientDir(): string { + const candidates = [ + resolve(thisDir, 'client'), // dist/client next to dist/module.mjs + resolve(thisDir, '../dist/client'), // src/devtools.ts → repo dist/client + resolve(thisDir, '../client/dist'), // client/dist symlink used in-repo + ] + return candidates.find((dir) => fs.existsSync(path.join(dir, 'index.html'))) ?? candidates[0]! +} + +function resolveClientPackageDir(): string { + return resolve(packageRoot, 'client') +} + +/** Force the client `nuxi dev` proxy even when a prebuilt UI exists (HMR for client work). */ +function useLiveClientDevServer(): boolean { + return process.env.I18N_DEVTOOLS_CLIENT_DEV === '1' || process.env.I18N_DEVTOOLS_CLIENT_DEV === 'true' +} export interface ServerFunctions { getConfigs: () => Promise @@ -33,10 +66,11 @@ export interface ClientFunctions { * Two situations, and they are distinguished by whether the prebuilt client shipped * with the package: * - * - **Installed as a dependency** — `dist/client` exists, so serve it as static files. - * This is the case for every user of the module. - * - **Developing this repo** — there is no prebuilt client, so run its dev server and - * forward to it, which keeps HMR while working on the UI. + * - **Installed / playground with prebuild** — `dist/client` exists, serve it as static + * files via sirv. Playground loads `src/module`, so resolution must find `../dist/client` + * (not the non-existent `src/client`). + * - **Live client UI work** — set `I18N_DEVTOOLS_CLIENT_DEV=1` to spawn `client/` `nuxi + * dev` and proxy to it for HMR. * * Only called when DevTools are enabled — nothing here should run for an app that * has them switched off, least of all the client's dev server. @@ -49,21 +83,21 @@ export interface ClientFunctions { */ export function setupDevToolsUI(options: ModuleOptions, rootDirs: string[]) { const nuxt = useNuxt() - const hasPrebuiltClient = fs.existsSync(clientDir) + const hasPrebuiltClient = fs.existsSync(path.join(clientDir, 'index.html')) + const liveClient = useLiveClientDevServer() - if (hasPrebuiltClient) { + if (hasPrebuiltClient && !liveClient) { addDevServerHandler({ route: DEVTOOLS_UI_ROUTE, handler: fromNodeMiddleware(sirv(clientDir, { dev: true, single: true })), }) } else { - // No prebuilt client: this is the repo itself, so run the client's dev server and - // forward to it, which keeps HMR while the UI is being worked on. + // Live client UI: run its dev server and forward, which keeps HMR while the UI is edited. startSubprocess( { command: 'npx', args: ['nuxi', 'dev', '--port', String(DEVTOOLS_UI_PORT)], - cwd: resolve(distDir, '../client'), + cwd: resolveClientPackageDir(), }, { id: 'nuxt-i18n-micro:client', name: 'i18n Micro DevTools UI' }, ) @@ -73,7 +107,9 @@ export function setupDevToolsUI(options: ModuleOptions, rootDirs: string[]) { // `addDevServerHandler` strips the mount prefix, so `event.path` arrives as `/` // or `/_nuxt/...` and the route has to be put back on for the client dev server, // which serves itself under that same base. - handler: defineEventHandler((event) => proxyRequest(event, `http://localhost:${DEVTOOLS_UI_PORT}${joinURL(DEVTOOLS_UI_ROUTE, event.path)}`)), + handler: defineEventHandler((event) => + proxyRequest(event, `http://localhost:${DEVTOOLS_UI_PORT}${joinURL(DEVTOOLS_UI_ROUTE, event.path || '/')}`), + ), }) } diff --git a/src/module.ts b/src/module.ts index 257c886a..51852394 100644 --- a/src/module.ts +++ b/src/module.ts @@ -565,6 +565,15 @@ export default defineNuxtModule({ }) } + if (nuxt.options.dev && options.plugin !== false && options.vueDevtools !== false) { + addPlugin({ + src: resolver.resolve('./runtime/plugins/04.vue-devtools'), + name: 'i18n-vue-devtools', + mode: 'client', + order: 5, + }) + } + addPlugin({ src: resolver.resolve('./runtime/plugins/06.redirect'), mode: 'server', diff --git a/src/runtime/devtools/bridge.ts b/src/runtime/devtools/bridge.ts new file mode 100644 index 00000000..ecb39568 --- /dev/null +++ b/src/runtime/devtools/bridge.ts @@ -0,0 +1,11 @@ +import type { ModuleOptionsExtend } from '@i18n-micro/types' +import type { Ref } from 'vue' +import type { NuxtI18n } from '../utils/nuxt-i18n' + +export const I18N_DEVTOOLS_BRIDGE_KEY = 'i18n-micro-devtools-bridge' + +export interface I18nDevtoolsBridge { + i18n: NuxtI18n + i18nConfig: ModuleOptionsExtend + localeState: Ref +} diff --git a/src/runtime/devtools/vue-devtools.ts b/src/runtime/devtools/vue-devtools.ts new file mode 100644 index 00000000..ac62851b --- /dev/null +++ b/src/runtime/devtools/vue-devtools.ts @@ -0,0 +1,337 @@ +import { + buildInspectorState, + buildInspectorTree, + countTranslationKeys, + parseInspectorNodeId, + type I18nDevtoolsStateSnapshot, +} from '@i18n-micro/core/devtools' +import { getByPath } from '@i18n-micro/core/helpers' +import type { ModuleOptionsExtend } from '@i18n-micro/types' +import type { App } from 'vue' +import type { NuxtI18n } from '../utils/nuxt-i18n' + +const INSPECTOR_ID = 'i18n-micro-inspector' +const PLUGIN_ID = 'nuxt-i18n-micro' +const TIMELINE_LAYER_ID = 'i18n-micro' + +export interface VueI18nDevtoolsContext { + app: App + i18n: NuxtI18n + i18nConfig: ModuleOptionsExtend + onContextChange: (listener: () => void) => () => void + onMissingKey: (listener: (locale: string, key: string, routeName: string) => void) => () => void +} + +export interface VueI18nDevtoolsNotifier { + notifyLocaleSwitch: (from: string, to: string, routeName: string) => void + notifyLoad: (locale: string, routeName: string, keyCount: number) => void + notifyMissing: (locale: string, key: string, routeName: string) => void + notifyTranslate: (key: string, result: unknown) => void + refreshInspector: () => void +} + +const isPlainObject = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value) + +function toInspectorState(snapshot: I18nDevtoolsStateSnapshot) { + const groups = buildInspectorState(snapshot) + const state: Record> = {} + + for (const [group, entries] of Object.entries(groups)) { + state[group] = entries.map((entry) => ({ + key: entry.key, + value: entry.value, + })) + } + + return state +} + +function createSnapshot(i18n: NuxtI18n, i18nConfig: ModuleOptionsExtend): I18nDevtoolsStateSnapshot { + return { + locale: i18n.getCurrentLocale(), + routeName: i18n.getCurrentRouteName() || 'index', + strategy: i18nConfig.strategy, + defaultLocale: i18nConfig.defaultLocale, + fallbackLocale: i18nConfig.fallbackLocale, + cachedChunks: i18n.storage.translations.size, + locales: (i18nConfig.locales ?? []).map((locale) => ({ + code: locale.code, + displayName: locale.displayName, + disabled: locale.disabled, + })), + } +} + +function buildNodeState(i18n: NuxtI18n, i18nConfig: ModuleOptionsExtend, nodeId: string): Record> { + const parsed = parseInspectorNodeId(nodeId) + + if (parsed.kind === 'active') { + return { + Translations: objectToStateEntries(i18n.resolveTranslations() as Record), + } + } + + if (parsed.kind === 'locale' && parsed.locale) { + const locale = parsed.locale + const configured = (i18nConfig.locales ?? []).find((item) => item.code === locale) + const prefix = `${locale}:` + const chunks: Array<{ key: string; value: unknown }> = [] + + for (const [cacheKey, data] of i18n.storage.translations) { + if (!cacheKey.startsWith(prefix)) continue + const routeName = cacheKey.slice(prefix.length) || 'index' + chunks.push({ + key: routeName, + value: `${countTranslationKeys(data)} keys`, + }) + } + + chunks.sort((a, b) => a.key.localeCompare(b.key)) + + return { + Locale: [ + { key: 'code', value: locale }, + { key: 'displayName', value: configured?.displayName ?? locale }, + { key: 'disabled', value: Boolean(configured?.disabled) }, + { key: 'isActive', value: i18n.getCurrentLocale() === locale }, + { key: 'loadedChunks', value: chunks.length }, + ], + Chunks: chunks.length ? chunks : [{ key: '—', value: 'No translation chunks loaded for this locale yet' }], + } + } + + if (parsed.kind === 'chunk' && parsed.locale) { + const routeName = parsed.routeName || 'index' + const cacheKey = `${parsed.locale}:${routeName}` + const chunk = i18n.storage.translations.get(cacheKey) as Record | undefined + + if (!chunk) { + return { + Chunk: [ + { key: 'cacheKey', value: cacheKey }, + { key: 'status', value: 'not loaded' }, + ], + } + } + + const entries = objectToStateEntries(chunk) + return { + Chunk: [ + { key: 'cacheKey', value: cacheKey }, + { key: 'keys', value: countTranslationKeys(chunk) }, + { + key: 'isActive', + value: i18n.getCurrentLocale() === parsed.locale && i18n.getCurrentRouteName() === routeName, + }, + ], + Translations: entries.length ? entries : [{ key: '—', value: 'Chunk is loaded but empty' }], + } + } + + if (parsed.kind === 'key' && parsed.locale) { + const routeName = parsed.routeName || 'index' + const path = parsed.path ?? '' + const chunk = i18n.storage.translations.get(`${parsed.locale}:${routeName}`) as Record | undefined + const value = path ? getByPath(chunk ?? {}, path) : chunk + + if (isPlainObject(value)) { + return { [path || 'root']: objectToStateEntries(value) } + } + + return { + Value: [{ key: path || 'value', value }], + } + } + + return toInspectorState(createSnapshot(i18n, i18nConfig)) +} + +function objectToStateEntries(obj: Record, limit = 500): Array<{ key: string; value: unknown }> { + const entries: Array<{ key: string; value: unknown }> = [] + let truncated = false + + // Breadth-first so shallow keys (navigation, test_key, …) aren't starved by deep + // playground nests like key0.key0.key0… before the leaf cap hits. + const queue: Array<{ node: Record; prefix: string }> = [{ node: obj, prefix: '' }] + + while (queue.length > 0) { + const { node, prefix } = queue.shift()! + const nested: Array<{ node: Record; prefix: string }> = [] + + for (const key of Object.keys(node)) { + if (key === '__proto__') continue + if (entries.length >= limit) { + truncated = true + break + } + + const path = prefix ? `${prefix}.${key}` : key + const value = node[key] + + if (isPlainObject(value)) { + nested.push({ node: value, prefix: path }) + continue + } + + entries.push({ key: path, value }) + } + + if (truncated) break + queue.push(...nested) + } + + entries.sort((a, b) => a.key.localeCompare(b.key)) + + if (truncated) { + entries.push({ + key: '…', + value: `Showing first ${limit} leaves (nested keys flattened)`, + }) + } + + return entries +} + +export async function setupVueI18nDevtools(ctx: VueI18nDevtoolsContext): Promise { + if (!import.meta.dev || import.meta.server) { + return null + } + + let notifier: VueI18nDevtoolsNotifier | null = null + + try { + const { setupDevtoolsPlugin } = await import('@vue/devtools-api') + + setupDevtoolsPlugin( + { + id: PLUGIN_ID, + label: 'i18n Micro', + packageName: 'nuxt-i18n-micro', + homepage: 'https://s00d.github.io/nuxt-i18n-micro/', + logo: 'https://s00d.github.io/nuxt-i18n-micro/favicon.svg', + app: ctx.app, + enableEarlyProxy: true, + }, + (api) => { + const refreshInspector = () => { + api.sendInspectorTree(INSPECTOR_ID) + api.sendInspectorState(INSPECTOR_ID) + } + + api.addInspector({ + id: INSPECTOR_ID, + label: 'i18n Micro', + icon: 'carbon:language', + actions: [ + { + icon: 'refresh', + tooltip: 'Refresh inspector', + action: refreshInspector, + }, + ], + }) + + api.addTimelineLayer({ + id: TIMELINE_LAYER_ID, + label: 'i18n Micro', + color: 0x42b883, + }) + + api.on.getInspectorTree((payload) => { + if (payload.inspectorId !== INSPECTOR_ID) return + + payload.rootNodes = buildInspectorTree({ + nodeId: 'root', + storage: ctx.i18n.storage, + configuredLocales: ctx.i18nConfig.locales ?? [], + activeLocale: ctx.i18n.getCurrentLocale(), + activeRouteName: ctx.i18n.getCurrentRouteName() || 'index', + activeTranslations: ctx.i18n.resolveTranslations() as Record, + }) as never + }) + + api.on.getInspectorState((payload) => { + if (payload.inspectorId !== INSPECTOR_ID) return + + payload.state = ( + payload.nodeId ? buildNodeState(ctx.i18n, ctx.i18nConfig, payload.nodeId) : toInspectorState(createSnapshot(ctx.i18n, ctx.i18nConfig)) + ) as never + }) + + ctx.onContextChange(refreshInspector) + ctx.onMissingKey((locale, key, routeName) => { + api.addTimelineEvent({ + layerId: TIMELINE_LAYER_ID, + event: { + time: api.now(), + title: 'Missing translation', + subtitle: key, + data: { locale, key, routeName }, + }, + }) + }) + + notifier = { + notifyLocaleSwitch(from, to, routeName) { + api.addTimelineEvent({ + layerId: TIMELINE_LAYER_ID, + event: { + time: api.now(), + title: 'Locale switch', + subtitle: `${from || '—'} → ${to}`, + data: { from, to, routeName }, + }, + }) + refreshInspector() + }, + notifyLoad(locale, routeName, keyCount) { + api.addTimelineEvent({ + layerId: TIMELINE_LAYER_ID, + event: { + time: api.now(), + title: 'Translations loaded', + subtitle: `${locale}:${routeName}`, + data: { locale, routeName, keyCount }, + }, + }) + refreshInspector() + }, + notifyMissing(locale, key, routeName) { + api.addTimelineEvent({ + layerId: TIMELINE_LAYER_ID, + event: { + time: api.now(), + title: 'Missing translation', + subtitle: key, + data: { locale, key, routeName }, + }, + }) + }, + notifyTranslate(key, result) { + api.addTimelineEvent({ + layerId: TIMELINE_LAYER_ID, + event: { + time: api.now(), + title: '$t', + subtitle: key, + data: { key, result }, + }, + }) + }, + refreshInspector, + } + + refreshInspector() + }, + ) + } catch { + return null + } + + return notifier +} + +export function countLoadedTranslationKeys(i18n: NuxtI18n, locale: string, routeName: string): number { + const chunk = i18n.storage.translations.get(i18n.getCacheKey(locale, routeName)) + return countTranslationKeys(chunk) +} diff --git a/src/runtime/plugins/01.plugin.ts b/src/runtime/plugins/01.plugin.ts index 1b514df2..55d603d5 100644 --- a/src/runtime/plugins/01.plugin.ts +++ b/src/runtime/plugins/01.plugin.ts @@ -139,6 +139,14 @@ export default defineNuxtPlugin(async (nuxtApp) => { }, }) + if (import.meta.dev && import.meta.client) { + useState('i18n-micro-devtools-bridge', () => ({ + i18n, + i18nConfig, + localeState, + })) + } + router.beforeEach(async (to, from) => { if (to.name !== from.name) { i18nRouteParams.value = {} diff --git a/src/runtime/plugins/04.vue-devtools.ts b/src/runtime/plugins/04.vue-devtools.ts new file mode 100644 index 00000000..754074a5 --- /dev/null +++ b/src/runtime/plugins/04.vue-devtools.ts @@ -0,0 +1,74 @@ +import { useState } from '#app' +import { defineNuxtPlugin, useNuxtApp } from '#imports' +import type { Params } from '@i18n-micro/types' +import { watch } from 'vue' +import { I18N_DEVTOOLS_BRIDGE_KEY, type I18nDevtoolsBridge } from '../devtools/bridge' +import { countLoadedTranslationKeys, setupVueI18nDevtools } from '../devtools/vue-devtools' + +export default defineNuxtPlugin({ + name: 'i18n-vue-devtools', + dependsOn: ['i18n-plugin-loader'], + async setup() { + if (!import.meta.dev || import.meta.server) { + return + } + + const nuxtApp = useNuxtApp() + const bridge = useState(I18N_DEVTOOLS_BRIDGE_KEY, () => null).value + if (!bridge) { + return + } + + const notifier = await setupVueI18nDevtools({ + app: nuxtApp.vueApp, + i18n: bridge.i18n, + i18nConfig: bridge.i18nConfig, + onContextChange: (listener) => bridge.i18n.onContextChange(listener), + onMissingKey: (listener) => bridge.i18n.onMissingKey(listener), + }) + + if (!notifier) { + return + } + + const notifyCurrentLoad = () => { + const locale = bridge.i18n.getCurrentLocale() + const routeName = bridge.i18n.getCurrentRouteName() || 'index' + notifier.notifyLoad(locale, routeName, countLoadedTranslationKeys(bridge.i18n, locale, routeName)) + } + + notifyCurrentLoad() + + watch( + () => bridge.localeState.value, + (to, from) => { + if (to && from && to !== from) { + notifier.notifyLocaleSwitch(from, to, bridge.i18n.getCurrentRouteName() || 'index') + } + }, + ) + + bridge.i18n.onContextChange(() => { + notifyCurrentLoad() + }) + + if (bridge.i18nConfig.debug) { + const wrapTranslate = unknown>(translate: T): T => + ((key: string, ...args: never[]) => { + const result = translate(key, ...args) + notifier.notifyTranslate(key, result) + return result + }) as T + + const app = nuxtApp as typeof nuxtApp & { + $t: (key: string, params?: Params, defaultValue?: string | null) => unknown + $ts: (key: string, params?: Params, defaultValue?: string) => string + $tc: (key: string, params: number | Params, defaultValue?: string) => string + } + + app.$t = wrapTranslate(app.$t.bind(app)) + app.$ts = wrapTranslate(app.$ts.bind(app)) + app.$tc = wrapTranslate(app.$tc.bind(app)) + } + }, +}) diff --git a/src/runtime/utils/nuxt-i18n.ts b/src/runtime/utils/nuxt-i18n.ts index bde681dc..43b9501c 100644 --- a/src/runtime/utils/nuxt-i18n.ts +++ b/src/runtime/utils/nuxt-i18n.ts @@ -92,6 +92,8 @@ export class NuxtI18n extends BaseI18n { private currentLocale = '' private currentRouteName = '' private resolveRouteContext?: GetRouteTranslationContext + private readonly contextListeners = new Set<() => void>() + private missingKeyListener?: (locale: string, key: string, routeName: string) => void constructor(options: NuxtI18nOptions = {}) { const storage: TranslationStorage = { @@ -168,6 +170,28 @@ export class NuxtI18n extends BaseI18n { return this.currentRouteName } + onContextChange(listener: () => void): () => void { + this.contextListeners.add(listener) + return () => { + this.contextListeners.delete(listener) + } + } + + onMissingKey(listener: (locale: string, key: string, routeName: string) => void): () => void { + this.missingKeyListener = listener + return () => { + if (this.missingKeyListener === listener) { + this.missingKeyListener = undefined + } + } + } + + private notifyContextChange(): void { + for (const listener of this.contextListeners) { + listener() + } + } + /** * Hot-reload the active dictionary for the next route — replace, don't merge. * @@ -187,6 +211,7 @@ export class NuxtI18n extends BaseI18n { this.currentLocale = locale this.currentRouteName = routeName || '' triggerRef(this.contextSignal) + this.notifyContextChange() } /** The outgoing page has unmounted, so its keys must stop resolving. */ @@ -194,6 +219,7 @@ export class NuxtI18n extends BaseI18n { if (this.outgoingTranslations === null) return this.outgoingTranslations = null triggerRef(this.contextSignal) + this.notifyContextChange() } mergeTranslations(newTranslations: Translations): void { @@ -202,6 +228,7 @@ export class NuxtI18n extends BaseI18n { // walking the whole thing again costs its size for the sake of a few new keys. this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations as Record) triggerRef(this.contextSignal) + this.notifyContextChange() } async loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise { @@ -209,6 +236,7 @@ export class NuxtI18n extends BaseI18n { if (locale === this.currentLocale && routeName === this.currentRouteName) { this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, translations as Record) triggerRef(this.contextSignal) + this.notifyContextChange() } } @@ -222,6 +250,7 @@ export class NuxtI18n extends BaseI18n { if (locale === this.currentLocale && routeName === this.currentRouteName) { this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations as Record) triggerRef(this.contextSignal) + this.notifyContextChange() } } @@ -293,6 +322,7 @@ export class NuxtI18n extends BaseI18n { } protected override warnMissing(key: TranslationKey): void { + this.missingKeyListener?.(this.currentLocale, String(key), this.currentRouteName) const customHandler = this.getCustomMissingHandler?.() if (customHandler) { customHandler(this.currentLocale, String(key), this.currentRouteName) From e44016240234d235366500960acc533b8d467595 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:20:01 +0500 Subject: [PATCH 4/8] chore: bump workspace package versions Patch bumps for packages depending on core/types changes. Co-authored-by: Cursor --- packages/astro/package.json | 2 +- packages/devtools-ui/package.json | 2 +- packages/hmr/package.json | 2 +- packages/node/package.json | 2 +- packages/path-strategy/package.json | 2 +- packages/preact/package.json | 2 +- packages/react/package.json | 2 +- packages/route-strategy/package.json | 2 +- packages/solid/package.json | 2 +- packages/test-utils/package.json | 2 +- packages/types-generator/package.json | 2 +- packages/utils/package.json | 2 +- packages/vue/package.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index d7fa59ed..5d342b42 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/astro", - "version": "1.3.10", + "version": "1.3.11", "description": "Astro integration for i18n-micro — translations, routing, and islands.", "keywords": [ "astro", diff --git a/packages/devtools-ui/package.json b/packages/devtools-ui/package.json index 2478956a..b58df38f 100644 --- a/packages/devtools-ui/package.json +++ b/packages/devtools-ui/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/devtools-ui", - "version": "1.2.7", + "version": "1.2.8", "description": "DevTools UI and Vite plugin for Nuxt I18n Micro.", "homepage": "https://github.com/s00d/nuxt-i18n-micro/tree/main/packages/devtools-ui#readme", "bugs": { diff --git a/packages/hmr/package.json b/packages/hmr/package.json index 41915b6b..1d3410cb 100644 --- a/packages/hmr/package.json +++ b/packages/hmr/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/hmr", - "version": "1.0.6", + "version": "1.0.7", "description": "Translation HMR utilities for Nuxt I18n Micro (granular subpath imports).", "keywords": [ "hmr", diff --git a/packages/node/package.json b/packages/node/package.json index b36f4479..e147f5b3 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/node", - "version": "1.2.6", + "version": "1.2.7", "description": "Node.js utilities for loading translations and locale data in Nuxt I18n Micro.", "keywords": [ "i18n", diff --git a/packages/path-strategy/package.json b/packages/path-strategy/package.json index 58eb6502..b0c1fcfd 100644 --- a/packages/path-strategy/package.json +++ b/packages/path-strategy/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/path-strategy", - "version": "1.3.9", + "version": "1.3.10", "description": "Shared strategy-based path and route name builder for Nuxt I18n Micro frontend routing.", "keywords": [ "i18n", diff --git a/packages/preact/package.json b/packages/preact/package.json index 20f215fb..b573da09 100644 --- a/packages/preact/package.json +++ b/packages/preact/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/preact", - "version": "1.2.5", + "version": "1.2.6", "description": "Preact bindings for i18n-micro — hooks, context, and UI components.", "keywords": [ "i18n", diff --git a/packages/react/package.json b/packages/react/package.json index 910c7853..8e9093b6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/react", - "version": "1.3.5", + "version": "1.3.6", "description": "React bindings for i18n-micro — hooks, context, and UI components.", "keywords": [ "i18n", diff --git a/packages/route-strategy/package.json b/packages/route-strategy/package.json index 542910f0..89c27223 100644 --- a/packages/route-strategy/package.json +++ b/packages/route-strategy/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/route-strategy", - "version": "1.1.14", + "version": "1.1.15", "description": "Locale-aware route generator for Nuxt i18n micro", "keywords": [ "i18n", diff --git a/packages/solid/package.json b/packages/solid/package.json index ac635be5..845bbd5e 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/solid", - "version": "1.2.6", + "version": "1.2.7", "description": "SolidJS bindings for i18n-micro — primitives, context, and UI components.", "keywords": [ "i18n", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 83958804..ce1bfa2f 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/test-utils", - "version": "1.2.8", + "version": "1.2.9", "description": "Testing helpers for Nuxt I18n Micro (translations, locales, formatting mocks).", "keywords": [ "i18n", diff --git a/packages/types-generator/package.json b/packages/types-generator/package.json index b1c458c1..d2395aec 100644 --- a/packages/types-generator/package.json +++ b/packages/types-generator/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/types-generator", - "version": "1.1.4", + "version": "1.1.5", "description": "Type generator for i18n-micro translation keys", "keywords": [ "i18n", diff --git a/packages/utils/package.json b/packages/utils/package.json index 38752ef4..3f41ef9a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/utils", - "version": "1.0.13", + "version": "1.0.14", "description": "Framework-agnostic utilities for Nuxt I18n Micro (granular subpath imports).", "keywords": [ "i18n", diff --git a/packages/vue/package.json b/packages/vue/package.json index 447523a4..ae867985 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@i18n-micro/vue", - "version": "1.3.9", + "version": "1.3.10", "description": "Vue 3 bindings for i18n-micro — composables, components, and routing helpers.", "keywords": [ "i18n", From b71bcfd3f7c1d071ceda8759fc4397f88776a4e6 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:20:01 +0500 Subject: [PATCH 5/8] docs: document Vue DevTools and refresh API surface Cover vueDevtools option and core/devtools export. Co-authored-by: Cursor --- docs/api/module-options.md | 1 + docs/api/packages.md | 2 +- docs/api/packages/astro.md | 5 +- docs/api/packages/core.md | 57 ++++++++++++++++++- docs/api/packages/preact.md | 4 +- docs/api/packages/solid.md | 4 +- docs/api/packages/types.md | 15 +++-- docs/guide/configuration.md | 1 + docs/guide/devtools.md | 25 ++++++++ docs/news/index.md | 12 ++++ scripts/api-surface/i18n-micro__astro.api.txt | 1 + scripts/api-surface/i18n-micro__core.api.txt | 27 +++++++++ .../api-surface/i18n-micro__preact.api.txt | 1 + scripts/api-surface/i18n-micro__solid.api.txt | 1 + scripts/api-surface/i18n-micro__types.api.txt | 3 + 15 files changed, 145 insertions(+), 14 deletions(-) diff --git a/docs/api/module-options.md b/docs/api/module-options.md index 30ce3023..3de8a7fc 100644 --- a/docs/api/module-options.md +++ b/docs/api/module-options.md @@ -124,6 +124,7 @@ Parts of the module you can switch off. | Option | Type | Default | Description | | --- | --- | --- | --- | | `hreflangBaseLanguage` | `boolean` | `false` | Also emit a bare-language `hreflang` derived from each locale's `iso` (e.g. `es-ES` → also `es`). The first regional locale in `locales` claims the bare tag for that language. Routing `code` is never used — only `iso \|\| code`. | +| `vueDevtools` | `boolean` | `true` | Register Vue DevTools inspector and timeline in dev (browser extension). Separate from the Nuxt DevTools custom tab. | | `localizedRouteNamePrefix` | `string` | `'localized-'` | Prefix prepended to localized route names (e.g. `'localized-index'`). Used internally to distinguish original routes from generated locale variants. | | `routeDisableMeta` | `Record` | — | Per-route meta tag disabling, extracted from `defineI18nRoute()` calls. Maps a route path to `true` (disable all meta) or an array of locale codes for which meta should be disabled. | | `missingWarn` | `boolean` | `true` | Show console warnings when a translation key is missing. | diff --git a/docs/api/packages.md b/docs/api/packages.md index ec7eadb2..e464ca08 100644 --- a/docs/api/packages.md +++ b/docs/api/packages.md @@ -19,7 +19,7 @@ or CI failing. | Package | Entry points | Exports | | --- | --- | --- | | [`@i18n-micro/astro`](/api/packages/astro) | `@i18n-micro/astro`, `@i18n-micro/astro/client`, `@i18n-micro/astro/client/preact`, `@i18n-micro/astro/client/react`, `@i18n-micro/astro/client/svelte`, `@i18n-micro/astro/client/vue` | 56 | -| [`@i18n-micro/core`](/api/packages/core) | `@i18n-micro/core`, `@i18n-micro/core/helpers` | 42 | +| [`@i18n-micro/core`](/api/packages/core) | `@i18n-micro/core`, `@i18n-micro/core/devtools`, `@i18n-micro/core/helpers` | 50 | | [`@i18n-micro/devtools-ui`](/api/packages/devtools-ui) | `@i18n-micro/devtools-ui`, `@i18n-micro/devtools-ui/bridge`, `@i18n-micro/devtools-ui/bridge/create`, `@i18n-micro/devtools-ui/vite` | 25 | | [`@i18n-micro/hmr`](/api/packages/hmr) | `@i18n-micro/hmr/cache-keys`, `@i18n-micro/hmr/generate-plugin`, `@i18n-micro/hmr/watcher` | 8 | | [`@i18n-micro/node`](/api/packages/node) | `@i18n-micro/node` | 15 | diff --git a/docs/api/packages/astro.md b/docs/api/packages/astro.md index 7ed7aee1..b422afe5 100644 --- a/docs/api/packages/astro.md +++ b/docs/api/packages/astro.md @@ -36,7 +36,7 @@ import { /* … */ } from '@i18n-micro/astro' | `Getter` | type | `(key: TranslationKey, params?: Record, defaultValue?: string) => unknown` | | `I18nClientProps` | interface | 4 members | | `i18nIntegration` | function | `(options: I18nIntegrationOptions) => AstroIntegration` | -| `I18nIntegrationOptions` | interface | 52 members | +| `I18nIntegrationOptions` | interface | 53 members | | `I18nMiddlewareOptions` | interface | 7 members | | `I18nRoutingStrategy` | interface | 10 members | | `interpolate` | function | `(template: string, params: Params) => string` | @@ -165,7 +165,7 @@ import { /* … */ } from '@i18n-micro/astro'
-I18nIntegrationOptions — 52 members +I18nIntegrationOptions — 53 members | Member | Type | | --- | --- | @@ -221,6 +221,7 @@ import { /* … */ } from '@i18n-micro/astro' | `translationDir?` | `string \| undefined` | | `translationPayloads?` | `TranslationPayloadOptions \| undefined` | | `types?` | `boolean \| undefined` | +| `vueDevtools?` | `boolean \| undefined` |
diff --git a/docs/api/packages/core.md b/docs/api/packages/core.md index 05197e63..637d5774 100644 --- a/docs/api/packages/core.md +++ b/docs/api/packages/core.md @@ -6,7 +6,7 @@ outline: 'deep' # `@i18n-micro/core` -42 exports across 2 entry points. +50 exports across 3 entry points. Generated from the API snapshot that [`pnpm run api:surface`](/guide/maintenance-commands#api-surface) checks against the TypeScript sources. @@ -166,6 +166,61 @@ import { /* … */ } from '@i18n-micro/core' | --- | --- | | `translations` | `Map` | +
+## `@i18n-micro/core/devtools` + +```ts +import { /* … */ } from '@i18n-micro/core/devtools' +``` + +| Export | Kind | Signature | +| --- | --- | --- | +| `buildInspectorState` | function | `(snapshot: I18nDevtoolsStateSnapshot) => Record>` | +| `buildInspectorTree` | function | `(options: BuildInspectorTreeOptions) => I18nDevtoolsInspectorNode[]` | +| `BuildInspectorTreeOptions` | interface | 6 members | +| `countTranslationKeys` | function | `(translations: Translations \| Record \| undefined) => number` | +| `flattenTranslationNode` | function | `(obj: Record, path?: string) => I18nDevtoolsInspectorNode[]` | +| `I18nDevtoolsInspectorNode` | interface | 4 members | +| `I18nDevtoolsStateSnapshot` | interface | 7 members | +| `parseInspectorNodeId` | function | `(nodeId: string) => { kind: "root" \| "active" \| "locale" \| "chunk" \| "key"; locale?: string; routeName?: string; path?: string; }` | + +
+BuildInspectorTreeOptions — 6 members + +| Member | Type | +| --- | --- | +| `activeLocale` | `string` | +| `activeRouteName` | `string` | +| `activeTranslations?` | `Record \| undefined` | +| `configuredLocales` | `Locale[]` | +| `nodeId` | `string` | +| `storage` | `TranslationStorage` | + +
+
+I18nDevtoolsInspectorNode — 4 members + +| Member | Type | +| --- | --- | +| `children?` | `I18nDevtoolsInspectorNode[] \| undefined` | +| `id` | `string` | +| `label` | `string` | +| `tags?` | `{ label: string; textColor: number; backgroundColor: number; tooltip?: string; }[] \| undefined` | + +
+
+I18nDevtoolsStateSnapshot — 7 members + +| Member | Type | +| --- | --- | +| `cachedChunks` | `number` | +| `defaultLocale?` | `string \| undefined` | +| `fallbackLocale?` | `string \| undefined` | +| `locale` | `string` | +| `locales` | `{ code: string; displayName?: string; disabled?: boolean; }[]` | +| `routeName` | `string` | +| `strategy?` | `string \| undefined` | +
## `@i18n-micro/core/helpers` diff --git a/docs/api/packages/preact.md b/docs/api/packages/preact.md index 535e91f1..27306499 100644 --- a/docs/api/packages/preact.md +++ b/docs/api/packages/preact.md @@ -43,7 +43,7 @@ import { /* … */ } from '@i18n-micro/preact' | `interpolate` | function | `(template: string, params: Params) => string` | | `Locale` | interface | 11 members | | `LocaleCode` | type | `string` | -| `ModuleOptions` | interface | 46 members | +| `ModuleOptions` | interface | 47 members | | `Params` | type | `Record` | | `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` | | `PreactI18n` | class | 44 members | @@ -1370,7 +1370,7 @@ import { /* … */ } from '@i18n-micro/preact' | `seo?` | `boolean \| undefined` | -ModuleOptions — 46 members, identical to [`ModuleOptions`](/api/packages/types). +ModuleOptions — 47 members, identical to [`ModuleOptions`](/api/packages/types).
PreactI18n — 44 members diff --git a/docs/api/packages/solid.md b/docs/api/packages/solid.md index 92ed32ea..4ae436df 100644 --- a/docs/api/packages/solid.md +++ b/docs/api/packages/solid.md @@ -38,7 +38,7 @@ import { /* … */ } from '@i18n-micro/solid' | `interpolate` | function | `(template: string, params: Params) => string` | | `Locale` | interface | 11 members | | `LocaleCode` | type | `string` | -| `ModuleOptions` | interface | 46 members | +| `ModuleOptions` | interface | 47 members | | `Params` | type | `Record` | | `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` | | `SolidI18n` | class | 53 members | @@ -79,7 +79,7 @@ import { /* … */ } from '@i18n-micro/solid'
Locale — 11 members, identical to [`Locale`](/api/packages/types). -ModuleOptions — 46 members, identical to [`ModuleOptions`](/api/packages/types). +ModuleOptions — 47 members, identical to [`ModuleOptions`](/api/packages/types).
SolidI18n — 53 members diff --git a/docs/api/packages/types.md b/docs/api/packages/types.md index 37cc445d..12108813 100644 --- a/docs/api/packages/types.md +++ b/docs/api/packages/types.md @@ -32,9 +32,9 @@ import { /* … */ } from '@i18n-micro/types' | `Locale` | interface | 11 members | | `LocaleCode` | type | `string` | | `MissingHandler` | type | `(locale: string, key: TranslationKey, routeName: string, instance?: unknown, type?: string) => void` | -| `ModuleOptions` | interface | 46 members | -| `ModuleOptionsExtend` | interface | 49 members | -| `ModulePrivateOptionsExtend` | interface | 49 members | +| `ModuleOptions` | interface | 47 members | +| `ModuleOptionsExtend` | interface | 50 members | +| `ModulePrivateOptionsExtend` | interface | 50 members | | `Params` | type | `Record` | | `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` | | `PluralTranslations` | interface | 2 members | @@ -113,7 +113,7 @@ import { /* … */ } from '@i18n-micro/types'
-ModuleOptions — 46 members +ModuleOptions — 47 members | Member | Type | | --- | --- | @@ -163,10 +163,11 @@ import { /* … */ } from '@i18n-micro/types' | `translationDir?` | `string \| undefined` | | `translationPayloads?` | `TranslationPayloadOptions \| undefined` | | `types?` | `boolean \| undefined` | +| `vueDevtools?` | `boolean \| undefined` |
-ModuleOptionsExtend — 49 members +ModuleOptionsExtend — 50 members | Member | Type | | --- | --- | @@ -219,10 +220,11 @@ import { /* … */ } from '@i18n-micro/types' | `translationPayloadMode?` | `"premerged" \| "source" \| undefined` | | `translationPayloads?` | `TranslationPayloadOptions \| undefined` | | `types?` | `boolean \| undefined` | +| `vueDevtools?` | `boolean \| undefined` |
-ModulePrivateOptionsExtend — 49 members +ModulePrivateOptionsExtend — 50 members | Member | Type | | --- | --- | @@ -275,6 +277,7 @@ import { /* … */ } from '@i18n-micro/types' | `translationDir` | `string` | | `translationPayloads?` | `TranslationPayloadOptions \| undefined` | | `types?` | `boolean \| undefined` | +| `vueDevtools?` | `boolean \| undefined` |
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 046b6ef7..1d91f9f2 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -55,6 +55,7 @@ The sections below explain how they work together; the | [`fallbackLocale`](/api/module-options) | `string` | `undefined (no fallback; returns the raw key)` | Global fallback locale code. | | [`localeCookie`](/api/module-options) | `string \| null` | `null` | Cookie name for persisting the user's locale preference across sessions. | | [`debug`](/api/module-options) | `boolean` | `false` | Enable verbose debug logging for locale detection, route generation, and translation loading. | +| [`vueDevtools`](/api/module-options) | `boolean` | `true` | Register Vue DevTools inspector and timeline in dev (browser extension). | | [`globalLocaleRoutes`](/api/module-options) | `GlobalLocaleRoutes` | `{}` | Global route-level locale configuration. | | [`customRegexMatcher`](/api/module-options) | `string \| RegExp` | `undefined (uses built-in pattern based on locale codes)` | Custom regular expression (or its string source) for matching locale codes in URL segments. | | [`noPrefixRedirect`](/api/module-options) | `boolean` | `false` | For `no_prefix` strategy: enable redirect from a locale-prefixed URL (e.g. `/en/about`) to the unprefixed version (`/about`). | diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index feadce7f..a93c724c 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -6,6 +6,31 @@ outline: 'deep' # 🛠️ DevTools in `nuxt-i18n-micro` +## Vue DevTools (browser extension) + +In development, the module can register a **Vue DevTools** custom inspector and timeline via `@vue/devtools-api`. This is separate from the Nuxt DevTools tab below. + +| Tool | Best for | +|------|----------| +| **Vue DevTools** | Live locale, in-memory translation cache, `$t` timeline (`debug: true`), missing-key events | +| **Nuxt DevTools tab** | Editing JSON files on disk, config overview, import/export | + +The vue-devtools plugin is registered only during `nuxt dev` (not in production builds). Dev-only bridge state in the main plugin is stripped by `import.meta.dev` dead-code elimination. + +Disable the browser integration while keeping the Nuxt tab: + +```ts +export default defineNuxtConfig({ + i18n: { + vueDevtools: false, + }, +}) +``` + +Verbose `$t` timeline events require `debug: true`. + +--- + ## 📖 What Is Nuxt DevTools? DevTools is a powerful development assistant seamlessly integrated into the `nuxt-i18n-micro` module. It provides an intuitive interface for managing localization and translation tasks, making your workflow faster and more efficient. Designed for developers who value convenience, Nuxt DevTools bridges the gap between coding and runtime management. diff --git a/docs/news/index.md b/docs/news/index.md index 57fc2fdd..7cd2d771 100644 --- a/docs/news/index.md +++ b/docs/news/index.md @@ -6,6 +6,18 @@ outline: 'deep' # News +## Nuxt I18n Micro v3.26.1 — Vue DevTools live inspector + +**Date**: 2026-08-05 + +**Version**: `v3.26.1` + +Adds optional **Vue DevTools** integration for live runtime debugging: custom inspector (locale, route chunks, active dictionary), timeline events (locale switch, load, missing keys), and optional `$t` tracing when `debug: true`. The existing **Nuxt DevTools** custom tab (JSON editor on disk) is unchanged. + +- New option: `i18n.vueDevtools` (default `true` in dev) +- Optional peer: `@vue/devtools-api` ^8.1.5 +- `@i18n-micro/core` exports devtools helpers from `@i18n-micro/core/devtools` (not in main entry) + ## Nuxt I18n Micro v3.24.0 — Node SSR from `public/`, no Rollup `raw:` **Date**: 2026-07-30 diff --git a/scripts/api-surface/i18n-micro__astro.api.txt b/scripts/api-surface/i18n-micro__astro.api.txt index 7944a388..42b40a78 100644 --- a/scripts/api-surface/i18n-micro__astro.api.txt +++ b/scripts/api-surface/i18n-micro__astro.api.txt @@ -150,6 +150,7 @@ member I18nIntegrationOptions.strategy?: Strategies | undefined member I18nIntegrationOptions.translationDir?: string | undefined member I18nIntegrationOptions.translationPayloads?: TranslationPayloadOptions | undefined member I18nIntegrationOptions.types?: boolean | undefined +member I18nIntegrationOptions.vueDevtools?: boolean | undefined interface I18nMiddlewareOptions member I18nMiddlewareOptions.autoDetect?: boolean | undefined member I18nMiddlewareOptions.defaultLocale: string diff --git a/scripts/api-surface/i18n-micro__core.api.txt b/scripts/api-surface/i18n-micro__core.api.txt index 76cb6462..150e427c 100644 --- a/scripts/api-surface/i18n-micro__core.api.txt +++ b/scripts/api-surface/i18n-micro__core.api.txt @@ -99,6 +99,33 @@ member TranslationStorage.translations: Map function useTranslationHelper: (storage?: TranslationStorage) => { hasCache(locale: string, page: string): boolean; getCache(locale: string, routeName: string): Translations | undefined; setCache(_locale: string, _routeName: string, _cache: Map): void; hasTranslation(locale: string, key: string): boolean; hasPageTranslation(locale: string, routeName: string): boolean; getTranslation(locale: string, routeName: string, key: string): T | null; loadTranslations(locale: string, data: Translations, routeName?: string): void; setTranslations(locale: string, data: Translations, routeName?: string): void; loadPageTranslations(locale: string, routeName: string, data: Translations): void; mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force?: boolean): void; clearCache(): void; } function withPrefixStrategy: (strategy: Strategies) => strategy is "prefix" | "prefix_and_default" +# ./devtools (src/devtools.ts) +function buildInspectorState: (snapshot: I18nDevtoolsStateSnapshot) => Record> +function buildInspectorTree: (options: BuildInspectorTreeOptions) => I18nDevtoolsInspectorNode[] +interface BuildInspectorTreeOptions +member BuildInspectorTreeOptions.activeLocale: string +member BuildInspectorTreeOptions.activeRouteName: string +member BuildInspectorTreeOptions.activeTranslations?: Record | undefined +member BuildInspectorTreeOptions.configuredLocales: Locale[] +member BuildInspectorTreeOptions.nodeId: string +member BuildInspectorTreeOptions.storage: TranslationStorage +function countTranslationKeys: (translations: Translations | Record | undefined) => number +function flattenTranslationNode: (obj: Record, path?: string) => I18nDevtoolsInspectorNode[] +interface I18nDevtoolsInspectorNode +member I18nDevtoolsInspectorNode.children?: I18nDevtoolsInspectorNode[] | undefined +member I18nDevtoolsInspectorNode.id: string +member I18nDevtoolsInspectorNode.label: string +member I18nDevtoolsInspectorNode.tags?: { label: string; textColor: number; backgroundColor: number; tooltip?: string; }[] | undefined +interface I18nDevtoolsStateSnapshot +member I18nDevtoolsStateSnapshot.cachedChunks: number +member I18nDevtoolsStateSnapshot.defaultLocale?: string | undefined +member I18nDevtoolsStateSnapshot.fallbackLocale?: string | undefined +member I18nDevtoolsStateSnapshot.locale: string +member I18nDevtoolsStateSnapshot.locales: { code: string; displayName?: string; disabled?: boolean; }[] +member I18nDevtoolsStateSnapshot.routeName: string +member I18nDevtoolsStateSnapshot.strategy?: string | undefined +function parseInspectorNodeId: (nodeId: string) => { kind: "root" | "active" | "locale" | "chunk" | "key"; locale?: string; routeName?: string; path?: string; } + # ./helpers (src/helpers.ts) function collectTranslationPaths: (obj: Record, paths: Set, prefix?: string) => void const defaultPlural: PluralFunc diff --git a/scripts/api-surface/i18n-micro__preact.api.txt b/scripts/api-surface/i18n-micro__preact.api.txt index a7834d9d..de2983cd 100644 --- a/scripts/api-surface/i18n-micro__preact.api.txt +++ b/scripts/api-surface/i18n-micro__preact.api.txt @@ -1352,6 +1352,7 @@ member ModuleOptions.strategy?: Strategies | undefined member ModuleOptions.translationDir?: string | undefined member ModuleOptions.translationPayloads?: TranslationPayloadOptions | undefined member ModuleOptions.types?: boolean | undefined +member ModuleOptions.vueDevtools?: boolean | undefined type Params: Record type PluralFunc: (key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string | null class PreactI18n diff --git a/scripts/api-surface/i18n-micro__solid.api.txt b/scripts/api-surface/i18n-micro__solid.api.txt index 99637725..9b8dfb6e 100644 --- a/scripts/api-surface/i18n-micro__solid.api.txt +++ b/scripts/api-surface/i18n-micro__solid.api.txt @@ -107,6 +107,7 @@ member ModuleOptions.strategy?: Strategies | undefined member ModuleOptions.translationDir?: string | undefined member ModuleOptions.translationPayloads?: TranslationPayloadOptions | undefined member ModuleOptions.types?: boolean | undefined +member ModuleOptions.vueDevtools?: boolean | undefined type Params: Record type PluralFunc: (key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string | null class SolidI18n diff --git a/scripts/api-surface/i18n-micro__types.api.txt b/scripts/api-surface/i18n-micro__types.api.txt index 030795cb..b5b317f6 100644 --- a/scripts/api-surface/i18n-micro__types.api.txt +++ b/scripts/api-surface/i18n-micro__types.api.txt @@ -85,6 +85,7 @@ member ModuleOptions.strategy?: Strategies | undefined member ModuleOptions.translationDir?: string | undefined member ModuleOptions.translationPayloads?: TranslationPayloadOptions | undefined member ModuleOptions.types?: boolean | undefined +member ModuleOptions.vueDevtools?: boolean | undefined interface ModuleOptionsExtend member ModuleOptionsExtend.apiBaseClientHost?: string | undefined member ModuleOptionsExtend.apiBaseServerHost?: string | undefined @@ -135,6 +136,7 @@ member ModuleOptionsExtend.translationDir?: string | undefined member ModuleOptionsExtend.translationPayloadMode?: "premerged" | "source" | undefined member ModuleOptionsExtend.translationPayloads?: TranslationPayloadOptions | undefined member ModuleOptionsExtend.types?: boolean | undefined +member ModuleOptionsExtend.vueDevtools?: boolean | undefined interface ModulePrivateOptionsExtend member ModulePrivateOptionsExtend.apiBaseClientHost?: string | undefined member ModulePrivateOptionsExtend.apiBaseServerHost?: string | undefined @@ -185,6 +187,7 @@ member ModulePrivateOptionsExtend.strategy?: Strategies | undefined member ModulePrivateOptionsExtend.translationDir: string member ModulePrivateOptionsExtend.translationPayloads?: TranslationPayloadOptions | undefined member ModulePrivateOptionsExtend.types?: boolean | undefined +member ModulePrivateOptionsExtend.vueDevtools?: boolean | undefined type Params: Record type PluralFunc: (key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string | null interface PluralTranslations From 5ea65c08fbd46b15b75f914d25ae789be1009f3b Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:35:52 +0500 Subject: [PATCH 6/8] fix: address cubic review on Vue DevTools Keep Ask AI trigger covering Mintlify shadow host, notify inspector on mutate, encode key paths, and drop dead bridge/useState paths. Co-authored-by: Cursor --- docs/.vitepress/config.mts | 32 +-------- docs/.vitepress/theme/custom.css | 6 +- docs/api/packages/core.md | 7 +- docs/news/index.md | 1 + docs/package.json | 2 +- package.json | 2 +- packages/core/src/devtools-entry.ts | 3 + packages/core/src/devtools.ts | 71 ++++++++++++-------- packages/core/tests/devtools.test.ts | 60 +++++++++++++++-- scripts/api-surface/i18n-micro__core.api.txt | 7 +- src/runtime/devtools/bridge.ts | 11 +++ src/runtime/devtools/vue-devtools.ts | 32 ++++----- src/runtime/plugins/01.plugin.ts | 5 +- src/runtime/plugins/04.vue-devtools.ts | 13 ++-- src/runtime/utils/nuxt-i18n.ts | 37 ++++++---- 15 files changed, 175 insertions(+), 114 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 192189c3..b0065214 100755 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -85,13 +85,8 @@ trigger.type = 'button' trigger.className = 'ni18n-ask-ai' trigger.setAttribute('aria-label', 'Ask AI') trigger.innerHTML = 'Ask AI' -trigger.hidden = true document.body.appendChild(trigger) -const setTriggerVisible = (visible) => { - trigger.hidden = !visible -} - await window.MintlifyAssistant.init({ id: "mint_widget_fe4a84f3-0f6c-4dc0-94d6-3941d70125c2", appearance: { @@ -115,37 +110,14 @@ await window.MintlifyAssistant.init({ "Which routing strategy should I use?", "How do page-level translations work?", ], - hooks: { - event(event) { - if (event.type === 'open') setTriggerVisible(false) - if (event.type === 'close' || event.type === 'init') setTriggerVisible(true) - }, - }, }) -setTriggerVisible(true) - +// Keep our trigger mounted over the closed Shadow DOM vendor button (z-index 50 > 40). +// Mintlify does not expose a supported way to hide the built-in trigger. trigger.addEventListener('click', () => { void window.MintlifyAssistant.open({ source: 'custom-trigger', focus: true }) }) -// Built-in trigger is closed Shadow DOM and stays white — cover it with our button. -const hideBuiltInTrigger = () => { - const host = document.querySelector('mintlify-assistant') - if (!host) return - const root = host.shadowRoot - if (!root) return - for (const el of root.querySelectorAll('button, [role="button"]')) { - const text = (el.textContent || '').trim() - if (text.includes('Ask AI') || el.getAttribute('aria-label')?.includes('Ask')) { - el.style.setProperty('visibility', 'hidden', 'important') - el.style.setProperty('pointer-events', 'none', 'important') - } - } -} -hideBuiltInTrigger() -new MutationObserver(hideBuiltInTrigger).observe(document.body, { childList: true, subtree: true }) - new MutationObserver(() => { void window.MintlifyAssistant.update({ appearance: { theme: theme() }, diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 0377af4f..a2a521c6 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -12,7 +12,7 @@ box-shadow: none; } -/* Custom Ask AI trigger — Mintlify built-in button stays white (closed Shadow DOM) */ +/* Custom Ask AI trigger — covers Mintlify built-in button (closed Shadow DOM, no hide API) */ .ni18n-ask-ai { position: fixed; right: 24px; @@ -56,6 +56,10 @@ outline-offset: 3px; } +.dark .ni18n-ask-ai:focus-visible { + outline-color: #5fe0ae; +} + .ni18n-ask-ai__icon { font-size: 15px; line-height: 1; diff --git a/docs/api/packages/core.md b/docs/api/packages/core.md index 637d5774..514ec7ba 100644 --- a/docs/api/packages/core.md +++ b/docs/api/packages/core.md @@ -179,10 +179,13 @@ import { /* … */ } from '@i18n-micro/core/devtools' | `buildInspectorTree` | function | `(options: BuildInspectorTreeOptions) => I18nDevtoolsInspectorNode[]` | | `BuildInspectorTreeOptions` | interface | 6 members | | `countTranslationKeys` | function | `(translations: Translations \| Record \| undefined) => number` | -| `flattenTranslationNode` | function | `(obj: Record, path?: string) => I18nDevtoolsInspectorNode[]` | +| `decodeInspectorPath` | function | `(encoded: string) => string[]` | +| `encodeInspectorPath` | function | `(segments: string[]) => string` | +| `flattenTranslationNode` | function | `(obj: Record, parentSegments?: string[]) => I18nDevtoolsInspectorNode[]` | +| `getByInspectorPath` | function | `(obj: Record, segments: string[]) => unknown` | | `I18nDevtoolsInspectorNode` | interface | 4 members | | `I18nDevtoolsStateSnapshot` | interface | 7 members | -| `parseInspectorNodeId` | function | `(nodeId: string) => { kind: "root" \| "active" \| "locale" \| "chunk" \| "key"; locale?: string; routeName?: string; path?: string; }` | +| `parseInspectorNodeId` | function | `(nodeId: string) => { kind: "root" \| "active" \| "locale" \| "chunk" \| "key"; locale?: string; routeName?: string; path?: string; segments?: string[]; }` |
BuildInspectorTreeOptions — 6 members diff --git a/docs/news/index.md b/docs/news/index.md index 7cd2d771..d3f0b9c7 100644 --- a/docs/news/index.md +++ b/docs/news/index.md @@ -17,6 +17,7 @@ Adds optional **Vue DevTools** integration for live runtime debugging: custom in - New option: `i18n.vueDevtools` (default `true` in dev) - Optional peer: `@vue/devtools-api` ^8.1.5 - `@i18n-micro/core` exports devtools helpers from `@i18n-micro/core/devtools` (not in main entry) +- Inspector key paths use encoded segments (`encodeInspectorPath`) so literal dotted keys stay distinct from nested paths ## Nuxt I18n Micro v3.24.0 — Node SSR from `public/`, no Rollup `raw:` diff --git a/docs/package.json b/docs/package.json index a91c4cc7..6a310dc0 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,7 +8,7 @@ "generate": "pnpm -C ../scripts cli docs-generate", "dev": "pnpm run generate && vitepress dev .", "build": "pnpm run generate && vitepress build .", - "serve": "vitepress serve .", + "serve": "vitepress preview .", "clean-dev": "pnpm run generate && vitepress dev . --clean-cache", "preview": "vitepress preview ." }, diff --git a/package.json b/package.json index a9fdcc72..80972102 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ }, "peerDependencies": { "vue-router": "catalog:", - "@vue/devtools-api": "^8.1.5" + "@vue/devtools-api": "catalog:" }, "peerDependenciesMeta": { "@vue/devtools-api": { diff --git a/packages/core/src/devtools-entry.ts b/packages/core/src/devtools-entry.ts index 0b74eedb..9a8cb6fd 100644 --- a/packages/core/src/devtools-entry.ts +++ b/packages/core/src/devtools-entry.ts @@ -2,7 +2,10 @@ export { buildInspectorState, buildInspectorTree, countTranslationKeys, + decodeInspectorPath, + encodeInspectorPath, flattenTranslationNode, + getByInspectorPath, parseInspectorNodeId, type BuildInspectorTreeOptions, type I18nDevtoolsInspectorNode, diff --git a/packages/core/src/devtools.ts b/packages/core/src/devtools.ts index 2438be7b..431248d2 100644 --- a/packages/core/src/devtools.ts +++ b/packages/core/src/devtools.ts @@ -1,5 +1,4 @@ import type { Locale, Translations } from '@i18n-micro/types' -import { getByPath } from './helpers' import type { TranslationStorage } from './translation' export interface I18nDevtoolsInspectorNode { @@ -83,11 +82,33 @@ function collectLocaleCodes(storage: TranslationStorage, configuredLocales: Loca return [...codes].sort() } +/** + * Encode path segments so a literal key `"a.b"` and nested path `a → b` get distinct node IDs. + */ +export function encodeInspectorPath(segments: string[]): string { + return segments.map((segment) => encodeURIComponent(segment)).join('/') +} + +export function decodeInspectorPath(encoded: string): string[] { + if (!encoded) return [] + return encoded.split('/').map((segment) => decodeURIComponent(segment)) +} + +export function getByInspectorPath(obj: Record, segments: string[]): unknown { + let current: unknown = obj + for (const segment of segments) { + if (!isPlainObject(current) || !Object.prototype.hasOwnProperty.call(current, segment)) return undefined + current = current[segment] + } + return current +} + export function parseInspectorNodeId(nodeId: string): { kind: 'root' | 'active' | 'locale' | 'chunk' | 'key' locale?: string routeName?: string path?: string + segments?: string[] } { if (!nodeId || nodeId === NODE_ROOT) return { kind: 'root' } if (nodeId === NODE_ACTIVE) return { kind: 'active' } @@ -106,25 +127,32 @@ export function parseInspectorNodeId(nodeId: string): { const locale = parts[0] ?? '' const routeName = parts[1] ?? 'index' const path = parts.slice(2).join('|') - return { kind: 'key', locale, routeName, path } + return { kind: 'key', locale, routeName, path, segments: decodeInspectorPath(path) } } return { kind: 'root' } } -export function flattenTranslationNode(obj: Record, path = ''): I18nDevtoolsInspectorNode[] { - const target = path ? (getByPath(obj, path) as Record | undefined) : obj +function withKeyPrefix(locale: string, routeName: string, node: I18nDevtoolsInspectorNode): I18nDevtoolsInspectorNode { + return { + ...node, + id: `${PREFIX_KEY}${locale}|${routeName}|${node.id.slice(PREFIX_KEY.length)}`, + } +} + +export function flattenTranslationNode(obj: Record, parentSegments: string[] = []): I18nDevtoolsInspectorNode[] { + const target = parentSegments.length === 0 ? obj : getByInspectorPath(obj, parentSegments) if (!isPlainObject(target)) return [] return Object.keys(target) .filter((key) => key !== '__proto__') .map((key) => { - const childPath = path ? `${path}.${key}` : key + const segments = [...parentSegments, key] const value = target[key] const nested = isPlainObject(value) return { - id: `${PREFIX_KEY}${childPath}`, + id: `${PREFIX_KEY}${encodeInspectorPath(segments)}`, label: nested ? key : `${key}: ${formatInspectorValue(value)}`, children: nested ? [] : undefined, } @@ -170,10 +198,9 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt } if (parsed.kind === 'active') { - return flattenTranslationNode(options.activeTranslations ?? {}).map((node) => ({ - ...node, - id: `${PREFIX_KEY}${options.activeLocale}|${options.activeRouteName || 'index'}|${node.id.slice(PREFIX_KEY.length)}`, - })) + const locale = options.activeLocale + const routeName = options.activeRouteName || 'index' + return flattenTranslationNode(options.activeTranslations ?? {}).map((node) => withKeyPrefix(locale, routeName, node)) } if (parsed.kind === 'locale' && parsed.locale) { @@ -190,32 +217,18 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt const locale = parsed.locale const routeName = parsed.routeName || 'index' const chunk = options.storage.translations.get(`${locale}:${routeName}`) as Record | undefined - return flattenTranslationNode(chunk ?? {}).map((node) => ({ - ...node, - id: `${PREFIX_KEY}${locale}|${routeName}|${node.id.slice(PREFIX_KEY.length)}`, - })) + return flattenTranslationNode(chunk ?? {}).map((node) => withKeyPrefix(locale, routeName, node)) } if (parsed.kind === 'key' && parsed.locale) { const locale = parsed.locale const routeName = parsed.routeName || 'index' const chunk = options.storage.translations.get(`${locale}:${routeName}`) as Record | undefined - const path = parsed.path ?? '' - - if (path && options.activeLocale === locale && options.activeRouteName === routeName) { - const activeValue = getByPath(options.activeTranslations ?? {}, path) - if (isPlainObject(activeValue)) { - return flattenTranslationNode(activeValue, '').map((node) => ({ - ...node, - id: `${PREFIX_KEY}${locale}|${routeName}|${path}.${node.id.slice(PREFIX_KEY.length)}`, - })) - } - } + const segments = parsed.segments ?? [] + const isActiveContext = options.activeLocale === locale && options.activeRouteName === routeName + const source = isActiveContext ? (options.activeTranslations ?? chunk ?? {}) : (chunk ?? {}) - return flattenTranslationNode(chunk ?? {}, path).map((node) => ({ - ...node, - id: `${PREFIX_KEY}${locale}|${routeName}|${path ? `${path}.${node.id.slice(PREFIX_KEY.length)}` : node.id.slice(PREFIX_KEY.length)}`, - })) + return flattenTranslationNode(source, segments).map((node) => withKeyPrefix(locale, routeName, node)) } return [] diff --git a/packages/core/tests/devtools.test.ts b/packages/core/tests/devtools.test.ts index d85be4e6..f7ae85c2 100644 --- a/packages/core/tests/devtools.test.ts +++ b/packages/core/tests/devtools.test.ts @@ -1,5 +1,13 @@ import { describe, expect, test } from 'vitest' -import { buildInspectorState, buildInspectorTree, countTranslationKeys, flattenTranslationNode, parseInspectorNodeId } from '../src/devtools' +import { + buildInspectorState, + buildInspectorTree, + countTranslationKeys, + encodeInspectorPath, + flattenTranslationNode, + getByInspectorPath, + parseInspectorNodeId, +} from '../src/devtools' import type { TranslationStorage } from '../src/translation' function createStorage(entries: Record>): TranslationStorage { @@ -14,11 +22,19 @@ describe('devtools helpers', () => { expect(parseInspectorNodeId('active')).toEqual({ kind: 'active' }) expect(parseInspectorNodeId('locale|en')).toEqual({ kind: 'locale', locale: 'en' }) expect(parseInspectorNodeId('chunk|en|about')).toEqual({ kind: 'chunk', locale: 'en', routeName: 'about' }) - expect(parseInspectorNodeId('key|en|index|nav.home')).toEqual({ + expect(parseInspectorNodeId('key|en|index|nav%2Fhome')).toEqual({ kind: 'key', locale: 'en', routeName: 'index', - path: 'nav.home', + path: 'nav%2Fhome', + segments: ['nav/home'], + }) + expect(parseInspectorNodeId(`key|en|index|${encodeInspectorPath(['nav', 'home'])}`)).toEqual({ + kind: 'key', + locale: 'en', + routeName: 'index', + path: 'nav/home', + segments: ['nav', 'home'], }) }) @@ -64,19 +80,49 @@ describe('devtools helpers', () => { expect(nodes.map((node) => node.label)).toEqual(['nav', 'title: "Welcome"']) expect(nodes.find((node) => node.label === 'nav')?.children).toEqual([]) + expect(nodes.find((node) => node.label === 'nav')?.id).toBe(`key|${encodeInspectorPath(['nav'])}`) }) - test('buildInspectorTree exposes active merged translations', () => { + test('flattenTranslationNode keeps literal dotted keys distinct from nested paths', () => { + const nodes = flattenTranslationNode({ + 'a.b': 'literal', + a: { b: 'nested' }, + }) + + const ids = nodes.map((node) => node.id) + expect(ids).toContain(`key|${encodeInspectorPath(['a.b'])}`) + expect(ids).toContain(`key|${encodeInspectorPath(['a'])}`) + expect(new Set(ids).size).toBe(ids.length) + + const nested = flattenTranslationNode( + { + 'a.b': 'literal', + a: { b: 'nested' }, + }, + ['a'], + ) + expect(nested.map((node) => node.id)).toEqual([`key|${encodeInspectorPath(['a', 'b'])}`]) + expect(nested[0]?.id).not.toBe(`key|${encodeInspectorPath(['a.b'])}`) + }) + + test('buildInspectorTree exposes active merged translations including fallback-only keys', () => { + const storage = createStorage({ + 'en:index': { hello: 'Hello' }, + }) + const nodes = buildInspectorTree({ nodeId: 'active', - storage: createStorage({}), + storage, configuredLocales: [{ code: 'en' }], activeLocale: 'en', activeRouteName: 'index', - activeTranslations: { hello: 'Hello', nested: { key: 'Value' } }, + activeTranslations: { hello: 'Hello', onlyFallback: 'From outgoing' }, }) - expect(nodes.map((node) => node.label)).toEqual(['hello: "Hello"', 'nested']) + expect(nodes.map((node) => node.label)).toEqual(['hello: "Hello"', 'onlyFallback: "From outgoing"']) + + const selected = parseInspectorNodeId(nodes.find((node) => node.label.startsWith('onlyFallback'))!.id) + expect(getByInspectorPath({ hello: 'Hello', onlyFallback: 'From outgoing' }, selected.segments ?? [])).toBe('From outgoing') }) test('buildInspectorState returns context and locales', () => { diff --git a/scripts/api-surface/i18n-micro__core.api.txt b/scripts/api-surface/i18n-micro__core.api.txt index 150e427c..a80a48e6 100644 --- a/scripts/api-surface/i18n-micro__core.api.txt +++ b/scripts/api-surface/i18n-micro__core.api.txt @@ -110,7 +110,10 @@ member BuildInspectorTreeOptions.configuredLocales: Locale[] member BuildInspectorTreeOptions.nodeId: string member BuildInspectorTreeOptions.storage: TranslationStorage function countTranslationKeys: (translations: Translations | Record | undefined) => number -function flattenTranslationNode: (obj: Record, path?: string) => I18nDevtoolsInspectorNode[] +function decodeInspectorPath: (encoded: string) => string[] +function encodeInspectorPath: (segments: string[]) => string +function flattenTranslationNode: (obj: Record, parentSegments?: string[]) => I18nDevtoolsInspectorNode[] +function getByInspectorPath: (obj: Record, segments: string[]) => unknown interface I18nDevtoolsInspectorNode member I18nDevtoolsInspectorNode.children?: I18nDevtoolsInspectorNode[] | undefined member I18nDevtoolsInspectorNode.id: string @@ -124,7 +127,7 @@ member I18nDevtoolsStateSnapshot.locale: string member I18nDevtoolsStateSnapshot.locales: { code: string; displayName?: string; disabled?: boolean; }[] member I18nDevtoolsStateSnapshot.routeName: string member I18nDevtoolsStateSnapshot.strategy?: string | undefined -function parseInspectorNodeId: (nodeId: string) => { kind: "root" | "active" | "locale" | "chunk" | "key"; locale?: string; routeName?: string; path?: string; } +function parseInspectorNodeId: (nodeId: string) => { kind: "root" | "active" | "locale" | "chunk" | "key"; locale?: string; routeName?: string; path?: string; segments?: string[]; } # ./helpers (src/helpers.ts) function collectTranslationPaths: (obj: Record, paths: Set, prefix?: string) => void diff --git a/src/runtime/devtools/bridge.ts b/src/runtime/devtools/bridge.ts index ecb39568..994fdb07 100644 --- a/src/runtime/devtools/bridge.ts +++ b/src/runtime/devtools/bridge.ts @@ -9,3 +9,14 @@ export interface I18nDevtoolsBridge { i18nConfig: ModuleOptionsExtend localeState: Ref } + +/** Dev-only bridge — not serializable, so keep it off `useState`. */ +let bridge: I18nDevtoolsBridge | null = null + +export function setI18nDevtoolsBridge(value: I18nDevtoolsBridge | null): void { + bridge = value +} + +export function getI18nDevtoolsBridge(): I18nDevtoolsBridge | null { + return bridge +} diff --git a/src/runtime/devtools/vue-devtools.ts b/src/runtime/devtools/vue-devtools.ts index ac62851b..d4adcae4 100644 --- a/src/runtime/devtools/vue-devtools.ts +++ b/src/runtime/devtools/vue-devtools.ts @@ -2,13 +2,13 @@ import { buildInspectorState, buildInspectorTree, countTranslationKeys, + getByInspectorPath, parseInspectorNodeId, type I18nDevtoolsStateSnapshot, } from '@i18n-micro/core/devtools' -import { getByPath } from '@i18n-micro/core/helpers' import type { ModuleOptionsExtend } from '@i18n-micro/types' import type { App } from 'vue' -import type { NuxtI18n } from '../utils/nuxt-i18n' +import type { I18nContextChangeReason, NuxtI18n } from '../utils/nuxt-i18n' const INSPECTOR_ID = 'i18n-micro-inspector' const PLUGIN_ID = 'nuxt-i18n-micro' @@ -18,14 +18,13 @@ export interface VueI18nDevtoolsContext { app: App i18n: NuxtI18n i18nConfig: ModuleOptionsExtend - onContextChange: (listener: () => void) => () => void + onContextChange: (listener: (reason: I18nContextChangeReason) => void) => () => void onMissingKey: (listener: (locale: string, key: string, routeName: string) => void) => () => void } export interface VueI18nDevtoolsNotifier { notifyLocaleSwitch: (from: string, to: string, routeName: string) => void notifyLoad: (locale: string, routeName: string, keyCount: number) => void - notifyMissing: (locale: string, key: string, routeName: string) => void notifyTranslate: (key: string, result: unknown) => void refreshInspector: () => void } @@ -130,16 +129,18 @@ function buildNodeState(i18n: NuxtI18n, i18nConfig: ModuleOptionsExtend, nodeId: if (parsed.kind === 'key' && parsed.locale) { const routeName = parsed.routeName || 'index' - const path = parsed.path ?? '' + const segments = parsed.segments ?? [] const chunk = i18n.storage.translations.get(`${parsed.locale}:${routeName}`) as Record | undefined - const value = path ? getByPath(chunk ?? {}, path) : chunk + const isActive = i18n.getCurrentLocale() === parsed.locale && i18n.getCurrentRouteName() === routeName + const source = isActive ? (i18n.resolveTranslations() as Record) : (chunk ?? {}) + const value = segments.length ? getByInspectorPath(source, segments) : source if (isPlainObject(value)) { - return { [path || 'root']: objectToStateEntries(value) } + return { [(segments.join('.') || 'root')]: objectToStateEntries(value) } } return { - Value: [{ key: path || 'value', value }], + Value: [{ key: segments.join('.') || 'value', value }], } } @@ -258,7 +259,9 @@ export async function setupVueI18nDevtools(ctx: VueI18nDevtoolsContext): Promise ) as never }) - ctx.onContextChange(refreshInspector) + ctx.onContextChange(() => { + refreshInspector() + }) ctx.onMissingKey((locale, key, routeName) => { api.addTimelineEvent({ layerId: TIMELINE_LAYER_ID, @@ -296,17 +299,6 @@ export async function setupVueI18nDevtools(ctx: VueI18nDevtoolsContext): Promise }) refreshInspector() }, - notifyMissing(locale, key, routeName) { - api.addTimelineEvent({ - layerId: TIMELINE_LAYER_ID, - event: { - time: api.now(), - title: 'Missing translation', - subtitle: key, - data: { locale, key, routeName }, - }, - }) - }, notifyTranslate(key, result) { api.addTimelineEvent({ layerId: TIMELINE_LAYER_ID, diff --git a/src/runtime/plugins/01.plugin.ts b/src/runtime/plugins/01.plugin.ts index 55d603d5..c284de9a 100644 --- a/src/runtime/plugins/01.plugin.ts +++ b/src/runtime/plugins/01.plugin.ts @@ -25,6 +25,7 @@ import { createError, defineNuxtPlugin, navigateTo, useRouter, useRuntimeConfig import { useI18nHead } from '../composables/useI18nHead' import { useI18nLocale } from '../composables/useI18nLocale' import { createNuxtI18nPluginApi, NuxtI18n, NuxtTranslationLoader } from '../utils/nuxt-i18n' +import { setI18nDevtoolsBridge } from '../devtools/bridge' import { translationStorage } from '../utils/storage' const isDev = process.env.NODE_ENV !== 'production' @@ -140,11 +141,11 @@ export default defineNuxtPlugin(async (nuxtApp) => { }) if (import.meta.dev && import.meta.client) { - useState('i18n-micro-devtools-bridge', () => ({ + setI18nDevtoolsBridge({ i18n, i18nConfig, localeState, - })) + }) } router.beforeEach(async (to, from) => { diff --git a/src/runtime/plugins/04.vue-devtools.ts b/src/runtime/plugins/04.vue-devtools.ts index 754074a5..e0c70f2c 100644 --- a/src/runtime/plugins/04.vue-devtools.ts +++ b/src/runtime/plugins/04.vue-devtools.ts @@ -1,8 +1,7 @@ -import { useState } from '#app' import { defineNuxtPlugin, useNuxtApp } from '#imports' import type { Params } from '@i18n-micro/types' import { watch } from 'vue' -import { I18N_DEVTOOLS_BRIDGE_KEY, type I18nDevtoolsBridge } from '../devtools/bridge' +import { getI18nDevtoolsBridge } from '../devtools/bridge' import { countLoadedTranslationKeys, setupVueI18nDevtools } from '../devtools/vue-devtools' export default defineNuxtPlugin({ @@ -14,7 +13,7 @@ export default defineNuxtPlugin({ } const nuxtApp = useNuxtApp() - const bridge = useState(I18N_DEVTOOLS_BRIDGE_KEY, () => null).value + const bridge = getI18nDevtoolsBridge() if (!bridge) { return } @@ -48,8 +47,12 @@ export default defineNuxtPlugin({ }, ) - bridge.i18n.onContextChange(() => { - notifyCurrentLoad() + bridge.i18n.onContextChange((reason) => { + if (reason === 'load') { + notifyCurrentLoad() + return + } + notifier.refreshInspector() }) if (bridge.i18nConfig.debug) { diff --git a/src/runtime/utils/nuxt-i18n.ts b/src/runtime/utils/nuxt-i18n.ts index 43b9501c..49f65336 100644 --- a/src/runtime/utils/nuxt-i18n.ts +++ b/src/runtime/utils/nuxt-i18n.ts @@ -76,6 +76,9 @@ export type GetRouteTranslationContext = (route?: unknown) => RouteTranslationCo export type LoadTranslationsChunk = (locale: string, routeName?: string) => Promise> +/** Why the active translation context changed — used by Vue DevTools timeline. */ +export type I18nContextChangeReason = 'load' | 'refresh' + export interface NuxtI18nOptions extends BaseI18nOptions {} export class NuxtI18n extends BaseI18n { @@ -92,7 +95,7 @@ export class NuxtI18n extends BaseI18n { private currentLocale = '' private currentRouteName = '' private resolveRouteContext?: GetRouteTranslationContext - private readonly contextListeners = new Set<() => void>() + private readonly contextListeners = new Set<(reason: I18nContextChangeReason) => void>() private missingKeyListener?: (locale: string, key: string, routeName: string) => void constructor(options: NuxtI18nOptions = {}) { @@ -170,7 +173,7 @@ export class NuxtI18n extends BaseI18n { return this.currentRouteName } - onContextChange(listener: () => void): () => void { + onContextChange(listener: (reason: I18nContextChangeReason) => void): () => void { this.contextListeners.add(listener) return () => { this.contextListeners.delete(listener) @@ -186,9 +189,9 @@ export class NuxtI18n extends BaseI18n { } } - private notifyContextChange(): void { + private notifyContextChange(reason: I18nContextChangeReason): void { for (const listener of this.contextListeners) { - listener() + listener(reason) } } @@ -211,7 +214,7 @@ export class NuxtI18n extends BaseI18n { this.currentLocale = locale this.currentRouteName = routeName || '' triggerRef(this.contextSignal) - this.notifyContextChange() + this.notifyContextChange('load') } /** The outgoing page has unmounted, so its keys must stop resolving. */ @@ -219,7 +222,7 @@ export class NuxtI18n extends BaseI18n { if (this.outgoingTranslations === null) return this.outgoingTranslations = null triggerRef(this.contextSignal) - this.notifyContextChange() + this.notifyContextChange('refresh') } mergeTranslations(newTranslations: Translations): void { @@ -228,7 +231,7 @@ export class NuxtI18n extends BaseI18n { // walking the whole thing again costs its size for the sake of a few new keys. this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations as Record) triggerRef(this.contextSignal) - this.notifyContextChange() + this.notifyContextChange('load') } async loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise { @@ -236,7 +239,7 @@ export class NuxtI18n extends BaseI18n { if (locale === this.currentLocale && routeName === this.currentRouteName) { this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, translations as Record) triggerRef(this.contextSignal) - this.notifyContextChange() + this.notifyContextChange('load') } } @@ -250,7 +253,7 @@ export class NuxtI18n extends BaseI18n { if (locale === this.currentLocale && routeName === this.currentRouteName) { this.cachedTranslations = deepMergeTranslationsRecursive(this.cachedTranslations, newTranslations as Record) triggerRef(this.contextSignal) - this.notifyContextChange() + this.notifyContextChange('load') } } @@ -315,21 +318,26 @@ export class NuxtI18n extends BaseI18n { setTranslationAtKey(this.getChunk(this.currentLocale, this.currentRouteName), String(key), value), ) triggerRef(this.contextSignal) + this.notifyContextChange('refresh') } - protected override getMissingContext(_routeContext?: unknown): { locale: string; routeName: string } { + protected override getMissingContext(routeContext?: unknown): { locale: string; routeName: string } { + if (routeContext && this.resolveRouteContext) { + return this.resolveRouteContext(routeContext) + } return { locale: this.currentLocale, routeName: this.currentRouteName } } - protected override warnMissing(key: TranslationKey): void { - this.missingKeyListener?.(this.currentLocale, String(key), this.currentRouteName) + protected override warnMissing(key: TranslationKey, routeContext?: unknown): void { + const { locale, routeName } = this.getMissingContext(routeContext) + this.missingKeyListener?.(locale, String(key), routeName) const customHandler = this.getCustomMissingHandler?.() if (customHandler) { - customHandler(this.currentLocale, String(key), this.currentRouteName) + customHandler(locale, String(key), routeName) return } if (this.missingWarn && process.env.NODE_ENV !== 'production' && import.meta.client) { - console.warn(`[i18n] Missing key '${key}' in '${this.currentLocale}' for route '${this.currentRouteName}'`) + console.warn(`[i18n] Missing key '${key}' in '${locale}' for route '${routeName}'`) } } @@ -338,6 +346,7 @@ export class NuxtI18n extends BaseI18n { this.cachedTranslations = {} this.outgoingTranslations = null triggerRef(this.contextSignal) + this.notifyContextChange('refresh') } } From 51979f7c52a62fd8fa27ded810be952fe08f0595 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:45:05 +0500 Subject: [PATCH 7/8] fix: cover cubic follow-ups on DevTools context Normalize empty route to index, drop duplicate docs serve, test onContextChange reasons and route-aware missing keys. Co-authored-by: Cursor --- docs/package.json | 1 - package.json | 2 +- packages/core/src/devtools.ts | 2 +- src/runtime/devtools/vue-devtools.ts | 4 ++-- test/nuxt-i18n.test.ts | 32 ++++++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/package.json b/docs/package.json index 6a310dc0..c9bc0bd9 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,7 +8,6 @@ "generate": "pnpm -C ../scripts cli docs-generate", "dev": "pnpm run generate && vitepress dev .", "build": "pnpm run generate && vitepress build .", - "serve": "vitepress preview .", "clean-dev": "pnpm run generate && vitepress dev . --clean-cache", "preview": "vitepress preview ." }, diff --git a/package.json b/package.json index 80972102..711c4c99 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "typecheck:nuxt": "nuxt typecheck --no-emit", "docs:dev": "pnpm --filter @i18n-micro/docs dev", "docs:build": "pnpm --filter @i18n-micro/docs build", - "docs:serve": "pnpm --filter @i18n-micro/docs serve", + "docs:serve": "pnpm --filter @i18n-micro/docs preview", "docs:clean-dev": "pnpm --filter @i18n-micro/docs clean-dev", "docs:preview": "pnpm --filter @i18n-micro/docs preview", "smoke:pack": "pnpm -C scripts cli smoke-pack", diff --git a/packages/core/src/devtools.ts b/packages/core/src/devtools.ts index 431248d2..51d9f4aa 100644 --- a/packages/core/src/devtools.ts +++ b/packages/core/src/devtools.ts @@ -225,7 +225,7 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt const routeName = parsed.routeName || 'index' const chunk = options.storage.translations.get(`${locale}:${routeName}`) as Record | undefined const segments = parsed.segments ?? [] - const isActiveContext = options.activeLocale === locale && options.activeRouteName === routeName + const isActiveContext = options.activeLocale === locale && (options.activeRouteName || 'index') === routeName const source = isActiveContext ? (options.activeTranslations ?? chunk ?? {}) : (chunk ?? {}) return flattenTranslationNode(source, segments).map((node) => withKeyPrefix(locale, routeName, node)) diff --git a/src/runtime/devtools/vue-devtools.ts b/src/runtime/devtools/vue-devtools.ts index d4adcae4..97971efe 100644 --- a/src/runtime/devtools/vue-devtools.ts +++ b/src/runtime/devtools/vue-devtools.ts @@ -120,7 +120,7 @@ function buildNodeState(i18n: NuxtI18n, i18nConfig: ModuleOptionsExtend, nodeId: { key: 'keys', value: countTranslationKeys(chunk) }, { key: 'isActive', - value: i18n.getCurrentLocale() === parsed.locale && i18n.getCurrentRouteName() === routeName, + value: i18n.getCurrentLocale() === parsed.locale && (i18n.getCurrentRouteName() || 'index') === routeName, }, ], Translations: entries.length ? entries : [{ key: '—', value: 'Chunk is loaded but empty' }], @@ -131,7 +131,7 @@ function buildNodeState(i18n: NuxtI18n, i18nConfig: ModuleOptionsExtend, nodeId: const routeName = parsed.routeName || 'index' const segments = parsed.segments ?? [] const chunk = i18n.storage.translations.get(`${parsed.locale}:${routeName}`) as Record | undefined - const isActive = i18n.getCurrentLocale() === parsed.locale && i18n.getCurrentRouteName() === routeName + const isActive = i18n.getCurrentLocale() === parsed.locale && (i18n.getCurrentRouteName() || 'index') === routeName const source = isActive ? (i18n.resolveTranslations() as Record) : (chunk ?? {}) const value = segments.length ? getByInspectorPath(source, segments) : source diff --git a/test/nuxt-i18n.test.ts b/test/nuxt-i18n.test.ts index 81996eb4..37266f33 100644 --- a/test/nuxt-i18n.test.ts +++ b/test/nuxt-i18n.test.ts @@ -146,6 +146,38 @@ describe('NuxtI18n', () => { expect(i18n.getChunk('en', 'index')).toEqual({}) }) + it('onContextChange reports load vs refresh reasons', () => { + const i18n = new NuxtI18n({ missingWarn: false }) + const reasons: string[] = [] + i18n.onContextChange((reason) => { + reasons.push(reason) + }) + + i18n.applySwitchContext('en', 'index', { title: 'Hello' }) + i18n.setTranslation('title', 'Updated') + i18n.applySwitchContext('en', 'about', { title: 'About' }) + i18n.finishTransition() + i18n.clearCache() + + expect(reasons).toEqual(['load', 'refresh', 'load', 'refresh', 'refresh']) + }) + + it('missing-key listener uses route context from tForRoute lookups', () => { + const i18n = new NuxtI18n({ missingWarn: false }) + i18n.setRouteContextResolver((route) => route as { locale: string; routeName: string }) + i18n.setChunk('en', 'page-a', { title: 'A' }) + i18n.applySwitchContext('en', 'page-b', { title: 'B' }) + + const missing: Array<{ locale: string; key: string; routeName: string }> = [] + i18n.onMissingKey((locale, key, routeName) => { + missing.push({ locale, key, routeName }) + }) + + const tForA = i18n.tForRoute({ locale: 'en', routeName: 'page-a' }) + expect(tForA('missing-on-a')).toBe('missing-on-a') + expect(missing).toEqual([{ locale: 'en', key: 'missing-on-a', routeName: 'page-a' }]) + }) + it('resolveTranslations exposes the hot-reload view during a transition', () => { const i18n = new NuxtI18n({ missingWarn: false }) i18n.applySwitchContext('en', 'page-a', { common: { fromA: 'From A' } }) From 1ea675542c73d4d4b65e37999d26282e0f5dda2a Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Wed, 5 Aug 2026 16:59:59 +0500 Subject: [PATCH 8/8] fix(core): normalize empty activeRoute for Active tags Keep chunk Active tags in sync with key-level active detection. Co-authored-by: Cursor --- packages/core/src/devtools.ts | 16 ++++++++-------- packages/core/tests/devtools.test.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/core/src/devtools.ts b/packages/core/src/devtools.ts index 51d9f4aa..fbb43a41 100644 --- a/packages/core/src/devtools.ts +++ b/packages/core/src/devtools.ts @@ -162,13 +162,15 @@ export function flattenTranslationNode(obj: Record, parentSegme export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevtoolsInspectorNode[] { const parsed = parseInspectorNodeId(options.nodeId) + const activeLocale = options.activeLocale + const activeRoute = options.activeRouteName || 'index' if (parsed.kind === 'root') { const locales = collectLocaleCodes(options.storage, options.configuredLocales) const nodes: I18nDevtoolsInspectorNode[] = [ { id: NODE_ACTIVE, - label: `Active (${options.activeLocale}:${options.activeRouteName || 'index'})`, + label: `Active (${activeLocale}:${activeRoute})`, tags: [ACTIVE_TAG], }, ] @@ -180,7 +182,7 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt nodes.push({ id: `${PREFIX_LOCALE}${code}`, label: configured?.displayName ? `${code} (${configured.displayName})` : code, - tags: code === options.activeLocale ? [{ ...ACTIVE_TAG, tooltip: 'Active locale' }] : undefined, + tags: code === activeLocale ? [{ ...ACTIVE_TAG, tooltip: 'Active locale' }] : undefined, }) // Flat chunk nodes (not nested): Vue DevTools selection is unreliable for nested @@ -189,7 +191,7 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt nodes.push({ id: `${PREFIX_CHUNK}${code}|${routeName}`, label: `${code}:${routeName}`, - tags: activeChunkTag(code, routeName, options.activeLocale, options.activeRouteName), + tags: activeChunkTag(code, routeName, activeLocale, activeRoute), }) } } @@ -198,9 +200,7 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt } if (parsed.kind === 'active') { - const locale = options.activeLocale - const routeName = options.activeRouteName || 'index' - return flattenTranslationNode(options.activeTranslations ?? {}).map((node) => withKeyPrefix(locale, routeName, node)) + return flattenTranslationNode(options.activeTranslations ?? {}).map((node) => withKeyPrefix(activeLocale, activeRoute, node)) } if (parsed.kind === 'locale' && parsed.locale) { @@ -209,7 +209,7 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt id: `${PREFIX_CHUNK}${locale}|${routeName}`, label: routeName, children: [], - tags: activeChunkTag(locale, routeName, options.activeLocale, options.activeRouteName), + tags: activeChunkTag(locale, routeName, activeLocale, activeRoute), })) } @@ -225,7 +225,7 @@ export function buildInspectorTree(options: BuildInspectorTreeOptions): I18nDevt const routeName = parsed.routeName || 'index' const chunk = options.storage.translations.get(`${locale}:${routeName}`) as Record | undefined const segments = parsed.segments ?? [] - const isActiveContext = options.activeLocale === locale && (options.activeRouteName || 'index') === routeName + const isActiveContext = activeLocale === locale && activeRoute === routeName const source = isActiveContext ? (options.activeTranslations ?? chunk ?? {}) : (chunk ?? {}) return flattenTranslationNode(source, segments).map((node) => withKeyPrefix(locale, routeName, node)) diff --git a/packages/core/tests/devtools.test.ts b/packages/core/tests/devtools.test.ts index f7ae85c2..9a2c0168 100644 --- a/packages/core/tests/devtools.test.ts +++ b/packages/core/tests/devtools.test.ts @@ -148,4 +148,21 @@ describe('devtools helpers', () => { }), ).toBe(4) }) + + test('buildInspectorTree tags index chunk when activeRouteName is empty', () => { + const storage = createStorage({ + 'en:index': { hello: 'Hello' }, + }) + + const root = buildInspectorTree({ + nodeId: 'root', + storage, + configuredLocales: [{ code: 'en' }], + activeLocale: 'en', + activeRouteName: '', + }) + + expect(root.find((node) => node.id === 'active')?.label).toBe('Active (en:index)') + expect(root.find((node) => node.id === 'chunk|en|index')?.tags?.[0]?.label).toBe('Active') + }) })