vite-intlayer
Version:
A Vite plugin for seamless internationalization (i18n), providing locale detection, redirection, and environment-based configuration
238 lines (235 loc) • 8.66 kB
JavaScript
import { getConfiguration } from "@intlayer/config";
import { parse } from "node:url";
import { DefaultValues } from "@intlayer/config/client";
import { getLocaleFromStorage, localeDetector, setLocaleInStorage } from "@intlayer/core";
//#region src/intlayerProxyPlugin.ts
/**
*
* A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.
*
* // Example usage of the plugin in a Vite configuration
* export default defineConfig({
* plugins: [ intlayerProxyPlugin() ],
* });
*
*/
const intlayerProxy = (configOptions, options) => {
const { internationalization, routing } = getConfiguration(configOptions);
const { locales: supportedLocales, defaultLocale } = internationalization;
const { basePath = "", mode = DefaultValues.Routing.ROUTING_MODE } = routing;
const noPrefix = mode === "no-prefix" || mode === "search-params";
const prefixDefault = mode === "prefix-all";
/**
* Retrieves the locale from storage (cookies, localStorage, sessionStorage).
*/
const getStorageLocale = (req) => {
return getLocaleFromStorage({ getCookie: (name) => {
return (req.headers.cookie ?? "").split(";").reduce((acc, cookie) => {
const [key, val] = cookie.trim().split("=");
acc[key] = val;
return acc;
}, {})[name] ?? null;
} });
};
/**
* Appends locale to search params when routing mode is 'search-params'.
*/
const appendLocaleSearchIfNeeded = (search, locale) => {
if (mode !== "search-params") return search;
const params = new URLSearchParams(search ?? "");
params.set("locale", locale);
return `?${params.toString()}`;
};
/**
* Extracts the locale from the URL pathname if present as the first segment.
*/
const getPathLocale = (pathname) => {
const firstSegment = pathname.split("/").filter(Boolean)[0];
if (firstSegment && supportedLocales.includes(firstSegment)) return firstSegment;
};
/**
* Writes a 301 redirect response with the given new URL.
*/
const redirectUrl = (res, newUrl) => {
res.writeHead(301, { Location: newUrl });
return res.end();
};
/**
* "Rewrite" the request internally by adjusting req.url;
* we also set the locale in the response header if needed.
*/
const rewriteUrl = (req, res, newUrl, locale) => {
req.url = newUrl;
if (locale) setLocaleInStorage(locale, { setHeader: (name, value) => {
res.setHeader(name, value);
req.headers[name] = value;
} });
};
/**
* Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.
* - basePath: (e.g., '/myapp')
* - locale: (e.g., 'en')
* - currentPath:(e.g., '/products/shoes')
* - search: (e.g., '?foo=bar')
*/
const constructPath = (locale, currentPath, search) => {
const pathWithoutPrefix = currentPath.startsWith(`/${locale}`) ? currentPath.slice(`/${locale}`.length) ?? "/" : currentPath;
const cleanBasePath = basePath.startsWith("/") ? basePath : `/${basePath}`;
const normalizedBasePath = cleanBasePath === "/" ? "" : cleanBasePath;
if (mode === "no-prefix") return search ? `${pathWithoutPrefix}${search}` : pathWithoutPrefix;
if (mode === "search-params") return search ? `${pathWithoutPrefix}${search}` : pathWithoutPrefix;
const pathWithLocalePrefix = currentPath.startsWith(`/${locale}`) ? currentPath : `/${locale}${currentPath}`;
let newPath = `${normalizedBasePath}${basePath.endsWith("/") ? "" : ""}${pathWithLocalePrefix}`;
if (!prefixDefault && locale === defaultLocale) newPath = `${normalizedBasePath}${pathWithoutPrefix}`;
if (search) newPath += search;
return newPath;
};
/**
* If `noPrefix` is true, we never prefix the locale in the URL.
* We simply rewrite the request to the same path, but with the best-chosen locale
* in a header or search params if desired.
*/
const handleNoPrefix = ({ req, res, next, originalPath, searchParams, storageLocale }) => {
let locale = storageLocale ?? defaultLocale;
if (!storageLocale) locale = localeDetector(req.headers, supportedLocales, defaultLocale);
if (mode === "search-params") {
if (new URLSearchParams(searchParams ?? "").get("locale") === locale) {
rewriteUrl(req, res, `${`/${locale}${originalPath}`}${searchParams ?? ""}`, locale);
return next();
}
const search$1 = appendLocaleSearchIfNeeded(searchParams, locale);
return redirectUrl(res, search$1 ? `${originalPath}${search$1}` : `${originalPath}${searchParams ?? ""}`);
}
const internalPath = `/${locale}${originalPath}`;
const search = appendLocaleSearchIfNeeded(searchParams, locale);
rewriteUrl(req, res, search ? `${internalPath}${search}` : `${internalPath}${searchParams ?? ""}`, locale);
return next();
};
/**
* The main prefix logic:
* - If there's no pathLocale in the URL, we might want to detect & redirect or rewrite
* - If there is a pathLocale, handle storage mismatch or default locale special cases
*/
const handlePrefix = ({ req, res, next, originalPath, searchParams, pathLocale, storageLocale }) => {
if (!pathLocale) {
handleMissingPathLocale({
req,
res,
next,
originalPath,
searchParams,
storageLocale
});
return;
}
handleExistingPathLocale({
req,
res,
next,
originalPath,
searchParams,
pathLocale
});
};
/**
* Handles requests where the locale is missing from the URL pathname.
* We detect a locale from storage / headers / default, then either redirect or rewrite.
*/
const handleMissingPathLocale = ({ req, res, next, originalPath, searchParams, storageLocale }) => {
let locale = storageLocale ?? localeDetector(req.headers, supportedLocales, defaultLocale);
if (!supportedLocales.includes(locale)) locale = defaultLocale;
const search = appendLocaleSearchIfNeeded(searchParams, locale);
const newPath = constructPath(locale, originalPath, search);
if (prefixDefault || locale !== defaultLocale) return redirectUrl(res, newPath);
rewriteUrl(req, res, newPath, locale);
return next();
};
/**
* Handles requests where the locale prefix is present in the pathname.
*/
const handleExistingPathLocale = ({ req, res, next, originalPath, searchParams, pathLocale }) => {
handleDefaultLocaleRedirect({
req,
res,
next,
originalPath,
searchParams,
pathLocale
});
};
/**
* If the path locale is the default locale but we don't want to prefix the default, remove it.
*/
const handleDefaultLocaleRedirect = ({ req, res, next, originalPath, searchParams, pathLocale }) => {
if (!prefixDefault && pathLocale === defaultLocale) {
let newPath = originalPath.replace(`/${defaultLocale}`, "") || "/";
if (searchParams) newPath += searchParams;
rewriteUrl(req, res, newPath, pathLocale);
return next();
}
rewriteUrl(req, res, searchParams ? `${originalPath}${searchParams}` : originalPath, pathLocale);
return next();
};
return {
name: "vite-intlayer-middleware-plugin",
configureServer: (server) => {
server.middlewares.use((req, res, next) => {
if ((options?.ignore?.(req) ?? false) || req.url?.startsWith("/node_modules") || req.url?.startsWith("/@") || req.url?.startsWith("/_") || req.url?.split("?")[0].match(/\.[a-z]+$/i)) return next();
const parsedUrl = parse(req.url ?? "/", true);
const originalPath = parsedUrl.pathname ?? "/";
const searchParams = parsedUrl.search ?? "";
const pathLocale = getPathLocale(originalPath);
const storageLocale = getStorageLocale(req);
const effectiveStorageLocale = pathLocale && supportedLocales.includes(pathLocale) ? pathLocale : storageLocale;
if (noPrefix) {
handleNoPrefix({
req,
res,
next,
originalPath,
searchParams,
storageLocale: effectiveStorageLocale
});
return;
}
handlePrefix({
req,
res,
next,
originalPath,
searchParams,
pathLocale,
storageLocale: effectiveStorageLocale
});
});
}
};
};
/**
* @deprecated Rename to intlayerProxy instead
*
* A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.
*
* ```ts
* // Example usage of the plugin in a Vite configuration
* export default defineConfig({
* plugins: [ intlayerMiddleware() ],
* });
* ```
*/
const intlayerMiddleware = intlayerProxy;
/**
* @deprecated Rename to intlayerProxy instead
*
* A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.
* ```ts
* // Example usage of the plugin in a Vite configuration
* export default defineConfig({
* plugins: [ intlayerMiddleware() ],
* });
* ```
*/
const intLayerMiddlewarePlugin = intlayerProxy;
//#endregion
export { intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
//# sourceMappingURL=intlayerProxyPlugin.mjs.map