UNPKG

vite-intlayer

Version:

A Vite plugin for seamless internationalization (i18n), providing locale detection, redirection, and environment-based configuration

1 lines 20.9 kB
{"version":3,"file":"intlayerProxyPlugin.mjs","names":["search"],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { parse } from 'node:url';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config';\nimport { DefaultValues } from '@intlayer/config/client';\nimport {\n getLocaleFromStorage,\n localeDetector,\n setLocaleInStorage,\n} from '@intlayer/core';\nimport type { Locale } from '@intlayer/types';\n/* @ts-ignore - Vite types error */\nimport type { Connect, Plugin } from 'vite';\n\ntype IntlayerProxyPluginOptions = {\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxyPlugin({ ignore: (req) => req.url?.startsWith('/api') }) ],\n * });\n * ```\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean;\n};\n\n/**\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerProxyPlugin() ],\n * });\n *\n */\nexport const intlayerProxy = (\n configOptions?: GetConfigurationOptions,\n options?: IntlayerProxyPluginOptions\n): Plugin => {\n const intlayerConfig = getConfiguration(configOptions);\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const { basePath = '', mode = DefaultValues.Routing.ROUTING_MODE } = routing;\n\n // Derived flags from routing.mode\n const noPrefix = mode === 'no-prefix' || mode === 'search-params';\n const prefixDefault = mode === 'prefix-all';\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n const locale = getLocaleFromStorage({\n getCookie: (name: string) => {\n const cookieHeader = req.headers.cookie ?? '';\n const cookies = cookieHeader.split(';').reduce(\n (acc, cookie) => {\n const [key, val] = cookie.trim().split('=');\n acc[key] = val;\n return acc;\n },\n {} as Record<string, string>\n );\n return cookies[name] ?? null;\n },\n });\n return locale;\n };\n\n /**\n * Appends locale to search params when routing mode is 'search-params'.\n */\n const appendLocaleSearchIfNeeded = (\n search: string | undefined,\n locale: Locale\n ): string | undefined => {\n if (mode !== 'search-params') return search;\n\n const params = new URLSearchParams(search ?? '');\n params.set('locale', locale);\n return `?${params.toString()}`;\n };\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\n // e.g. if pathname is /en/some/page or /en\n // we check if \"en\" is in your supportedLocales\n const segments = pathname.split('/').filter(Boolean);\n const firstSegment = segments[0];\n if (firstSegment && supportedLocales.includes(firstSegment as Locale)) {\n return firstSegment as Locale;\n }\n return undefined;\n };\n\n /**\n * Writes a 301 redirect response with the given new URL.\n */\n const redirectUrl = (\n res: ServerResponse<IncomingMessage>,\n newUrl: string\n ) => {\n res.writeHead(301, { Location: newUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url;\n * we also set the locale in the response header if needed.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n req.url = newUrl;\n // If you want to mimic Next.js's behavior of setting a header for the locale:\n if (locale) {\n setLocaleInStorage(locale, {\n setHeader: (name: string, value: string) => res.setHeader(name, value),\n });\n }\n };\n\n /**\n * Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.\n * - basePath: (e.g., '/myapp')\n * - locale: (e.g., 'en')\n * - currentPath:(e.g., '/products/shoes')\n * - search: (e.g., '?foo=bar')\n */\n const constructPath = (\n locale: Locale,\n currentPath: string,\n search?: string\n ) => {\n // Strip any incoming locale prefix if present\n const pathWithoutPrefix = currentPath.startsWith(`/${locale}`)\n ? (currentPath.slice(`/${locale}`.length) ?? '/')\n : currentPath;\n\n // Ensure basePath always starts with '/', and remove trailing slash if needed\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n // If basePath is '/', no trailing slash is needed\n const normalizedBasePath = cleanBasePath === '/' ? '' : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (mode === 'no-prefix') {\n const newPath = search\n ? `${pathWithoutPrefix}${search}`\n : pathWithoutPrefix;\n return newPath;\n }\n\n if (mode === 'search-params') {\n const newPath = search\n ? `${pathWithoutPrefix}${search}`\n : pathWithoutPrefix;\n return newPath;\n }\n\n // Check if path already starts with locale to avoid double-prefixing\n const pathWithLocalePrefix = currentPath.startsWith(`/${locale}`)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n const basePathTrailingSlash = basePath.endsWith('/');\n\n let newPath = `${normalizedBasePath}${basePathTrailingSlash ? '' : ''}${pathWithLocalePrefix}`;\n\n // Special case: if prefixDefault is false and locale is defaultLocale, remove the locale prefix\n if (!prefixDefault && locale === defaultLocale) {\n newPath = `${normalizedBasePath}${pathWithoutPrefix}`;\n }\n\n // Append search parameters if provided\n if (search) {\n newPath += search;\n }\n\n return newPath;\n };\n\n /* --------------------------------------------------------------------\n * Handlers that mirror Next.js style logic\n * --------------------------------------------------------------------\n */\n\n /**\n * If `noPrefix` is true, we never prefix the locale in the URL.\n * We simply rewrite the request to the same path, but with the best-chosen locale\n * in a header or search params if desired.\n */\n const handleNoPrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n }) => {\n // Determine the best locale\n let locale = storageLocale ?? defaultLocale;\n\n // Use fallback to localeDetector if no storage locale\n if (!storageLocale) {\n const detectedLocale = localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n );\n locale = detectedLocale as Locale;\n }\n\n // In search-params mode, we need to redirect to add the locale search param\n if (mode === 'search-params') {\n // Check if locale search param already exists and matches the detected locale\n const existingSearchParams = new URLSearchParams(searchParams ?? '');\n const existingLocale = existingSearchParams.get('locale');\n\n // If the existing locale matches the detected locale, no redirect needed\n if (existingLocale === locale) {\n // For internal routing, we need to add the locale prefix so the framework can match [locale] param\n const internalPath = `/${locale}${originalPath}`;\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally (URL stays the same in browser, but internally routes to /[locale]/path)\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match - redirect to add/update it\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n // Redirect to add/update the locale search param (URL changes in browser)\n return redirectUrl(res, redirectPath);\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n const internalPath = `/${locale}${originalPath}`;\n\n // Add search params if needed\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const rewritePath = search\n ? `${internalPath}${search}`\n : `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally (URL stays the same in browser, but internally routes to /[locale]/path)\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic:\n * - If there's no pathLocale in the URL, we might want to detect & redirect or rewrite\n * - If there is a pathLocale, handle storage mismatch or default locale special cases\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale?: Locale;\n storageLocale?: Locale;\n }) => {\n // 1. If pathLocale is missing, handle\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n });\n return;\n }\n\n // 2. If pathLocale exists, handle it\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * We detect a locale from storage / headers / default, then either redirect or rewrite.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n }) => {\n // 1. Choose the best locale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // 2. If still invalid, fallback\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // 3. Construct new path - preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, originalPath, search);\n\n // If we always prefix default or if this is not the default locale, do a 301 redirect\n // so that the user sees the locale in the URL.\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(res, newPath);\n }\n\n // If we do NOT prefix the default locale, just rewrite in place\n rewriteUrl(req, res, newPath, locale);\n return next();\n };\n\n /**\n * Handles requests where the locale prefix is present in the pathname.\n */\n const handleExistingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n }) => {\n // In prefix modes, respect the URL path locale\n // The path locale takes precedence, and we'll update storage to match\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n });\n };\n\n /**\n * If the path locale is the default locale but we don't want to prefix the default, remove it.\n */\n const handleDefaultLocaleRedirect = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n }) => {\n // If we don't prefix default AND the path locale is the default locale -> remove it\n if (!prefixDefault && pathLocale === defaultLocale) {\n // Remove the default locale part from the path\n let newPath = originalPath.replace(`/${defaultLocale}`, '') || '/';\n // In prefix modes, don't add locale to search params\n // Just preserve the original search params if they exist\n if (searchParams) {\n newPath += searchParams;\n }\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n // If we do prefix default or pathLocale != default, keep as is, but rewrite headers\n // Preserve original search params without adding locale\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return {\n name: 'vite-intlayer-middleware-plugin',\n configureServer: (server) => {\n server.middlewares.use((req, res, next) => {\n // Bypass assets and special Vite endpoints\n if (\n // Custom ignore function\n (options?.ignore?.(req) ?? false) ||\n req.url?.startsWith('/node_modules') ||\n /**\n * /^@vite/ # HMR client and helpers\n * /^@fs/ # file-system import serving\n * /^@id/ # virtual module ids\n * /^@tanstack/start-router-manifest # Tanstack Start Router manifest\n */\n req.url?.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n req.url?.startsWith('/_') ||\n /**\n * ./myFile.js\n */\n req.url\n ?.split('?')[0]\n .match(/\\.[a-z]+$/i) // checks for file extensions\n ) {\n return next();\n }\n\n // Parse original URL for path and query\n const parsedUrl = parse(req.url ?? '/', true);\n const originalPath = parsedUrl.pathname ?? '/';\n const searchParams = parsedUrl.search ?? '';\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\n\n // Attempt to read the locale from storage (cookies, localStorage, etc.)\n const storageLocale = getStorageLocale(req);\n\n // CRITICAL FIX: If there's a valid pathLocale, it takes precedence over storage\n // This prevents race conditions when cookies are stale during locale switches\n const effectiveStorageLocale =\n pathLocale && supportedLocales.includes(pathLocale)\n ? pathLocale\n : storageLocale;\n\n // If noPrefix is true, we skip prefix logic altogether\n if (noPrefix) {\n handleNoPrefix({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n });\n return;\n }\n\n // Otherwise, handle prefix logic\n handlePrefix({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n });\n });\n },\n };\n};\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intlayerMiddleware = intlayerProxy;\n\n/**\n * @deprecated Rename to intlayerProxy instead\n * \n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intLayerMiddlewarePlugin = intlayerProxy;\n"],"mappings":"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"}