UNPKG

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