nuxt-i18n-micro 1.102.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import path from "node:path";
2
+ import { watch } from "chokidar";
3
+ import { useStorage, useRuntimeConfig, defineNitroPlugin } from "#imports";
4
+ let watcherInstance = null;
5
+ export default defineNitroPlugin((nitroApp) => {
6
+ if (watcherInstance) {
7
+ return;
8
+ }
9
+ const config = useRuntimeConfig();
10
+ const i18nConfig = config.i18nConfig;
11
+ if (!i18nConfig || i18nConfig.disableUpdater || process.env.NODE_ENV !== "development") {
12
+ return;
13
+ }
14
+ const log = (...args) => i18nConfig.debug && console.log("[i18n-hmr]", ...args);
15
+ const warn = (...args) => i18nConfig.debug && console.warn("[i18n-hmr]", ...args);
16
+ const translationsRoot = path.resolve(i18nConfig.rootDir, i18nConfig.translationDir);
17
+ log(`Watching for translation changes in: ${translationsRoot}`);
18
+ const storage = useStorage("assets:server");
19
+ const invalidateCache = async (filePath) => {
20
+ if (!filePath.endsWith(".json")) return;
21
+ const relativePath = path.relative(translationsRoot, filePath).replace(/\\/g, "/");
22
+ const isPageTranslation = relativePath.startsWith("pages/");
23
+ try {
24
+ const allKeys = await storage.getKeys("_locales:merged:");
25
+ if (isPageTranslation) {
26
+ const match = relativePath.match(/^pages\/([^/]+)\/(.+)\.json$/);
27
+ if (!match) return;
28
+ const pageName = match[1];
29
+ const locale = match[2];
30
+ const keyToRemove = `_locales:merged:${pageName}:${locale}`;
31
+ if (allKeys.includes(keyToRemove)) {
32
+ await storage.removeItem(keyToRemove);
33
+ log(`Invalidated page cache: ${keyToRemove}`);
34
+ }
35
+ } else {
36
+ const match = relativePath.match(/^([^/]+)\.json$/);
37
+ if (!match) return;
38
+ const locale = match[1];
39
+ const keysToRemove = allKeys.filter((key) => key.endsWith(`:${locale}`));
40
+ if (keysToRemove.length > 0) {
41
+ await Promise.all(keysToRemove.map((key) => storage.removeItem(key)));
42
+ log(`Invalidated ALL page caches for locale '${locale}'. Removed ${keysToRemove.length} entries.`);
43
+ }
44
+ }
45
+ } catch (e) {
46
+ warn("Failed to invalidate server cache for", filePath, e);
47
+ }
48
+ };
49
+ const watcher = watch(translationsRoot, { persistent: true, ignoreInitial: true, depth: 5 });
50
+ watcher.on("add", invalidateCache);
51
+ watcher.on("change", invalidateCache);
52
+ watcher.on("unlink", invalidateCache);
53
+ watcherInstance = watcher;
54
+ nitroApp.hooks.hook("close", async () => {
55
+ if (watcherInstance) {
56
+ await watcherInstance.close();
57
+ watcherInstance = null;
58
+ }
59
+ });
60
+ });
@@ -1,63 +1,88 @@
1
- import { resolve, join } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { readFile } from "node:fs/promises";
3
- import { defineEventHandler } from "h3";
3
+ import { defineEventHandler, setResponseHeader } from "h3";
4
4
  import { useRuntimeConfig, createError, useStorage } from "#imports";
5
5
  let storageInit = false;
