nuxt-i18n-micro
Version: 
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.
1,092 lines (1,078 loc) • 44.4 kB
JavaScript
import path, { resolve, join } from 'node:path';
import * as fs from 'node:fs';
import fs__default, { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
import { useNuxt, defineNuxtModule, useLogger, createResolver, addTemplate, addImportsDir, addPlugin, addServerHandler, addComponentsDir, addTypeTemplate, addPrerenderRoutes } from '@nuxt/kit';
import { watch } from 'chokidar';
import { isPrefixAndDefaultStrategy, isPrefixStrategy, isNoPrefixStrategy, isPrefixExceptDefaultStrategy, withPrefixStrategy } from 'nuxt-i18n-micro-core';
import { fileURLToPath } from 'node:url';
import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
import sirv from 'sirv';
import { isInternalPath } from '../dist/runtime/utils/path-utils.js';
import { globby } from 'globby';
const DEVTOOLS_UI_PORT = 3030;
const DEVTOOLS_UI_ROUTE = "/__nuxt-i18n-micro";
const distDir = resolve(fileURLToPath(import.meta.url), "..");
const clientDir = resolve(distDir, "client");
function setupDevToolsUI(options, resolve2) {
  const nuxt = useNuxt();
  const clientPath = resolve2("./client");
  const clientDirExists = fs.existsSync(clientPath);
  const ROUTE_PATH = `${nuxt.options.app.baseURL || "/"}/__nuxt-i18n-micro`.replace(/\/+/g, "/");
  const ROUTE_CLIENT = `${ROUTE_PATH}/client`;
  if (clientDirExists) {
    nuxt.hook("vite:serverCreated", (server) => {
      const indexHtmlPath = path.join(clientDir, "index.html");
      if (!fs.existsSync(indexHtmlPath)) {
        return;
      }
      const indexContent = fs.readFileSync(indexHtmlPath);
      const handleStatic = sirv(clientDir, {
        dev: true,
        single: false
      });
      const handleIndex = async (res) => {
        res.setHeader("Content-Type", "text/html");
        res.statusCode = 200;
        res.write((await indexContent).toString().replace(/\/__NUXT_DEVTOOLS_I18N_BASE__\//g, `${ROUTE_CLIENT}/`));
        res.end();
      };
      server.middlewares.use(ROUTE_CLIENT, (req, res) => {
        if (req.url === "/")
          return handleIndex(res);
        return handleStatic(req, res, () => handleIndex(res));
      });
    });
  } else {
    nuxt.hook("vite:extendConfig", (config) => {
      config.server = config.server || {};
      config.server.proxy = config.server.proxy || {};
      config.server.proxy[DEVTOOLS_UI_ROUTE] = {
        target: `http://localhost:${DEVTOOLS_UI_PORT}${DEVTOOLS_UI_ROUTE}`,
        changeOrigin: true,
        followRedirects: true,
        rewrite: (path2) => path2.replace(DEVTOOLS_UI_ROUTE, "")
      };
    });
  }
  onDevToolsInitialized(async () => {
    extendServerRpc("nuxt-i18n-micro", {
      async saveTranslationContent(file, content) {
        const filePath = path.resolve(file);
        if (fs.existsSync(filePath)) {
          fs.writeFileSync(filePath, JSON.stringify(content, null, 2), "utf-8");
        } else {
          throw new Error(`File not found: ${filePath}`);
        }
      },
      async getConfigs() {
        return Promise.resolve(options);
      },
      async getLocalesAndTranslations() {
        const rootDirs = nuxt.options.runtimeConfig.i18nConfig?.rootDirs || [nuxt.options.rootDir];
        const filesList = {};
        for (const rootDir of rootDirs) {
          const localesDir = path.join(rootDir, options.translationDir || "locales");
          const pagesDir = path.join(localesDir, "pages");
          const processDirectory = (dir) => {
            if (!fs.existsSync(dir)) return;
            fs.readdirSync(dir).forEach((file) => {
              const filePath = path.join(dir, file);
              const stat = fs.lstatSync(filePath);
              if (stat.isDirectory()) {
                processDirectory(filePath);
              } else if (file.endsWith(".json")) {
                try {
                  filesList[filePath] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
                } catch (e) {
                  console.error(`Error parsing locale file ${filePath}:`, e);
                }
              }
            });
          };
          processDirectory(localesDir);
          processDirectory(pagesDir);
        }
        return filesList;
      }
    });
    nuxt.hook("devtools:customTabs", (tabs) => {
      tabs.push({
        name: "nuxt-i18n-micro",
        title: "i18n Micro",
        icon: "carbon:language",
        view: {
          type: "iframe",
          src: ROUTE_CLIENT
        }
      });
    });
  });
}
function extractScriptContent(content) {
  const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
  return scriptMatch ? scriptMatch[1] : null;
}
function removeTypeScriptTypes(scriptContent) {
  return scriptContent.replace(/\((\w+):[^)]*\)/g, "($1)");
}
function findDefineI18nRouteConfig(scriptContent) {
  try {
    const defineStart = scriptContent.indexOf("$defineI18nRoute(");
    if (defineStart === -1) {
      return null;
    }
    const openParen = scriptContent.indexOf("(", defineStart);
    if (openParen === -1) {
      return null;
    }
    let braceCount = 0;
    let parenCount = 1;
    let i = openParen + 1;
    for (; i < scriptContent.length; i++) {
      if (scriptContent[i] === "{") braceCount++;
      if (scriptContent[i] === "}") braceCount--;
      if (scriptContent[i] === "(") parenCount++;
      if (scriptContent[i] === ")") {
        parenCount--;
        if (parenCount === 0 && braceCount === 0) break;
      }
    }
    if (i >= scriptContent.length) {
      return null;
    }
    const configStr = scriptContent.substring(openParen + 1, i);
    try {
      const cleanConfigStr = removeTypeScriptTypes(configStr);
      try {
        const configObject = Function('"use strict";return (' + cleanConfigStr + ")")();
        try {
          const serialized = JSON.stringify(configObject);
          return JSON.parse(serialized);
        } catch {
          return configObject;
        }
      } catch {
        const scriptWithoutImports = scriptContent.split("\n").filter((line) => !line.trim().startsWith("import ")).join("\n");
        const cleanScript = removeTypeScriptTypes(scriptWithoutImports);
        const safeScript = `
          // Mock $defineI18nRoute to prevent errors
          const $defineI18nRoute = () => {}
          const defineI18nRoute = () => {}
          // Mock process.env for conditional logic
          const process = { env: { NODE_ENV: 'development' } }
          // Execute the script content without imports and TypeScript types
          ${cleanScript}
          // Return the config object
          return (${cleanConfigStr})
        `;
        const configObject = Function('"use strict";' + safeScript)();
        try {
          const serialized = JSON.stringify(configObject);
          return JSON.parse(serialized);
        } catch {
          return configObject;
        }
      }
    } catch {
      return null;
    }
  } catch {
    return null;
  }
}
function extractDefineI18nRouteData(content, _filePath) {
  try {
    const scriptContent = extractScriptContent(content);
    if (!scriptContent) {
      return null;
    }
    const configObject = findDefineI18nRouteConfig(scriptContent);
    if (!configObject) {
      return null;
    }
    return configObject;
  } catch {
    return null;
  }
}
const normalizePath = (routePath) => {
  if (!routePath) {
    return "";
  }
  const normalized = path.posix.normalize(routePath).replace(/\/+$/, "");
  return normalized === "." ? "" : normalized;
};
const cloneArray = (array) => array.map((item) => ({ ...item }));
const isPageRedirectOnly = (page) => !!(page.redirect && !page.file);
const removeLeadingSlash = (routePath) => routePath.startsWith("/") ? routePath.slice(1) : routePath;
const buildRouteName = (baseName, localeCode, isCustom) => isCustom ? `localized-${baseName}-${localeCode}` : `localized-${baseName}`;
const shouldAddLocalePrefix = (locale, defaultLocale, addLocalePrefix, includeDefaultLocaleRoute) => addLocalePrefix && !(locale === defaultLocale.code && !includeDefaultLocaleRoute);
const isLocaleDefault = (locale, defaultLocale, includeDefaultLocaleRoute) => {
  const localeCode = typeof locale === "string" ? locale : locale.code;
  return localeCode === defaultLocale.code && !includeDefaultLocaleRoute;
};
const buildFullPath = (locale, basePath, customRegex) => {
  const regexString = normalizeRegex(customRegex?.toString());
  const localeParam = regexString ? regexString : Array.isArray(locale) ? locale.join("|") : locale;
  return normalizePath(path.posix.join("/", `:locale(${localeParam})`, basePath));
};
const buildFullPathNoPrefix = (basePath) => {
  return normalizePath(basePath);
};
const normalizeRegex = (toNorm) => {
  if (typeof toNorm === "undefined") return void 0;
  return toNorm.startsWith("/") && toNorm.endsWith("/") ? toNorm?.slice(1, -1) : toNorm;
};
const buildRouteNameFromRoute = (name, path2) => {
  return name ?? (path2 ?? "").replace(/[^a-z0-9]/gi, "-").replace(/^-+|-+$/g, "");
};
class PageManager {
  locales;
  defaultLocale;
  strategy;
  localizedPaths = {};
  activeLocaleCodes;
  globalLocaleRoutes;
  filesLocaleRoutes;
  routeLocales;
  noPrefixRedirect;
  excludePatterns;
  constructor(locales, defaultLocaleCode, strategy, globalLocaleRoutes, filesLocaleRoutes, routeLocales, noPrefixRedirect, excludePatterns) {
    this.locales = locales;
    this.defaultLocale = this.findLocaleByCode(defaultLocaleCode) || { code: defaultLocaleCode };
    this.strategy = strategy;
    this.noPrefixRedirect = noPrefixRedirect;
    this.excludePatterns = excludePatterns;
    this.activeLocaleCodes = this.computeActiveLocaleCodes();
    this.globalLocaleRoutes = globalLocaleRoutes || {};
    this.filesLocaleRoutes = filesLocaleRoutes || {};
    this.routeLocales = routeLocales || {};
  }
  findLocaleByCode(code) {
    return this.locales.find((locale) => locale.code === code);
  }
  computeActiveLocaleCodes() {
    return this.locales.filter((locale) => locale.code !== this.defaultLocale.code || isPrefixAndDefaultStrategy(this.strategy) || isPrefixStrategy(this.strategy)).map((locale) => locale.code);
  }
  getAllowedLocalesForPage(pagePath, pageName) {
    const allowedLocales = this.routeLocales[pagePath] || this.routeLocales[pageName];
    if (allowedLocales && allowedLocales.length > 0) {
      return allowedLocales.filter(
        (locale) => this.locales.some((l) => l.code === locale)
      );
    }
    return this.locales.map((locale) => locale.code);
  }
  hasLocaleRestrictions(pagePath, pageName) {
    return !!(this.routeLocales[pagePath] || this.routeLocales[pageName]);
  }
  // private isAlreadyLocalized(p: string) {
  //   const codes = this.locales.map(l => l.code).join('|') // en|de|ru…
  //   return p.startsWith('/:locale(') // dynamic prefix
  //     || new RegExp(`^/(${codes})(/|$)`).test(p) // static /de/…
  // }
  extendPages(pages, customRegex, isCloudflarePages) {
    this.localizedPaths = this.extractLocalizedPaths(pages);
    const additionalRoutes = [];
    for (const page of [...pages]) {
      if (page.path && isInternalPath(page.path, this.excludePatterns)) {
        continue;
      }
      if (!page.name && page.file?.endsWith(".vue")) {
        console.warn(`[nuxt-i18n-next] Page name is missing for the file: ${page.file}`);
      }
      const customRoute = this.globalLocaleRoutes[page.name ?? ""];
      if (customRoute === false) {
        continue;
      }
      if (typeof customRoute === "object" && customRoute !== null) {
        this.addCustomGlobalLocalizedRoutes(page, customRoute, additionalRoutes, customRegex);
      } else {
        this.localizePage(page, additionalRoutes, customRegex);
      }
    }
    if (isPrefixStrategy(this.strategy) && !isCloudflarePages) {
      for (let i = pages.length - 1; i >= 0; i--) {
        const page = pages[i];
        if (!page) continue;
        const pagePath = page.path ?? "";
        const pageName = page.name ?? "";
        if (isInternalPath(pagePath, this.excludePatterns)) continue;
        if (this.globalLocaleRoutes[pageName] === false) continue;
        if (!/^\/:locale/.test(pagePath) && pagePath !== "/") {
          pages.splice(i, 1);
        }
      }
    }
    pages.push(...additionalRoutes);
  }
  extractLocalizedPaths(pages, parentPath = "") {
    const localizedPaths = {};
    pages.forEach((page) => {
      const pageName = buildRouteNameFromRoute(page.name, page.path);
      const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
      const globalLocalePath = this.globalLocaleRoutes[normalizedFullPath] || this.globalLocaleRoutes[pageName];
      if (!globalLocalePath) {
        const filesLocalePath = this.filesLocaleRoutes[pageName];
        if (filesLocalePath && typeof filesLocalePath === "object") {
          localizedPaths[normalizedFullPath] = filesLocalePath;
        }
      } else if (typeof globalLocalePath === "object") {
        localizedPaths[normalizedFullPath] = globalLocalePath;
      }
      if (page.children?.length) {
        const parentFullPath = normalizePath(path.posix.join(parentPath, page.path));
        Object.assign(localizedPaths, this.extractLocalizedPaths(page.children, parentFullPath));
      }
    });
    return localizedPaths;
  }
  addCustomGlobalLocalizedRoutes(page, customRoutePaths, additionalRoutes, customRegex) {
    const normalizedFullPath = normalizePath(page.path);
    const pageName = buildRouteNameFromRoute(page.name, page.path);
    const allowedLocales = this.getAllowedLocalesForPage(normalizedFullPath, pageName);
    const hasRestrictions = this.hasLocaleRestrictions(normalizedFullPath, pageName);
    const localesToUse = hasRestrictions ? this.locales.filter((locale) => allowedLocales.includes(locale.code)) : this.locales;
    localesToUse.forEach((locale) => {
      const customPath = customRoutePaths[locale.code];
      const isDefaultLocale = isLocaleDefault(locale, this.defaultLocale, isPrefixStrategy(this.strategy) || isPrefixAndDefaultStrategy(this.strategy));
      if (customPath) {
        if (isNoPrefixStrategy(this.strategy)) {
          const newRoute = this.createLocalizedRoute(page, [locale.code], page.children ?? [], true, customPath, customRegex, false, locale.code);
          if (newRoute) {
            additionalRoutes.push(newRoute);
            if (this.noPrefixRedirect) page.redirect = newRoute.path;
          }
        } else {
          if (isDefaultLocale) {
            page.path = normalizePath(customPath);
          } else {
            const newRoute = this.createLocalizedRoute(page, [locale.code], page.children ?? [], true, customPath, customRegex, false, locale.code);
            if (newRoute) additionalRoutes.push(newRoute);
          }
        }
      } else {
        const localeCodes = [locale.code];
        const originalChildren = cloneArray(page.children ?? []);
        const newRoute = this.createLocalizedRoute(page, localeCodes, originalChildren, false, "", customRegex, false, locale.code);
        if (newRoute) {
          additionalRoutes.push(newRoute);
        }
      }
    });
  }
  localizePage(page, additionalRoutes, customRegex) {
    if (isPageRedirectOnly(page)) return;
    const originalChildren = cloneArray(page.children ?? []);
    const normalizedFullPath = normalizePath(page.path);
    const pageName = buildRouteNameFromRoute(page.name, page.path);
    const allowedLocales = this.getAllowedLocalesForPage(normalizedFullPath, pageName);
    const hasRestrictions = this.hasLocaleRestrictions(normalizedFullPath, pageName);
    const localeCodesWithoutCustomPaths = this.filterLocaleCodesWithoutCustomPaths(normalizedFullPath).filter((locale) => hasRestrictions ? allowedLocales.includes(locale) : true);
    if (localeCodesWithoutCustomPaths.length) {
      const newRoute = this.createLocalizedRoute(page, localeCodesWithoutCustomPaths, originalChildren, false, "", customRegex, false, true);
      if (newRoute) additionalRoutes.push(newRoute);
    }
    this.addCustomLocalizedRoutes(page, normalizedFullPath, originalChildren, additionalRoutes, hasRestrictions ? allowedLocales : void 0);
    this.adjustRouteForDefaultLocale(page, originalChildren);
    this.handleAliasRoutes(page, additionalRoutes, customRegex, hasRestrictions ? allowedLocales : void 0);
  }
  filterLocaleCodesWithoutCustomPaths(fullPath) {
    return this.activeLocaleCodes.filter((code) => !this.localizedPaths[fullPath]?.[code]);
  }
  handleAliasRoutes(page, additionalRoutes, customRegex, allowedLocales) {
    const aliasRoutes = page.alias || page.meta?.alias;
    if (!aliasRoutes || !Array.isArray(aliasRoutes)) {
      return;
    }
    const localesToUse = allowedLocales || this.activeLocaleCodes;
    aliasRoutes.forEach((aliasPath) => {
      const localizedAliasPath = buildFullPath(localesToUse, aliasPath, customRegex);
      const aliasRoute = {
        ...page,
        path: localizedAliasPath,
        name: `localized-${page.name ?? ""}`,
        meta: {
          ...page.meta,
          alias: void 0
          // Remove alias to prevent infinite recursion
        },
        alias: void 0
        // Remove alias from root to prevent infinite recursion
      };
      additionalRoutes.push(aliasRoute);
    });
  }
  adjustRouteForDefaultLocale(page, originalChildren) {
    if (isNoPrefixStrategy(this.strategy)) {
      return;
    }
    const defaultLocalePath = this.localizedPaths[page.path]?.[this.defaultLocale.code];
    if (defaultLocalePath) {
      page.path = normalizePath(defaultLocalePath);
    }
    if (originalChildren.length) {
      const newName = normalizePath(path.posix.join("/", buildRouteNameFromRoute(page.name, page.path)));
      const currentChildren = page.children ? [...page.children] : [];
      const localizedChildren = this.createLocalizedChildren(
        originalChildren,
        newName,
        [this.defaultLocale.code],
        true,
        false,
        false
      );
      const childrenMap = new Map(currentChildren.map((child) => [child.name, child]));
      localizedChildren.forEach((localizedChild) => {
        if (childrenMap.has(localizedChild.name)) {
          const existingChild = childrenMap.get(localizedChild.name);
          if (existingChild) {
            Object.assign(existingChild, localizedChild);
          }
        } else {
          currentChildren.push(localizedChild);
        }
      });
      page.children = currentChildren;
    }
  }
  addCustomLocalizedRoutes(page, fullPath, originalChildren, additionalRoutes, allowedLocales, customRegex) {
    const localesToUse = allowedLocales ? this.locales.filter((locale) => allowedLocales.includes(locale.code)) : this.locales;
    localesToUse.forEach((locale) => {
      const customPath = this.localizedPaths[fullPath]?.[locale.code];
      if (!customPath) return;
      const isDefaultLocale = isLocaleDefault(locale, this.defaultLocale, isPrefixStrategy(this.strategy) || isNoPrefixStrategy(this.strategy));
      if (isDefaultLocale && isPrefixExceptDefaultStrategy(this.strategy)) {
        page.path = normalizePath(customPath);
        page.children = this.createLocalizedChildren(originalChildren, "", [locale.code], false, false, false, { [locale.code]: customPath });
      } else {
        const newRoute = this.createLocalizedRoute(page, [locale.code], originalChildren, true, customPath, customRegex, false, locale.code);
        if (newRoute) {
          additionalRoutes.push(newRoute);
        }
      }
      if (isPrefixAndDefaultStrategy(this.strategy) && locale === this.defaultLocale) {
        const newRoute = this.createLocalizedRoute(page, [locale.code], originalChildren, true, customPath, customRegex, true, locale.code);
        if (newRoute) additionalRoutes.push(newRoute);
      }
    });
  }
  createLocalizedChildren(routes, parentPath, localeCodes, modifyName = true, addLocalePrefix = false, parentLocale = false, localizedParentPaths = {}) {
    return routes.flatMap(
      (route) => this.createLocalizedVariants(
        route,
        parentPath,
        localeCodes,
        modifyName,
        addLocalePrefix,
        parentLocale,
        localizedParentPaths
      )
    );
  }
  createLocalizedVariants(route, parentPath, localeCodes, modifyName, addLocalePrefix, parentLocale = false, localizedParentPaths) {
    const routePath = normalizePath(route.path);
    const fullPath = normalizePath(path.posix.join(parentPath, routePath));
    let customLocalePaths = this.localizedPaths[fullPath] ?? this.localizedPaths[normalizePath(route.path)];
    if (!customLocalePaths && Object.keys(localizedParentPaths).length > 0) {
      const hasLocalizedContext = Object.values(localizedParentPaths).some((path2) => path2 && path2 !== "");
      if (hasLocalizedContext) {
        const originalRoutePath = normalizePath(path.posix.join("/activity-locale", route.path));
        customLocalePaths = this.localizedPaths[originalRoutePath];
      }
    }
    const isCustomLocalized = !!customLocalePaths;
    const result = [];
    if (!isCustomLocalized) {
      const finalPathForRoute = removeLeadingSlash(routePath);
      const localizedChildren = this.createLocalizedChildren(
        cloneArray(route.children ?? []),
        path.posix.join(parentPath, routePath),
        localeCodes,
        modifyName,
        addLocalePrefix,
        parentLocale,
        localizedParentPaths
      );
      const newName = this.buildChildRouteName(route.name, parentLocale);
      result.push({
        ...route,
        name: newName,
        path: finalPathForRoute,
        children: localizedChildren
      });
      return result;
    }
    for (const locale of localeCodes) {
      const parentLocalizedPath = localizedParentPaths?.[locale];
      const hasParentLocalized = !!parentLocalizedPath;
      const customPath = customLocalePaths?.[locale];
      let basePath = customPath ? normalizePath(customPath) : normalizePath(route.path);
      if (hasParentLocalized && parentLocalizedPath) {
        if (customPath) {
          basePath = normalizePath(customPath);
        } else {
          basePath = normalizePath(path.posix.join(parentLocalizedPath, route.path));
        }
      }
      const finalRoutePath = shouldAddLocalePrefix(
        locale,
        this.defaultLocale,
        addLocalePrefix,
        isPrefixStrategy(this.strategy)
      ) ? buildFullPath(locale, basePath) : basePath;
      const finalPathForRoute = removeLeadingSlash(finalRoutePath);
      const nextParentPath = customPath ? normalizePath(customPath) : hasParentLocalized ? parentLocalizedPath : normalizePath(path.posix.join(parentPath, routePath));
      const localizedChildren = this.createLocalizedChildren(
        cloneArray(route.children ?? []),
        nextParentPath,
        [locale],
        modifyName,
        addLocalePrefix,
        locale,
        {
          ...localizedParentPaths,
          [locale]: nextParentPath
        }
      );
      const routeName = this.buildLocalizedRouteName(
        buildRouteNameFromRoute(route.name, route.path),
        locale,
        modifyName,
        !!customLocalePaths
      );
      result.push({
        ...route,
        name: routeName,
        path: finalPathForRoute,
        children: localizedChildren
      });
    }
    return result;
  }
  buildChildRouteName(baseName, parentLocale) {
    if (parentLocale === true) {
      return `localized-${baseName}`;
    }
    if (typeof parentLocale === "string") {
      return `localized-${baseName}-${parentLocale}`;
    }
    return baseName;
  }
  createLocalizedRoute(page, localeCodes, originalChildren, isCustom, customPath = "", customRegex, force = false, parentLocale = false) {
    const routePath = this.buildRoutePath(localeCodes, page.path, encodeURI(customPath), isCustom, customRegex, force);
    if (!routePath || routePath == page.path) return null;
    if (localeCodes.length === 0) return null;
    const firstLocale = localeCodes[0];
    if (!firstLocale) return null;
    const routeName = buildRouteName(buildRouteNameFromRoute(page.name ?? "", page.path ?? ""), firstLocale, isCustom);
    return {
      ...page,
      children: this.createLocalizedChildren(originalChildren, page.path, localeCodes, true, false, parentLocale, { [firstLocale]: customPath }),
      path: routePath,
      name: routeName
    };
  }
  buildLocalizedRouteName(baseName, locale, modifyName, forceLocaleSuffixOrCustom = false) {
    if (!modifyName) return baseName;
    if (forceLocaleSuffixOrCustom) {
      return `localized-${baseName}-${locale}`;
    }
    const shouldAddLocaleSuffix = locale && !isLocaleDefault(locale, this.defaultLocale, isPrefixAndDefaultStrategy(this.strategy));
    return shouldAddLocaleSuffix ? `localized-${baseName}-${locale}` : `localized-${baseName}`;
  }
  buildRoutePath(localeCodes, originalPath, customPath, isCustom, customRegex, force = false) {
    if (isNoPrefixStrategy(this.strategy)) {
      return buildFullPathNoPrefix(isCustom ? customPath : originalPath);
    }
    if (isCustom) {
      return force || isPrefixStrategy(this.strategy) || !localeCodes.includes(this.defaultLocale.code) ? buildFullPath(localeCodes, customPath, customRegex) : normalizePath(customPath);
    }
    return buildFullPath(localeCodes, originalPath, customRegex);
  }
}
class LocaleManager {
  locales;
  options;
  rootDirs;
  defaultLocale;
  constructor(options, rootDirs) {
    this.options = options;
    this.rootDirs = rootDirs;
    this.locales = this.mergeLocales(options.locales ?? []);
    this.defaultLocale = options.defaultLocale ?? "en";
  }
  mergeLocales(locales) {
    return locales.reduce((acc, locale) => {
      const existingLocale = acc.find((l) => l.code === locale.code);
      if (existingLocale) {
        Object.assign(existingLocale, locale);
      } else {
        acc.push(locale);
      }
      return acc;
    }, []).filter((locale) => !locale.disabled);
  }
  ensureTranslationFilesExist(pagesNames, translationDir, rootDir) {
    this.locales.forEach((locale) => {
      const globalFilePath = path.join(rootDir, translationDir, `${locale.code}.json`);
      this.ensureFileExists(globalFilePath);
      if (!this.options.disablePageLocales) {
        pagesNames.forEach((name) => {
          const pageFilePath = path.join(rootDir, translationDir, "pages", `${name}/${locale.code}.json`);
          this.ensureFileExists(pageFilePath);
        });
      }
    });
  }
  ensureFileExists(filePath) {
    const fileDir = path.dirname(filePath);
    if (!existsSync(fileDir)) {
      mkdirSync(fileDir, { recursive: true });
    }
    if (!existsSync(filePath)) {
      writeFileSync(filePath, JSON.stringify({}), "utf-8");
    }
  }
}
function generateI18nTypes() {
  return `
import type {PluginsInjections} from "nuxt-i18n-micro";
declare module 'vue/types/vue' {
  interface Vue extends PluginsInjections { }
}
declare module '@nuxt/types' {
  interface NuxtAppOptions extends PluginsInjections { }
  interface Context extends PluginsInjections { }
}
declare module '#app' {
  interface NuxtApp extends PluginsInjections { }
}
export {}`;
}
const module = defineNuxtModule({
  meta: {
    name: "nuxt-i18n-micro",
    configKey: "i18n"
  },
  // Default configuration options of the Nuxt module
  defaults: {
    locales: [],
    meta: true,
    debug: false,
    define: true,
    redirects: true,
    plugin: true,
    hooks: true,
    types: true,
    defaultLocale: "en",
    strategy: "prefix_except_default",
    translationDir: "locales",
    autoDetectPath: "/",
    autoDetectLanguage: true,
    disablePageLocales: false,
    disableWatcher: false,
    disableUpdater: false,
    noPrefixRedirect: false,
    includeDefaultLocaleRoute: void 0,
    fallbackLocale: void 0,
    localeCookie: "user-locale",
    apiBaseUrl: "_locales",
    routesLocaleLinks: {},
    globalLocaleRoutes: {},
    canonicalQueryWhitelist: ["page", "sort", "filter", "search", "q", "query", "tag"],
    plural: (key, count, params, _locale, getTranslation) => {
      const translation = getTranslation(key, params);
      if (!translation) {
        return null;
      }
      const forms = translation.toString().split("|");
      if (forms.length === 0) return null;
      const selectedForm = count < forms.length ? forms[count] : forms[forms.length - 1];
      if (!selectedForm) return null;
      return selectedForm.trim().replace("{count}", count.toString());
    },
    customRegexMatcher: void 0,
    excludePatterns: void 0
  },
  async setup(options, nuxt) {
    const defaultLocale = process.env.DEFAULT_LOCALE ?? options.defaultLocale ?? "en";
    const isSSG = nuxt.options.nitro.static ?? nuxt.options._generate ?? false;
    const isCloudflarePages = nuxt.options.nitro.preset?.startsWith("cloudflare");
    const logger = useLogger("nuxt-i18n-micro");
    if (options.includeDefaultLocaleRoute !== void 0) {
      logger.debug("The 'includeDefaultLocaleRoute' option is deprecated. Use 'strategy' instead.");
      if (options.includeDefaultLocaleRoute) {
        options.strategy = "prefix";
      } else {
        options.strategy = "prefix_except_default";
      }
    }
    const resolver = createResolver(import.meta.url);
    const rootDirs = nuxt.options._layers.map((layer) => layer.config.rootDir).reverse();
    const localeManager = new LocaleManager(options, rootDirs);
    const routeLocales = {};
    const globalLocaleRoutes = {};
    const routeDisableMeta = {};
    const pageFiles = await globby("pages/**/*.vue", { cwd: nuxt.options.rootDir });
    for (const pageFile of pageFiles) {
      const fullPath = join(nuxt.options.rootDir, pageFile);
      try {
        const fileContent = readFileSync(fullPath, "utf-8");
        const config = extractDefineI18nRouteData(fileContent, fullPath);
        if (!config) continue;
        const { locales: extractedLocales, localeRoutes, disableMeta } = config;
        const routePath = pageFile.replace(/^pages\//, "/").replace(/\/index\.vue$/, "").replace(/\.vue$/, "").replace(/\/$/, "") || "/";
        if (extractedLocales) {
          if (Array.isArray(extractedLocales)) {
            routeLocales[routePath] = extractedLocales;
          } else if (typeof extractedLocales === "object") {
            routeLocales[routePath] = Object.keys(extractedLocales);
          }
        }
        if (localeRoutes) {
          globalLocaleRoutes[routePath] = localeRoutes;
        }
        if (disableMeta !== void 0) {
          routeDisableMeta[routePath] = disableMeta;
        }
      } catch {
      }
    }
    const mergedGlobalLocaleRoutes = { ...options.globalLocaleRoutes, ...globalLocaleRoutes };
    const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy, mergedGlobalLocaleRoutes, globalLocaleRoutes, routeLocales, options.noPrefixRedirect, options.excludePatterns);
    addTemplate({
      filename: "i18n.plural.mjs",
      write: true,
      getContents: () => `export const plural = ${options.plural.toString()};`
    });
    const apiBaseUrl = (process.env.NUXT_I18N_APP_BASE_URL ?? options.apiBaseUrl ?? "_locales").replace(/^\/+|\/+$|\/{2,}/, "");
    nuxt.options.runtimeConfig.public.i18nConfig = {
      locales: localeManager.locales ?? [],
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      metaBaseUrl: options.metaBaseUrl ?? void 0,
      defaultLocale,
      localeCookie: options.localeCookie ?? "user-locale",
      autoDetectPath: options.autoDetectPath ?? "/",
      strategy: options.strategy ?? "prefix_except_default",
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      routesLocaleLinks: options.routesLocaleLinks ?? {},
      dateBuild: Date.now(),
      hashMode: nuxt.options?.router?.options?.hashMode ?? false,
      apiBaseUrl,
      isSSG,
      disablePageLocales: options.disablePageLocales ?? false,
      canonicalQueryWhitelist: options.canonicalQueryWhitelist ?? [],
      excludePatterns: options.excludePatterns ?? [],
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      routeLocales,
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      routeDisableMeta,
      experimental: {
        i18nPreviousPageFallback: options.experimental?.i18nPreviousPageFallback ?? false
      }
    };
    if (typeof options.customRegexMatcher !== "undefined") {
      const localeCodes = localeManager.locales.map((l) => l.code);
      if (!localeCodes.every((code) => code.match(options.customRegexMatcher))) {
        throw new Error("Nuxt-18n-micro: Some locale codes does not match customRegexMatcher");
      }
    }
    nuxt.options.runtimeConfig.i18nConfig = {
      rootDir: nuxt.options.rootDir,
      rootDirs,
      debug: options.debug ?? false,
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      fallbackLocale: options.fallbackLocale ?? void 0,
      translationDir: options.translationDir ?? "locales",
      customRegexMatcher: options.customRegexMatcher
    };
    addImportsDir(resolver.resolve("./runtime/composables"));
    if (process.env && process.env.TEST) {
      return;
    }
    if (options.plugin) {
      addPlugin({
        src: resolver.resolve("./runtime/plugins/01.plugin"),
        name: "i18n-plugin-loader",
        order: -5
      });
    }
    if (options.hooks) {
      addPlugin({
        src: resolver.resolve("./runtime/plugins/05.hooks"),
        name: "i18n-plugin-hooks",
        order: 1
      });
    }
    if (options.meta) {
      addPlugin({
        src: resolver.resolve("./runtime/plugins/02.meta"),
        name: "i18n-plugin-meta",
        order: 2
      });
    }
    if (options.define) {
      addPlugin({
        src: resolver.resolve("./runtime/plugins/03.define"),
        name: "i18n-plugin-define",
        mode: "all",
        order: 3
      });
    }
    if (options.autoDetectLanguage) {
      addPlugin({
        src: resolver.resolve("./runtime/plugins/04.auto-detect"),
        mode: "server",
        name: "i18n-plugin-auto-detect",
        order: 4
      });
    }
    if (options.redirects) {
      addPlugin({
        src: resolver.resolve("./runtime/plugins/06.redirect"),
        name: "i18n-plugin-redirect",
        mode: "all",
        order: 6
      });
    }
    addServerHandler({
      route: `/${apiBaseUrl}/:page/:locale/data.json`,
      handler: resolver.resolve("./runtime/server/routes/get")
    });
    addComponentsDir({
      path: resolver.resolve("./runtime/components"),
      pathPrefix: false,
      extensions: ["vue"]
    });
    if (options.types) {
      addTypeTemplate({
        filename: "types/i18n-plugin.d.ts",
        getContents: () => generateI18nTypes()
      });
    }
    nuxt.hook("pages:resolved", (pages) => {
      const prerenderRoutes = [];
      const routeRules = nuxt.options.routeRules || {};
      const pagesNames = pages.map((page) => page.name).filter((name) => name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[name]));
      localeManager.locales.forEach((locale) => {
        if (!options.disablePageLocales) {
          pagesNames.forEach((name) => {
            prerenderRoutes.push(`/${apiBaseUrl}/${name}/${locale.code}/data.json`);
          });
        }
        prerenderRoutes.push(`/${apiBaseUrl}/general/${locale.code}/data.json`);
      });
      if (!options.disableWatcher) {
        localeManager.ensureTranslationFilesExist(pagesNames, options.translationDir, nuxt.options.rootDir);
      }
      pageManager.extendPages(pages, options.customRegexMatcher, isCloudflarePages);
      if (isPrefixStrategy(options.strategy) && !isCloudflarePages) {
        const fallbackRoute = {
          path: "/:pathMatch(.*)*",
          name: "custom-fallback-route",
          file: resolver.resolve("./runtime/components/locale-redirect.vue"),
          meta: {
            globalLocaleRoutes: options.globalLocaleRoutes
          }
        };
        pages.push(fallbackRoute);
      }
      if (!isNoPrefixStrategy(options.strategy)) {
        if (isCloudflarePages) {
          const processPageWithChildren = (page, parentPath = "") => {
            if (!page.path) return;
            const fullPath = path.posix.normalize(`${parentPath}/${page.path}`);
            if (isInternalPath(fullPath, options.excludePatterns)) {
              return;
            }
            const routeRule = routeRules[fullPath];
            if (routeRule && routeRule.prerender === false) {
              return;
            }
            const localeSegmentMatch = fullPath.match(/:locale\(([^)]+)\)/);
            if (localeSegmentMatch && localeSegmentMatch[1]) {
              const availableLocales = localeSegmentMatch[1].split("|");
              localeManager.locales.forEach((locale) => {
                const localeCode = locale.code;
                if (availableLocales.includes(localeCode)) {
                  let localizedPath = fullPath;
                  localizedPath = localizedPath.replace(/:locale\([^)]+\)/, localeCode);
                  const localizedRouteRule = routeRules[localizedPath];
                  if (localizedRouteRule && localizedRouteRule.prerender === false) {
                    return;
                  }
                  if (!isInternalPath(localizedPath, options.excludePatterns)) {
                    prerenderRoutes.push(localizedPath);
                  }
                }
              });
            } else {
              if (!isInternalPath(fullPath, options.excludePatterns)) {
                prerenderRoutes.push(fullPath);
              }
            }
            if (page.children && page.children.length) {
              page.children.forEach((childPage) => processPageWithChildren(childPage, fullPath));
            }
          };
          pages.forEach((page) => {
            processPageWithChildren(page);
          });
        }
      }
      addPrerenderRoutes(prerenderRoutes);
    });
    nuxt.hook("nitro:config", (nitroConfig) => {
      if (nitroConfig.imports) {
        nitroConfig.imports.presets = nitroConfig.imports.presets || [];
        nitroConfig.imports.presets.push({
          from: resolver.resolve("./runtime/translation-server-middleware"),
          imports: ["useTranslationServerMiddleware"]
        });
        nitroConfig.imports.presets.push({
          from: resolver.resolve("./runtime/locale-server-middleware"),
          imports: ["useLocaleServerMiddleware"]
        });
      }
      const routeRules = nuxt.options.routeRules || {};
      const strategy = options.strategy;
      if (routeRules && Object.keys(routeRules).length && !isNoPrefixStrategy(strategy)) {
        nitroConfig.routeRules = nitroConfig.routeRules || {};
        for (const [originalPath, ruleValue] of Object.entries(routeRules)) {
          if (originalPath.startsWith("/api")) {
            continue;
          }
          localeManager.locales.forEach((localeObj) => {
            const localeCode = localeObj.code;
            const isDefaultLocale = localeCode === defaultLocale;
            const skip = (isPrefixExceptDefaultStrategy(strategy) || isPrefixAndDefaultStrategy(strategy)) && isDefaultLocale;
            if (skip) {
              return;
            }
            const suffix = originalPath === "/" ? "" : originalPath;
            const localizedPath = `/${localeCode}${suffix}`;
            const { redirect, ...restRuleValue } = ruleValue;
            if (!Object.keys(restRuleValue).length) {
              return;
            }
            nitroConfig.routeRules = nitroConfig.routeRules || {};
            nitroConfig.routeRules[localizedPath] = {
              ...nitroConfig.routeRules[localizedPath],
              ...restRuleValue
            };
            logger.debug(`Replicated routeRule for ${localizedPath}: ${JSON.stringify(restRuleValue)}`);
          });
        }
      }
      if (isNoPrefixStrategy(options.strategy)) {
        return;
      }
      const routes = nitroConfig.prerender?.routes || [];
      nitroConfig.prerender = nitroConfig.prerender || {};
      nitroConfig.prerender.routes = Array.isArray(nitroConfig.prerender.routes) ? nitroConfig.prerender.routes : [];
      const pages = nitroConfig.prerender.routes || [];
      localeManager.locales.forEach((locale) => {
        const shouldGenerate = locale.code !== defaultLocale || withPrefixStrategy(options.strategy);
        if (shouldGenerate) {
          pages.forEach((page) => {
            if (page && !/\.[a-z0-9]+$/i.test(page) && !isInternalPath(page)) {
              const localizedPage = `/${locale.code}${page}`;
              const routeRule = routeRules[page];
              if (routeRule && routeRule.prerender === false) {
                return;
              }
              const localizedRouteRule = routeRules[localizedPage];
              if (localizedRouteRule && localizedRouteRule.prerender === false) {
                return;
              }
              routes.push(localizedPage);
            }
          });
        }
      });
      nitroConfig.prerender = nitroConfig.prerender || {};
      nitroConfig.prerender.routes = routes;
    });
    nuxt.hook("nitro:build:public-assets", (nitro) => {
      const isProd = nuxt.options.dev === false;
      if (isProd) {
        const publicDir = path.join(nitro.options.output.publicDir ?? "./dist", options.translationDir ?? "locales");
        const translationDir = path.join(nuxt.options.rootDir, options.translationDir ?? "locales");
        try {
          fs__default.cpSync(translationDir, publicDir, { recursive: true });
          logger.log(`Translations copied successfully to ${translationDir} directory`);
        } catch (err) {
          logger.error("Error copying translations:", err);
        }
      }
    });
    if (!options.disableUpdater) {
      nuxt.hook("nitro:build:before", async (_nitro) => {
        const isProd = nuxt.options.dev === false;
        if (!isProd) {
          const translationPath = path.resolve(nuxt.options.rootDir, options.translationDir);
          logger.log("\u2139 add file watcher: " + translationPath);
          const watcherEvent = async (path2) => {
            await watcher.close();
            logger.log("\u21BB update store item: " + path2);
            nuxt.callHook("restart");
          };
          const watcher = watch(translationPath, { depth: 2, persistent: true }).on("change", watcherEvent);
          nuxt.hook("close", () => {
            watcher.close();
          });
        }
      });
    }
    nuxt.hook("prerender:routes", async (prerenderRoutes) => {
      if (isNoPrefixStrategy(options.strategy)) {
        return;
      }
      const routesSet = prerenderRoutes.routes;
      const routesToRemove = [];
      routesSet.forEach((route) => {
        if (isInternalPath(route, options.excludePatterns)) {
          routesToRemove.push(route);
        }
      });
      routesToRemove.forEach((route) => routesSet.delete(route));
      const additionalRoutes = /* @__PURE__ */ new Set();
      const routeRules = nuxt.options.routeRules || {};
      routesSet.forEach((route) => {
        if (!/\.[a-z0-9]+$/i.test(route) && !isInternalPath(route, options.excludePatterns)) {
          localeManager.locales.forEach((locale) => {
            const shouldGenerate = locale.code !== defaultLocale || withPrefixStrategy(options.strategy);
            if (shouldGenerate) {
              let localizedRoute;
              if (route === "/") {
                localizedRoute = `/${locale.code}`;
              } else {
                localizedRoute = `/${locale.code}${route}`;
              }
              const routeRule = routeRules[route];
              if (routeRule && routeRule.prerender === false) {
                return;
              }
              const localizedRouteRule = routeRules[localizedRoute];
              if (localizedRouteRule && localizedRouteRule.prerender === false) {
                return;
              }
              additionalRoutes.add(localizedRoute);
            }
          });
        }
      });
      additionalRoutes.forEach((route) => routesSet.add(route));
    });
    if (nuxt.options.dev) {
      setupDevToolsUI(options, resolver.resolve);
    }
  }
});
export { module as default };