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