UNPKG

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
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 };