nuxt-i18n-micro 1.96.0 → 1.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Donate](https://img.shields.io/badge/Donate-Donationalerts-ff4081?style=for-the-badge)](https://www.donationalerts.com/r/s00d88)
5
5
 
6
6
  <p align="center">
7
- <img src="https://github.com/s00d/nuxt-i18n-micro/blob/main/logo.png?raw=true" alt="logo">
7
+ <img src="https://github.com/s00d/nuxt-i18n-micro/blob/main/branding/logo_full.png?raw=true" alt="logo">
8
8
  </p>
9
9
 
10
10
  # Nuxt I18n Micro
@@ -8,5 +8,5 @@
8
8
  <link rel="prefetch" as="script" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/BrUpQP6I.js">
9
9
  <link rel="prefetch" as="style" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/error-500.DGwSTbEi.css">
10
10
  <link rel="prefetch" as="script" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/C0zq22yw.js">
11
- <script type="module" src="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/ivw-WKkv.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1758176687705,false]</script>
12
- <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__NUXT_DEVTOOLS_I18N_BASE__/",buildId:"2bd7945f-6cbd-45d4-9b72-30c2c2aeb875",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
11
+ <script type="module" src="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/ivw-WKkv.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1759229335371,false]</script>
12
+ <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__NUXT_DEVTOOLS_I18N_BASE__/",buildId:"9bdb882b-a145-42be-bbc5-f7c9185266d5",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
@@ -8,5 +8,5 @@
8
8
  <link rel="prefetch" as="script" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/BrUpQP6I.js">
9
9
  <link rel="prefetch" as="style" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/error-500.DGwSTbEi.css">
10
10
  <link rel="prefetch" as="script" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/C0zq22yw.js">
11
- <script type="module" src="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/ivw-WKkv.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1758176687705,false]</script>
12
- <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__NUXT_DEVTOOLS_I18N_BASE__/",buildId:"2bd7945f-6cbd-45d4-9b72-30c2c2aeb875",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
11
+ <script type="module" src="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/ivw-WKkv.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1759229335372,false]</script>
12
+ <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__NUXT_DEVTOOLS_I18N_BASE__/",buildId:"9bdb882b-a145-42be-bbc5-f7c9185266d5",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
@@ -1 +1 @@
1
- {"id":"2bd7945f-6cbd-45d4-9b72-30c2c2aeb875","timestamp":1758176680760}
1
+ {"id":"9bdb882b-a145-42be-bbc5-f7c9185266d5","timestamp":1759229326396}
@@ -0,0 +1 @@
1
+ {"id":"9bdb882b-a145-42be-bbc5-f7c9185266d5","timestamp":1759229326396,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
@@ -8,5 +8,5 @@
8
8
  <link rel="prefetch" as="script" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/BrUpQP6I.js">
9
9
  <link rel="prefetch" as="style" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/error-500.DGwSTbEi.css">
10
10
  <link rel="prefetch" as="script" crossorigin href="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/C0zq22yw.js">
11
- <script type="module" src="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/ivw-WKkv.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1758176687705,false]</script>
12
- <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__NUXT_DEVTOOLS_I18N_BASE__/",buildId:"2bd7945f-6cbd-45d4-9b72-30c2c2aeb875",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
11
+ <script type="module" src="/__NUXT_DEVTOOLS_I18N_BASE__/_nuxt/ivw-WKkv.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1759229335372,false]</script>
12
+ <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__NUXT_DEVTOOLS_I18N_BASE__/",buildId:"9bdb882b-a145-42be-bbc5-f7c9185266d5",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
3
  "configKey": "i18n",
4
- "version": "1.96.0",
4
+ "version": "1.98.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,12 +1,14 @@
1
- import path, { resolve } from 'node:path';
1
+ import path, { resolve, join } from 'node:path';
2
2
  import * as fs from 'node:fs';
3
- import fs__default, { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import fs__default, { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
4
4
  import { useNuxt, defineNuxtModule, useLogger, createResolver, addTemplate, addImportsDir, addPlugin, addServerHandler, addComponentsDir, addTypeTemplate, addPrerenderRoutes } from '@nuxt/kit';
5
5
  import { watch } from 'chokidar';
6
6
  import { isPrefixAndDefaultStrategy, isPrefixStrategy, isNoPrefixStrategy, isPrefixExceptDefaultStrategy, withPrefixStrategy } from 'nuxt-i18n-micro-core';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
9
9
  import sirv from 'sirv';
10
+ import { isInternalPath } from '../dist/runtime/utils/path-utils.js';
11
+ import { globby } from 'globby';
10
12
 
11
13
  const DEVTOOLS_UI_PORT = 3030;
12
14
  const DEVTOOLS_UI_ROUTE = "/__nuxt-i18n-micro";
@@ -108,27 +110,88 @@ function setupDevToolsUI(options, resolve2) {
108
110
  });
109
111
  }
110
112
 
111
- const isInternalPath = (p) => /(?:^|\/)__[^/]+/.test(p);
112
- function extractLocaleRoutes(content, filePath) {
113
+ function extractDefineI18nRouteData(content, filePath) {
113
114
  const defineMatch = content.match(/\$?\bdefineI18nRoute\s*\(\s*\{[\s\S]*?\}\s*\)/);
114
- if (defineMatch) {
115
- const localeRoutesMatch = defineMatch[0].match(/localeRoutes:\s*(\{[\s\S]*?\})/);
116
- if (localeRoutesMatch && localeRoutesMatch[1]) {
117
- try {
118
- const parsedLocaleRoutes = Function('"use strict";return (' + localeRoutesMatch[1] + ")")();
119
- if (typeof parsedLocaleRoutes === "object" && parsedLocaleRoutes !== null) {
120
- if (validateDefineI18nRouteConfig(parsedLocaleRoutes)) {
121
- return parsedLocaleRoutes;
122
- }
115
+ if (!defineMatch) {
116
+ return { locales: null, localeRoutes: null };
117
+ }
118
+ const defineContent = defineMatch[0];
119
+ let locales = null;
120
+ let localeRoutes = null;
121
+ let localesStr = "";
122
+ const localesStart = defineContent.indexOf("locales:");
123
+ if (localesStart !== -1) {
124
+ const afterLocales = defineContent.substring(localesStart + 8);
125
+ const trimmed = afterLocales.trim();
126
+ if (trimmed.startsWith("[")) {
127
+ let bracketCount = 0;
128
+ let i = 0;
129
+ for (; i < trimmed.length; i++) {
130
+ if (trimmed[i] === "[") bracketCount++;
131
+ if (trimmed[i] === "]") bracketCount--;
132
+ if (bracketCount === 0) break;
133
+ }
134
+ localesStr = trimmed.substring(0, i + 1);
135
+ } else if (trimmed.startsWith("{")) {
136
+ let braceCount = 0;
137
+ let i = 0;
138
+ for (; i < trimmed.length; i++) {
139
+ if (trimmed[i] === "{") braceCount++;
140
+ if (trimmed[i] === "}") braceCount--;
141
+ if (braceCount === 0) break;
142
+ }
143
+ localesStr = trimmed.substring(0, i + 1);
144
+ }
145
+ }
146
+ if (localesStr) {
147
+ try {
148
+ const localesStrTrimmed = localesStr.trim();
149
+ if (localesStrTrimmed.startsWith("[") && localesStrTrimmed.endsWith("]")) {
150
+ const arrayMatch = localesStrTrimmed.match(/\[(.*?)\]/s);
151
+ if (arrayMatch && arrayMatch[1]) {
152
+ const elements = arrayMatch[1].split(",").map((el) => el.trim().replace(/['"]/g, "")).filter((el) => el.length > 0);
153
+ locales = elements;
154
+ }
155
+ }
156
+ if (localesStrTrimmed.startsWith("{") && localesStrTrimmed.endsWith("}")) {
157
+ const topLevelKeyMatches = localesStrTrimmed.match(/^\s*(\w+)\s*:\s*\{/gm);
158
+ if (topLevelKeyMatches) {
159
+ const keys = topLevelKeyMatches.map((match) => {
160
+ const keyMatch = match.match(/^\s*(\w+)\s*:/);
161
+ return keyMatch ? keyMatch[1] : "";
162
+ }).filter((key) => key.length > 0);
163
+ locales = keys;
123
164
  } else {
124
- console.error("localeRoutes found but it is not a valid object in file:", filePath);
165
+ const fallbackMatches = localesStrTrimmed.match(/(\w+)\s*:\s*\{/g);
166
+ if (fallbackMatches) {
167
+ const keys = fallbackMatches.map((match) => {
168
+ const keyMatch = match.match(/(\w+)\s*:/);
169
+ return keyMatch ? keyMatch[1] : "";
170
+ }).filter((key) => key.length > 0);
171
+ locales = keys;
172
+ }
125
173
  }
126
- } catch (error) {
127
- console.error("Failed to parse localeRoutes:", error, "in file:", filePath);
128
174
  }
175
+ } catch (error) {
176
+ console.error("Failed to parse locales:", error, "in file:", filePath);
129
177
  }
130
178
  }
131
- return null;
179
+ const localeRoutesMatch = defineContent.match(/localeRoutes:\s*(\{[\s\S]*?\})/);
180
+ if (localeRoutesMatch && localeRoutesMatch[1]) {
181
+ try {
182
+ const parsedLocaleRoutes = Function('"use strict";return (' + localeRoutesMatch[1] + ")")();
183
+ if (typeof parsedLocaleRoutes === "object" && parsedLocaleRoutes !== null) {
184
+ if (validateDefineI18nRouteConfig(parsedLocaleRoutes)) {
185
+ localeRoutes = parsedLocaleRoutes;
186
+ }
187
+ } else {
188
+ console.error("localeRoutes found but it is not a valid object in file:", filePath);
189
+ }
190
+ } catch (error) {
191
+ console.error("Failed to parse localeRoutes:", error, "in file:", filePath);
192
+ }
193
+ }
194
+ return { locales, localeRoutes };
132
195
  }
133
196
  function validateDefineI18nRouteConfig(obj) {
134
197
  if (typeof obj !== "object") return false;
@@ -176,14 +239,18 @@ class PageManager {
176
239
  localizedPaths = {};
177
240
  activeLocaleCodes;
178
241
  globalLocaleRoutes;
242
+ filesLocaleRoutes;
179
243
  noPrefixRedirect;
180
- constructor(locales, defaultLocaleCode, strategy, globalLocaleRoutes, noPrefixRedirect) {
244
+ excludePatterns;
245
+ constructor(locales, defaultLocaleCode, strategy, globalLocaleRoutes, filesLocaleRoutes, noPrefixRedirect, excludePatterns) {
181
246
  this.locales = locales;
182
247
  this.defaultLocale = this.findLocaleByCode(defaultLocaleCode) || { code: defaultLocaleCode };
183
248
  this.strategy = strategy;
184
249
  this.noPrefixRedirect = noPrefixRedirect;
250
+ this.excludePatterns = excludePatterns;
185
251
  this.activeLocaleCodes = this.computeActiveLocaleCodes();
186
252
  this.globalLocaleRoutes = globalLocaleRoutes || {};
253
+ this.filesLocaleRoutes = filesLocaleRoutes || {};
187
254
  }
188
255
  findLocaleByCode(code) {
189
256
  return this.locales.find((locale) => locale.code === code);
@@ -200,7 +267,7 @@ class PageManager {
200
267
  this.localizedPaths = this.extractLocalizedPaths(pages);
201
268
  const additionalRoutes = [];
202
269
  for (const page of [...pages]) {
203
- if (page.path && isInternalPath(page.path)) {
270
+ if (page.path && isInternalPath(page.path, this.excludePatterns)) {
204
271
  continue;
205
272
  }
206
273
  if (!page.name && page.file?.endsWith(".vue")) {
@@ -222,7 +289,7 @@ class PageManager {
222
289
  if (!page) continue;
223
290
  const pagePath = page.path ?? "";
224
291
  const pageName = page.name ?? "";
225
- if (isInternalPath(pagePath)) continue;
292
+ if (isInternalPath(pagePath, this.excludePatterns)) continue;
226
293
  if (this.globalLocaleRoutes[pageName] === false) continue;
227
294
  if (!/^\/:locale/.test(pagePath) && pagePath !== "/") {
228
295
  pages.splice(i, 1);
@@ -237,13 +304,10 @@ class PageManager {
237
304
  const pageName = buildRouteNameFromRoute(page.name, page.path);
238
305
  const globalLocalePath = this.globalLocaleRoutes[pageName];
239
306
  if (!globalLocalePath) {
240
- if (page.file) {
241
- const fileContent = readFileSync(page.file, "utf-8");
242
- const localeRoutes = extractLocaleRoutes(fileContent, page.file);
243
- if (localeRoutes) {
244
- const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
245
- localizedPaths[normalizedFullPath] = localeRoutes;
246
- }
307
+ const filesLocalePath = this.filesLocaleRoutes[pageName];
308
+ if (filesLocalePath && typeof filesLocalePath === "object") {
309
+ const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
310
+ localizedPaths[normalizedFullPath] = filesLocalePath;
247
311
  }
248
312
  } else if (typeof globalLocalePath === "object") {
249
313
  const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
@@ -578,7 +642,8 @@ const module = defineNuxtModule({
578
642
  if (!selectedForm) return null;
579
643
  return selectedForm.trim().replace("{count}", count.toString());
580
644
  },
581
- customRegexMatcher: void 0
645
+ customRegexMatcher: void 0,
646
+ excludePatterns: void 0
582
647
  },
583
648
  async setup(options, nuxt) {
584
649
  const defaultLocale = process.env.DEFAULT_LOCALE ?? options.defaultLocale ?? "en";
@@ -596,7 +661,26 @@ const module = defineNuxtModule({
596
661
  const resolver = createResolver(import.meta.url);
597
662
  const rootDirs = nuxt.options._layers.map((layer) => layer.config.rootDir).reverse();
598
663
  const localeManager = new LocaleManager(options, rootDirs);
599
- const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy, options.globalLocaleRoutes, options.noPrefixRedirect);
664
+ const routeLocales = {};
665
+ const globalLocaleRoutes = {};
666
+ const pageFiles = await globby("pages/**/*.vue", { cwd: nuxt.options.rootDir });
667
+ for (const pageFile of pageFiles) {
668
+ const fullPath = join(nuxt.options.rootDir, pageFile);
669
+ try {
670
+ const fileContent = readFileSync(fullPath, "utf-8");
671
+ const { locales: extractedLocales, localeRoutes } = extractDefineI18nRouteData(fileContent, fullPath);
672
+ const routePath = pageFile.replace(/^pages\//, "/").replace(/\/index\.vue$/, "").replace(/\.vue$/, "").replace(/\/$/, "") || "/";
673
+ const pageName = routePath.replace(/[^a-z0-9]/gi, "-").replace(/^-+|-+$/g, "");
674
+ if (extractedLocales) {
675
+ routeLocales[routePath] = extractedLocales;
676
+ }
677
+ if (localeRoutes) {
678
+ globalLocaleRoutes[pageName] = localeRoutes;
679
+ }
680
+ } catch {
681
+ }
682
+ }
683
+ const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy, options.globalLocaleRoutes, globalLocaleRoutes, options.noPrefixRedirect, options.excludePatterns);
600
684
  addTemplate({
601
685
  filename: "i18n.plural.mjs",
602
686
  write: true,
@@ -620,7 +704,11 @@ const module = defineNuxtModule({
620
704
  apiBaseUrl,
621
705
  isSSG,
622
706
  disablePageLocales: options.disablePageLocales ?? false,
623
- canonicalQueryWhitelist: options.canonicalQueryWhitelist ?? []
707
+ canonicalQueryWhitelist: options.canonicalQueryWhitelist ?? [],
708
+ excludePatterns: options.excludePatterns ?? [],
709
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
710
+ // @ts-ignore
711
+ routeLocales
624
712
  };
625
713
  if (typeof options.customRegexMatcher !== "undefined") {
626
714
  const localeCodes = localeManager.locales.map((l) => l.code);
@@ -734,7 +822,7 @@ const module = defineNuxtModule({
734
822
  const processPageWithChildren = (page, parentPath = "") => {
735
823
  if (!page.path) return;
736
824
  const fullPath = path.posix.normalize(`${parentPath}/${page.path}`);
737
- if (isInternalPath(fullPath)) {
825
+ if (isInternalPath(fullPath, options.excludePatterns)) {
738
826
  return;
739
827
  }
740
828
  const routeRule = routeRules[fullPath];
@@ -753,13 +841,13 @@ const module = defineNuxtModule({
753
841
  if (localizedRouteRule && localizedRouteRule.prerender === false) {
754
842
  return;
755
843
  }
756
- if (!isInternalPath(localizedPath)) {
844
+ if (!isInternalPath(localizedPath, options.excludePatterns)) {
757
845
  prerenderRoutes.push(localizedPath);
758
846
  }
759
847
  }
760
848
  });
761
849
  } else {
762
- if (!isInternalPath(fullPath)) {
850
+ if (!isInternalPath(fullPath, options.excludePatterns)) {
763
851
  prerenderRoutes.push(fullPath);
764
852
  }
765
853
  }
@@ -883,7 +971,7 @@ const module = defineNuxtModule({
883
971
  const routesSet = prerenderRoutes.routes;
884
972
  const routesToRemove = [];
885
973
  routesSet.forEach((route) => {
886
- if (isInternalPath(route)) {
974
+ if (isInternalPath(route, options.excludePatterns)) {
887
975
  routesToRemove.push(route);
888
976
  }
889
977
  });
@@ -891,7 +979,7 @@ const module = defineNuxtModule({
891
979
  const additionalRoutes = /* @__PURE__ */ new Set();
892
980
  const routeRules = nuxt.options.routeRules || {};
893
981
  routesSet.forEach((route) => {
894
- if (!/\.[a-z0-9]+$/i.test(route) && !isInternalPath(route)) {
982
+ if (!/\.[a-z0-9]+$/i.test(route) && !isInternalPath(route, options.excludePatterns)) {
895
983
  localeManager.locales.forEach((locale) => {
896
984
  const shouldGenerate = locale.code !== defaultLocale || withPrefixStrategy(options.strategy);
897
985
  if (shouldGenerate) {
@@ -3,13 +3,22 @@
3
3
  </template>
4
4
 
5
5
  <script setup>
6
- import { useRoute, useI18n, createError, navigateTo } from "#imports";
6
+ import { useRoute, useI18n, createError, navigateTo, useRuntimeConfig } from "#imports";
7
+ import { isInternalPath } from "../utils/path-utils";
7
8
  const route = useRoute();
8
9
  const { $getLocales, $defaultLocale } = useI18n();
10
+ const config = useRuntimeConfig();
9
11
  const locales = $getLocales().map((locale) => locale.code);
10
12
  const defaultLocale = $defaultLocale() || "en";
11
13
  const pathSegments = route.fullPath.split("/");
12
14
  const firstSegment = pathSegments[1];
15
+ const excludePatterns = config.public.i18nConfig?.excludePatterns;
16
+ if (isInternalPath(route.fullPath, excludePatterns)) {
17
+ throw createError({
18
+ statusCode: 404,
19
+ statusMessage: "Static file - should not be processed by i18n"
20
+ });
21
+ }
13
22
  const generateRouteName = (segments) => {
14
23
  return segments.slice(1).map((segment) => segment.replace(/:/g, "")).join("-");
15
24
  };
@@ -22,7 +22,7 @@ export declare const useLocaleHead: ({ addDirAttribute, identifierAttribute, add
22
22
  identifierAttribute?: string | undefined;
23
23
  addSeoAttributes?: boolean | undefined;
24
24
  baseUrl?: string | undefined;
25
- }) => import("vue").Ref<{
25
+ }) => Promise<import("vue").Ref<{
26
26
  htmlAttrs: {
27
27
  lang?: string | undefined;
28
28
  dir?: "ltr" | "rtl" | "auto" | undefined;
@@ -54,5 +54,5 @@ export declare const useLocaleHead: ({ addDirAttribute, identifierAttribute, add
54
54
  property: string;
55
55
  content: string;
56
56
  }[];
57
- }>;
57
+ }>>;
58
58
  export {};
@@ -1,7 +1,7 @@
1
1
  import { joinURL, parseURL, withQuery } from "ufo";
2
2
  import { isPrefixExceptDefaultStrategy, isNoPrefixStrategy } from "nuxt-i18n-micro-core";
3
3
  import { unref, useRoute, useRuntimeConfig, watch, onUnmounted, ref, useNuxtApp } from "#imports";
4
- export const useLocaleHead = ({ addDirAttribute = true, identifierAttribute = "id", addSeoAttributes = true, baseUrl = "/" } = {}) => {
4
+ export const useLocaleHead = async ({ addDirAttribute = true, identifierAttribute = "id", addSeoAttributes = true, baseUrl = "/" } = {}) => {
5
5
  const metaObject = ref({
6
6
  htmlAttrs: {},
7
7
  link: [],
@@ -19,15 +19,17 @@ export const useLocaleHead = ({ addDirAttribute = true, identifierAttribute = "i
19
19
  return withQuery(pathname, filtered);
20
20
  }
21
21
  function updateMeta() {
22
- const { defaultLocale, strategy, canonicalQueryWhitelist } = useRuntimeConfig().public.i18nConfig;
22
+ const { defaultLocale, strategy, canonicalQueryWhitelist, routeLocales } = useRuntimeConfig().public.i18nConfig;
23
23
  const { $getLocales, $getLocale } = useNuxtApp();
24
24
  if (!$getLocale || !$getLocales) return;
25
25
  const route = useRoute();
26
26
  const locale = unref($getLocale());
27
- const locales = unref($getLocales());
27
+ const allLocales = unref($getLocales());
28
28
  const routeName = (route.name ?? "").toString();
29
29
  const currentLocale = unref($getLocales().find((loc) => loc.code === locale));
30
30
  if (!currentLocale) return;
31
+ const currentRouteLocales = routeLocales?.[routeName] || routeLocales?.[route.path];
32
+ const locales = currentRouteLocales ? allLocales.filter((loc) => currentRouteLocales.includes(loc.code)) : allLocales;
31
33
  const currentIso = currentLocale.iso || locale;
32
34
  const currentDir = currentLocale.dir || "auto";
33
35
  let fullPath = unref(route.fullPath);
@@ -55,7 +57,7 @@ export const useLocaleHead = ({ addDirAttribute = true, identifierAttribute = "i
55
57
  meta: []
56
58
  };
57
59
  if (!addSeoAttributes) return;
58
- const alternateLocales = $getLocales() ?? [];
60
+ const alternateLocales = locales;
59
61
  const ogLocaleMeta = {
60
62
  [identifierAttribute]: "i18n-og",
61
63
  property: "og:locale",
@@ -2,20 +2,18 @@ import { useLocaleHead } from "../composables/useLocaleHead.js";
2
2
  import { useRequestURL, useHead, defineNuxtPlugin, useRuntimeConfig } from "#imports";
3
3
  const host = process.env.HOST ?? "localhost";
4
4
  const port = process.env.PORT ?? "host";
5
- export default defineNuxtPlugin((nuxtApp) => {
5
+ export default defineNuxtPlugin(async (_nuxtApp) => {
6
6
  const config = useRuntimeConfig();
7
7
  const i18nConfig = config.public.i18nConfig;
8
8
  const schema = port === "443" ? "https" : "http";
9
9
  const defaultUrl = port === "80" || port === "443" ? `${schema}://${host}` : `${schema}://${host}:${port}`;
10
- nuxtApp.hook("app:rendered", (_context) => {
11
- const url = useRequestURL();
12
- const baseUrl = (i18nConfig.metaBaseUrl || url.origin || defaultUrl).toString();
13
- const head = useLocaleHead({
14
- addDirAttribute: true,
15
- identifierAttribute: "id",
16
- addSeoAttributes: true,
17
- baseUrl
18
- });
19
- useHead(head);
10
+ const url = useRequestURL();
11
+ const baseUrl = (i18nConfig.metaBaseUrl || url.origin || defaultUrl).toString();
12
+ const head = await useLocaleHead({
13
+ addDirAttribute: true,
14
+ identifierAttribute: "id",
15
+ addSeoAttributes: true,
16
+ baseUrl
20
17
  });
18
+ useHead(head);
21
19
  });
@@ -1,10 +1,31 @@
1
1
  import { isNoPrefixStrategy, isPrefixStrategy } from "nuxt-i18n-micro-core";
2
- import { defineNuxtPlugin, useRuntimeConfig, useRoute, useRouter, navigateTo } from "#imports";
2
+ import { defineNuxtPlugin, useRuntimeConfig, useRoute, useRouter, navigateTo, createError } from "#imports";
3
3
  export default defineNuxtPlugin(async (nuxtApp) => {
4
4
  const config = useRuntimeConfig();
5
5
  const i18nConfig = config.public.i18nConfig;
6
+ const { routeLocales } = useRuntimeConfig().public.i18nConfig;
6
7
  const route = useRoute();
7
8
  const router = useRouter();
9
+ const checkRouteLocales = (to) => {
10
+ const routePath = to.path;
11
+ const routeName = to.name?.toString();
12
+ const normalizedRouteName = routeName?.replace("localized-", "");
13
+ const normalizedRoutePath = normalizedRouteName ? `/${normalizedRouteName}` : void 0;
14
+ const allowedLocales = routeName && routeLocales?.[routeName] || normalizedRouteName && routeLocales?.[normalizedRouteName] || normalizedRoutePath && routeLocales?.[normalizedRoutePath] || routeLocales?.[routePath];
15
+ if (!allowedLocales || allowedLocales.length === 0) {
16
+ return;
17
+ }
18
+ const pathSegments = routePath.split("/").filter(Boolean);
19
+ const firstSegment = pathSegments[0];
20
+ const allLocales = i18nConfig.locales?.map((l) => l.code) || [];
21
+ if (firstSegment && allLocales.includes(firstSegment) && !allowedLocales.includes(firstSegment)) {
22
+ console.log("Locale not allowed, throwing 404");
23
+ throw createError({
24
+ statusCode: 404,
25
+ statusMessage: "Page Not Found"
26
+ });
27
+ }
28
+ };
8
29
  const handleRedirect = async (to) => {
9
30
  const currentLocale = nuxtApp.$getLocale().toString();
10
31
  const name = to.name?.toString();
@@ -26,10 +47,16 @@ export default defineNuxtPlugin(async (nuxtApp) => {
26
47
  });
27
48
  }
28
49
  };
29
- if (import.meta.server && (isPrefixStrategy(i18nConfig.strategy) || isNoPrefixStrategy(i18nConfig.strategy))) {
30
- await handleRedirect(route);
50
+ if (import.meta.server) {
51
+ checkRouteLocales(route);
52
+ if (isPrefixStrategy(i18nConfig.strategy) || isNoPrefixStrategy(i18nConfig.strategy)) {
53
+ await handleRedirect(route);
54
+ }
31
55
  }
32
56
  router.beforeEach(async (to, from, next) => {
57
+ if (from.path !== to.path) {
58
+ checkRouteLocales(to);
59
+ }
33
60
  if (isPrefixStrategy(i18nConfig.strategy) || isNoPrefixStrategy(i18nConfig.strategy)) {
34
61
  await handleRedirect(to);
35
62
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Checks if a path should be excluded from i18n routing
3
+ * @param path - The path to check
4
+ * @param excludePatterns - Optional custom exclusion patterns
5
+ * @returns true if the path should be excluded
6
+ */
7
+ export declare const isInternalPath: (path: string, excludePatterns?: (string | RegExp | object)[]) => boolean;
@@ -0,0 +1,40 @@
1
+ const DEFAULT_STATIC_PATTERNS = [
2
+ /^\/sitemap.*\.xml$/,
3
+ /^\/sitemap\.xml$/,
4
+ /^\/robots\.txt$/,
5
+ /^\/favicon\.ico$/,
6
+ /^\/apple-touch-icon.*\.png$/,
7
+ /^\/manifest\.json$/,
8
+ /^\/sw\.js$/,
9
+ /^\/workbox-.*\.js$/,
10
+ /\.(xml|txt|ico|json|js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/
11
+ ];
12
+ export const isInternalPath = (path, excludePatterns) => {
13
+ if (/(?:^|\/)__[^/]+/.test(path)) {
14
+ return true;
15
+ }
16
+ for (const pattern of DEFAULT_STATIC_PATTERNS) {
17
+ if (pattern.test(path)) {
18
+ return true;
19
+ }
20
+ }
21
+ if (excludePatterns) {
22
+ for (const pattern of excludePatterns) {
23
+ if (typeof pattern === "string") {
24
+ if (pattern.includes("*") || pattern.includes("?")) {
25
+ const regex = new RegExp(pattern.replace(/\*/g, ".*").replace(/\?/g, "."));
26
+ if (regex.test(path)) {
27
+ return true;
28
+ }
29
+ } else if (path === pattern || path.startsWith(pattern)) {
30
+ return true;
31
+ }
32
+ } else if (pattern instanceof RegExp) {
33
+ if (pattern.test(path)) {
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return false;
40
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
- "version": "1.96.0",
3
+ "version": "1.98.0",
4
4
  "description": "Nuxt I18n Micro is a lightweight, high-performance internationalization module for Nuxt, designed to handle multi-language support with minimal overhead, fast build times, and efficient runtime performance.",
5
5
  "repository": "s00d/nuxt-i18n-micro",
6
6
  "license": "MIT",
@@ -62,8 +62,8 @@
62
62
  "sirv": "^2.0.4",
63
63
  "ufo": "^1.5.4",
64
64
  "nuxt-i18n-micro-core": "1.0.18",
65
- "nuxt-i18n-micro-test-utils": "1.0.6",
66
- "nuxt-i18n-micro-types": "1.0.6"
65
+ "nuxt-i18n-micro-types": "1.0.8",
66
+ "nuxt-i18n-micro-test-utils": "1.0.6"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@nuxt/devtools": "^2.6.3",
@@ -1 +0,0 @@
1
- {"id":"2bd7945f-6cbd-45d4-9b72-30c2c2aeb875","timestamp":1758176680760,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}