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.

926 lines (915 loc) 37.5 kB
import path, { resolve } from 'node:path'; import * as fs from 'node:fs'; import fs__default, { readFileSync, existsSync, mkdirSync, writeFileSync } 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'; 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 } }); }); }); } const isInternalPath = (p) => /(?:^|\/)__[^/]+/.test(p); function extractLocaleRoutes(content, filePath) { const defineMatch = content.match(/\$?\bdefineI18nRoute\s*\(\s*\{[\s\S]*?\}\s*\)/); if (defineMatch) { const localeRoutesMatch = defineMatch[0].match(/localeRoutes:\s*(\{[\s\S]*?\})/); if (localeRoutesMatch && localeRoutesMatch[1]) { try { const parsedLocaleRoutes = Function('"use strict";return (' + localeRoutesMatch[1] + ")")(); if (typeof parsedLocaleRoutes === "object" && parsedLocaleRoutes !== null) { if (validateDefineI18nRouteConfig(parsedLocaleRoutes)) { return parsedLocaleRoutes; } } else { console.error("localeRoutes found but it is not a valid object in file:", filePath); } } catch (error) { console.error("Failed to parse localeRoutes:", error, "in file:", filePath); } } } return null; } function validateDefineI18nRouteConfig(obj) { if (typeof obj !== "object") return false; for (const routeKey in obj.localeRoutes) { if (typeof obj.localeRoutes[routeKey] !== "string") return false; } return true; } 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; noPrefixRedirect; constructor(locales, defaultLocaleCode, strategy, globalLocaleRoutes, noPrefixRedirect) { this.locales = locales; this.defaultLocale = this.findLocaleByCode(defaultLocaleCode) || { code: defaultLocaleCode }; this.strategy = strategy; this.noPrefixRedirect = noPrefixRedirect; this.activeLocaleCodes = this.computeActiveLocaleCodes(); this.globalLocaleRoutes = globalLocaleRoutes || {}; } 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); } // private isAlreadyLocalized(p: string) { // const codes = this.locales.map(l => l.code).join('|') // en|de|ru… // return p.startsWith('/:locale(') // динамический префикс // || new RegExp(`^/(${codes})(/|$)`).test(p) // статический /de/… // } extendPages(pages, customRegex, isCloudflarePages) { this.localizedPaths = this.extractLocalizedPaths(pages); const additionalRoutes = []; for (const page of [...pages]) { if (page.path && isInternalPath(page.path)) { 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)) 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 globalLocalePath = this.globalLocaleRoutes[pageName]; if (!globalLocalePath) { if (page.file) { const fileContent = readFileSync(page.file, "utf-8"); const localeRoutes = extractLocaleRoutes(fileContent, page.file); if (localeRoutes) { const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path)); localizedPaths[normalizedFullPath] = localeRoutes; } } } else if (typeof globalLocalePath === "object") { const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path)); 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) { this.locales.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 localeCodesWithoutCustomPaths = this.filterLocaleCodesWithoutCustomPaths(normalizedFullPath); 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); this.adjustRouteForDefaultLocale(page, originalChildren); } filterLocaleCodesWithoutCustomPaths(fullPath) { return this.activeLocaleCodes.filter((code) => !this.localizedPaths[fullPath]?.[code]); } 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, customRegex) { this.locales.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) { page.children = this.createLocalizedChildren(originalChildren, "", [locale.code], false); } 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)); const customLocalePaths = this.localizedPaths[fullPath] ?? this.localizedPaths[normalizePath(route.path)]; 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]; const basePath = customPath ? normalizePath(customPath) : normalizePath(route.path); const finalRoutePath = shouldAddLocalePrefix( locale, this.defaultLocale, addLocalePrefix, isPrefixStrategy(this.strategy) ) ? buildFullPath(locale, basePath) : basePath; const isChildRoute = parentPath !== ""; const finalPathForRoute = isChildRoute && hasParentLocalized ? normalizePath(route.path) : 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), 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 }, 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 pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy, options.globalLocaleRoutes, options.noPrefixRedirect); 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 ?? [] }; 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)) { 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)) { prerenderRoutes.push(localizedPath); } } }); } else { if (!isInternalPath(fullPath)) { 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)) { 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)) { 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 };