6
6
  function deepMerge(target, source) {
7
+ const output = { ...target };
7
8
  for (const key in source) {
8
9
  if (key === "__proto__" || key === "constructor") continue;
9
- if (Array.isArray(source[key])) {
10
- target[key] = source[key];
11
- } else if (source[key] instanceof Object) {
12
- target[key] = target[key] instanceof Object ? deepMerge(target[key], source[key]) : source[key];
10
+ const src = source[key];
11
+ const dst = output[key];
12
+ if (src && typeof src === "object" && !Array.isArray(src) && dst && typeof dst === "object" && !Array.isArray(dst)) {
13
+ output[key] = deepMerge(dst, src);
13
14
  } else {
14
- target[key] = source[key];
15
+ output[key] = src;
15
16
  }
16
17
  }
17
- return target;
18
+ return output;
19
+ }
20
+ async function readTranslationFile(filePath, debug) {
21
+ try {
22
+ const content = await readFile(filePath, "utf-8");
23
+ return JSON.parse(content);
24
+ } catch (e) {
25
+ if (debug && e?.code !== "ENOENT") {
26
+ console.error(`[i18n] Error loading locale file: ${filePath}`, e);
27
+ }
28
+ return null;
29
+ }
18
30
  }
19
31
  export default defineEventHandler(async (event) => {
32
+ setResponseHeader(event, "Content-Type", "application/json");
20
33
  const { page, locale } = event.context.params;
21
34
  const config = useRuntimeConfig();
22
- const { rootDirs, debug, translationDir, fallbackLocale, customRegexMatcher } = config.i18nConfig;
35
+ const { rootDirs, debug, translationDir, fallbackLocale, routesLocaleLinks } = config.i18nConfig;
23
36
  const { locales } = config.public.i18nConfig;
24
- if (customRegexMatcher && locales && !locales.map((l) => l.code).includes(locale)) {
37
+ if (locales && !locales.map((l) => l.code).includes(locale)) {
25
38
  throw createError({ statusCode: 404 });
26
39
  }
27
- const currentLocaleConfig = locales?.find((l) => l.code === locale) ?? null;
28
- const getTranslationPath = (locale2, page2) => page2 === "general" ? `${locale2}.json` : `pages/${page2}/${locale2}.json`;
29
- let translations = {};
40
+ let fileLookupPage = page;
41
+ if (routesLocaleLinks && routesLocaleLinks[page]) {
42
+ fileLookupPage = routesLocaleLinks[page];
43
+ if (debug) {
44
+ console.log(`[i18n] Route link found: '${page}' -> '${fileLookupPage}'. Using linked translations.`);
45
+ }
46
+ }
30
47
  const serverStorage = useStorage("assets:server");
48
+ const cacheKey = `_locales:merged:${page}:${locale}`;
31
49
  if (!storageInit) {
32
50
  if (debug) console.log("[nuxt-i18n-micro] clear storage cache");
33
51
  await Promise.all((await serverStorage.getKeys("_locales")).map((key) => serverStorage.removeItem(key)));
34
52
  storageInit = true;
35
53
  }
36
- const cacheName = join("_locales", getTranslationPath(locale, page));
37
- const isThereAsset = await serverStorage.hasItem(cacheName);
38
- if (isThereAsset) {
39
- const rawContent = await serverStorage.getItemRaw(cacheName) ?? {};
40
- return typeof rawContent === "string" ? JSON.parse(rawContent) : rawContent;
54
+ const cachedMerged = await serverStorage.getItem(cacheKey);
55
+ if (cachedMerged) {
56
+ return cachedMerged;
41
57
  }
42
- const createPaths = (locale2) => rootDirs.map((dir) => ({
43
- translationPath: resolve(dir, translationDir, getTranslationPath(locale2, page)),
44
- name: `_locales/${getTranslationPath(locale2, page)}`
45
- }));
46
- const paths = [
47
- ...fallbackLocale && fallbackLocale !== locale ? createPaths(fallbackLocale) : [],
48
- ...currentLocaleConfig && currentLocaleConfig.fallbackLocale ? createPaths(currentLocaleConfig.fallbackLocale) : [],
49
- ...createPaths(locale)
50
- ];
51
- for (const { translationPath, name } of paths) {
52
- try {
53
- if (debug) console.log("[nuxt-i18n-micro] load locale", translationPath, name);
54
- const content = await readFile(translationPath, "utf-8");
55
- const fileContent = JSON.parse(content);
56
- translations = deepMerge(translations, fileContent);
57
- } catch (e) {
58
- if (debug) console.error("[nuxt-i18n-micro] load locale error", e);
58
+ const getPathsFor = (targetLocale, targetPage) => rootDirs.map((dir) => resolve(dir, translationDir, targetPage === "general" ? `${targetLocale}.json` : `pages/${targetPage}/${targetLocale}.json`));
59
+ let finalTranslations = {};
60
+ const currentLocaleConfig = locales?.find((l) => l.code === locale) ?? null;
61
+ const loadAndMerge = async (targetLocale) => {
62
+ let globalTranslations = {};
63
+ let pageTranslations = {};
64
+ for (const p of getPathsFor(targetLocale, "general")) {
65
+ const content = await readTranslationFile(p, debug);
66
+ if (content) globalTranslations = deepMerge(globalTranslations, content);
67
+ }
68
+ if (page !== "general") {
69
+ for (const p of getPathsFor(targetLocale, fileLookupPage)) {
70
+ const content = await readTranslationFile(p, debug);
71
+ if (content) pageTranslations = deepMerge(pageTranslations, content);
72
+ }
59
73
  }
74
+ return deepMerge(globalTranslations, pageTranslations);
75
+ };
76
+ const fallbackLocalesList = [
77
+ fallbackLocale,
78
+ currentLocaleConfig?.fallbackLocale
79
+ ].filter((l) => !!l && l !== locale);
80
+ for (const fb of [...new Set(fallbackLocalesList)]) {
81
+ const fbTranslations = await loadAndMerge(fb);
82
+ finalTranslations = deepMerge(finalTranslations, fbTranslations);
60
83
  }
61
- await serverStorage.setItem(cacheName, translations);
62
- return translations;
84
+ const mainTranslations = await loadAndMerge(locale);
85
+ finalTranslations = deepMerge(finalTranslations, mainTranslations);
86
+ await serverStorage.setItem(cacheKey, finalTranslations);
87
+ return finalTranslations;
63
88
  });
@@ -1,6 +1,7 @@
1
1
  import { interpolate, useTranslationHelper } from "nuxt-i18n-micro-core";
2
2
  import { detectCurrentLocale } from "./utils/locale-detector.js";
3
3
  import { useRuntimeConfig } from "#imports";
4
+ const I18N_CONTEXT_KEY = "__i18n_cache__";
4
5
  async function fetchTranslations(locale) {
5
6
  try {
6
7
  const translations = await $fetch(`/_locales/general/${locale}/data.json`);
@@ -11,7 +12,16 @@ async function fetchTranslations(locale) {
11
12
  }
12
13
  }
13
14
  export const useTranslationServerMiddleware = async (event, defaultLocale, currentLocale) => {
14
- const { getTranslation, loadTranslations, hasGeneralTranslation } = useTranslationHelper();
15
+ if (!event.context[I18N_CONTEXT_KEY]) {
16
+ event.context[I18N_CONTEXT_KEY] = {
17
+ generalLocaleCache: {},
18
+ routeLocaleCache: {},
19
+ dynamicTranslationsCaches: [],
20
+ serverTranslationCache: {}
21
+ };
22
+ }
23
+ const requestScopedCache = event.context[I18N_CONTEXT_KEY];
24
+ const { getTranslation, loadTranslations, hasGeneralTranslation } = useTranslationHelper(requestScopedCache);
15
25
  const config = useRuntimeConfig(event).i18nConfig;
16
26
  const locale = currentLocale || detectCurrentLocale(event, config, defaultLocale);
17
27
  if (!hasGeneralTranslation(locale)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
- "version": "1.102.0",
3
+ "version": "2.0.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",
@@ -61,9 +61,9 @@
61
61
  "globby": "^14.1.0",
62
62
  "sirv": "^2.0.4",
63
63
  "ufo": "^1.5.4",
64
- "nuxt-i18n-micro-types": "1.0.10",
65
- "nuxt-i18n-micro-core": "1.0.18",
66
- "nuxt-i18n-micro-test-utils": "1.0.6"
64
+ "nuxt-i18n-micro-test-utils": "1.0.7",
65
+ "nuxt-i18n-micro-types": "1.0.11",
66
+ "nuxt-i18n-micro-core": "1.0.20"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@nuxt/devtools": "^2.6.3",
@@ -1 +0,0 @@
1
- {"id":"3b71a52e-40e0-4913-a13a-e722e382d06d","timestamp":1760083211792,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
@@ -1 +0,0 @@
1
- .spotlight[data-v-ed876d78]{background:linear-gradient(45deg,#00dc82,#36e4da 50%,#0047e1);bottom:-30vh;filter:blur(20vh);height:40vh}.gradient-border[data-v-ed876d78]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:.5rem;position:relative}@media (prefers-color-scheme:light){.gradient-border[data-v-ed876d78]{background-color:#ffffff4d}.gradient-border[data-v-ed876d78]:before{background:linear-gradient(90deg,#e2e2e2,#e2e2e2 25%,#00dc82,#36e4da 75%,#0047e1)}}@media (prefers-color-scheme:dark){.gradient-border[data-v-ed876d78]{background-color:#1414144d}.gradient-border[data-v-ed876d78]:before{background:linear-gradient(90deg,#303030,#303030 25%,#00dc82,#36e4da 75%,#0047e1)}}.gradient-border[data-v-ed876d78]:before{background-size:400% auto;border-radius:.5rem;content:"";inset:0;-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:.5;padding:2px;position:absolute;transition:background-position .3s ease-in-out,opacity .2s ease-in-out;width:100%}.gradient-border[data-v-ed876d78]:hover:before{background-position:-50% 0;opacity:1}.fixed[data-v-ed876d78]{position:fixed}.left-0[data-v-ed876d78]{left:0}.right-0[data-v-ed876d78]{right:0}.z-10[data-v-ed876d78]{z-index:10}.z-20[data-v-ed876d78]{z-index:20}.grid[data-v-ed876d78]{display:grid}.mb-16[data-v-ed876d78]{margin-bottom:4rem}.mb-8[data-v-ed876d78]{margin-bottom:2rem}.max-w-520px[data-v-ed876d78]{max-width:520px}.min-h-screen[data-v-ed876d78]{min-height:100vh}.w-full[data-v-ed876d78]{width:100%}.flex[data-v-ed876d78]{display:flex}.cursor-pointer[data-v-ed876d78]{cursor:pointer}.place-content-center[data-v-ed876d78]{place-content:center}.items-center[data-v-ed876d78]{align-items:center}.justify-center[data-v-ed876d78]{justify-content:center}.overflow-hidden[data-v-ed876d78]{overflow:hidden}.bg-white[data-v-ed876d78]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-4[data-v-ed876d78]{padding-left:1rem;padding-right:1rem}.px-8[data-v-ed876d78]{padding-left:2rem;padding-right:2rem}.py-2[data-v-ed876d78]{padding-bottom:.5rem;padding-top:.5rem}.text-center[data-v-ed876d78]{text-align:center}.text-8xl[data-v-ed876d78]{font-size:6rem;line-height:1}.text-xl[data-v-ed876d78]{font-size:1.25rem;line-height:1.75rem}.text-black[data-v-ed876d78]{--un-text-opacity:1;color:rgb(0 0 0/var(--un-text-opacity))}.font-light[data-v-ed876d78]{font-weight:300}.font-medium[data-v-ed876d78]{font-weight:500}.leading-tight[data-v-ed876d78]{line-height:1.25}.font-sans[data-v-ed876d78]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.antialiased[data-v-ed876d78]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (prefers-color-scheme:dark){.dark\:bg-black[data-v-ed876d78]{--un-bg-opacity:1;background-color:rgb(0 0 0/var(--un-bg-opacity))}.dark\:text-white[data-v-ed876d78]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media (min-width:640px){.sm\:px-0[data-v-ed876d78]{padding-left:0;padding-right:0}.sm\:px-6[data-v-ed876d78]{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-3[data-v-ed876d78]{padding-bottom:.75rem;padding-top:.75rem}.sm\:text-4xl[data-v-ed876d78]{font-size:2.25rem;line-height:2.5rem}.sm\:text-xl[data-v-ed876d78]{font-size:1.25rem;line-height:1.75rem}}
@@ -1 +0,0 @@
1
- .spotlight[data-v-09b9ec2d]{background:linear-gradient(45deg,#00dc82,#36e4da 50%,#0047e1);filter:blur(20vh)}.fixed[data-v-09b9ec2d]{position:fixed}.-bottom-1\/2[data-v-09b9ec2d]{bottom:-50%}.left-0[data-v-09b9ec2d]{left:0}.right-0[data-v-09b9ec2d]{right:0}.grid[data-v-09b9ec2d]{display:grid}.mb-16[data-v-09b9ec2d]{margin-bottom:4rem}.mb-8[data-v-09b9ec2d]{margin-bottom:2rem}.h-1\/2[data-v-09b9ec2d]{height:50%}.max-w-520px[data-v-09b9ec2d]{max-width:520px}.min-h-screen[data-v-09b9ec2d]{min-height:100vh}.place-content-center[data-v-09b9ec2d]{place-content:center}.overflow-hidden[data-v-09b9ec2d]{overflow:hidden}.bg-white[data-v-09b9ec2d]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-8[data-v-09b9ec2d]{padding-left:2rem;padding-right:2rem}.text-center[data-v-09b9ec2d]{text-align:center}.text-8xl[data-v-09b9ec2d]{font-size:6rem;line-height:1}.text-xl[data-v-09b9ec2d]{font-size:1.25rem;line-height:1.75rem}.text-black[data-v-09b9ec2d]{--un-text-opacity:1;color:rgb(0 0 0/var(--un-text-opacity))}.font-light[data-v-09b9ec2d]{font-weight:300}.font-medium[data-v-09b9ec2d]{font-weight:500}.leading-tight[data-v-09b9ec2d]{line-height:1.25}.font-sans[data-v-09b9ec2d]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.antialiased[data-v-09b9ec2d]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (prefers-color-scheme:dark){.dark\:bg-black[data-v-09b9ec2d]{--un-bg-opacity:1;background-color:rgb(0 0 0/var(--un-bg-opacity))}.dark\:text-white[data-v-09b9ec2d]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media (min-width:640px){.sm\:px-0[data-v-09b9ec2d]{padding-left:0;padding-right:0}.sm\:text-4xl[data-v-09b9ec2d]{font-size:2.25rem;line-height:2.5rem}}