vite-intlayer 9.4.0 → 9.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -330,4 +330,4 @@ Contribute on [GitHub](https://github.com/aymericzip/intlayer), [GitLab](https:/
330
330
 
331
331
  If you like Intlayer, give us a ⭐ on GitHub. It helps others discover the project! [See why GitHub Stars matter](https://github.com/aymericzip/intlayer/blob/main/CONTRIBUTING.md#why-github-stars-matter-).
332
332
 
333
- [![Star History Chart](https://star-history.dera.page/svg?repos=aymericzip/intlayer&type=Date)](https://star-history.dera.page/#aymericzip/intlayer&Date)
333
+ [![Star History Chart](https://api.star-history.com/svg?repos=aymericzip/intlayer&type=Date)](https://api.star-history.com/#aymericzip/intlayer&Date)
@@ -120,7 +120,7 @@ const intlayerMinify = (intlayerConfig, pruneContext) => {
120
120
  colorize("editor.enabled", ANSIColors.BLUE),
121
121
  "is",
122
122
  colorize("true", ANSIColors.GREY_DARK),
123
- colorize("— the editor resolves edits by keyPath against the unmerged dictionaries", ANSIColors.GREY)
123
+ colorize("— the editor needs complete keys", ANSIColors.GREY)
124
124
  ]), { cacheTimeoutMs: 1e4 });
125
125
  return true;
126
126
  };
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerMinifyPlugin.mjs","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { NestedRenameMap, PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Field-rename helper\n\n/**\n * Recursively renames user-defined keys in a compiled intlayer content value\n * using the provided `NestedRenameMap`.\n *\n * Traversal rules (mirrors `buildNestedRenameMapFromContent`):\n * - Arrays → each element is recursed into with the same rename map.\n * This mirrors the array pass-through in the source-code rename walk,\n * where numeric indices (e.g. [0]) are transparent.\n * - Object with `nodeType: 'translation'` → intlayer translation node.\n * Keys at this level (`nodeType`, `translation`) are NOT renamed.\n * Recurse into each per-locale value with the same rename map.\n * - Object without `nodeType` → user-defined record.\n * Rename its keys using the current rename map level, then recurse into\n * each value with that entry's `children` map.\n * - Primitives → returned as-is.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n // Arrays: each element is renamed with the same map (indices are transparent).\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale with the same rename map\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val; // key not in map – keep as-is (e.g. already-pruned)\n }\n }\n return result;\n};\n\n/**\n * Applies the nested field rename map to a parsed dictionary object and\n * returns the renamed copy. The top-level dict keys (`key`, `locale`, etc.)\n * are never touched; only keys inside `content` are renamed.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n/**\n * Discriminates a compiled qualified dictionary group (collections, variants)\n * from a plain dictionary. Mirrors `isQualifiedDictionaryGroup` from\n * `@intlayer/core` but is inlined here to avoid a runtime dependency on core\n * inside the build plugin.\n */\nconst isQualifiedDictionaryGroup = (\n dict: Record<string, unknown>\n): dict is Record<string, unknown> & {\n qualifierTypes: unknown;\n content: Record<string, unknown>;\n} =>\n 'qualifierTypes' in dict &&\n Array.isArray(dict.qualifierTypes) &&\n 'content' in dict &&\n typeof dict.content === 'object' &&\n dict.content !== null;\n\n/**\n * Applies the field-rename map to a single qualified-group content node (the\n * value stored under a composite id). Coordinates are not stored on the node —\n * they live in the key — so only the node's content fields are renamed.\n */\nconst minifyGroupEntryNode = (\n node: unknown,\n fieldRenameMap: NestedRenameMap | undefined\n): unknown =>\n fieldRenameMap &&\n fieldRenameMap.size > 0 &&\n node &&\n typeof node === 'object' &&\n !Array.isArray(node)\n ? renameContentRecursively(node, fieldRenameMap)\n : node;\n\n// Plugin\n\n/**\n * Returns the Vite plugin that minifies compiled dictionary JSON files by\n * removing all unnecessary whitespace and optionally renaming content field\n * names to short alphabetic aliases (a, b, c, …).\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * The plugin is deliberately independent of the prune plugin: it can run\n * on its own when only `build.minify` is enabled. When both `purge` and\n * `minify` are active, the prune plugin runs first (it uses `enforce: 'pre'`\n * and is registered before this one); this plugin then receives the already-\n * pruned JSON, renames its field keys, and compacts it.\n *\n * Files listed in `pruneContext.dictionariesWithEdgeCases` are skipped:\n * those dictionaries encountered a structural issue during the prune phase\n * and should be left completely untouched to avoid shipping broken data.\n *\n * Field renaming (property mangling) is applied only for dictionaries that\n * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,\n * and only while the visual editor is disabled: renaming rewrites the content\n * keys the interpreter walks, so the `keyPath` reported to the editor would no\n * longer resolve against the unmerged dictionaries. The whitespace compaction\n * and the top-level metadata strip are keyPath-neutral and always run.\n * The corresponding rename is also applied to source-file property accesses by\n * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields\n * such as `nodeType` are never renamed.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Optional shared state from the prune plugin. When\n * provided, dictionaries flagged as edge-cases are\n * skipped and field renames are applied. Pass `null`\n * if the prune plugin is not active.\n */\nexport const intlayerMinify = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, minify } = intlayerConfig.build;\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` emitted by `ContentSelector` would no longer resolve against the\n // unmerged dictionaries the editor edits. Whitespace compaction and the\n // top-level metadata strip are keyPath-neutral and stay active.\n const isFieldRenameEnabled = !intlayerConfig.editor.enabled;\n\n const { dictionariesDir, dynamicDictionariesDir, baseDir } =\n intlayerConfig.system;\n\n // Fetch-mode dictionaries are served from a remote API at runtime using their\n // original field names. Minifying them (renaming fields) would create a\n // mismatch between the server response and the renamed client-side accesses.\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir));\n\n const isMinifyEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isOptimizeActive) return false;\n if (!minify) return false;\n if (!isBuildCommand) return false;\n\n if (!isFieldRenameEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize(\n '— the editor resolves edits by keyPath against the unmerged dictionaries',\n ANSIColors.GREY\n ),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n }\n\n return true;\n };\n\n const minifyPlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-minify',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM\n // conversion. Declaration order in the plugin array ensures this runs\n // after the prune plugin (which is also 'pre' but registered earlier).\n enforce: 'pre',\n apply: isMinifyEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n return null;\n\n // Parse JSON\n let parsedDict: Record<string, unknown>;\n try {\n parsedDict = JSON.parse(rawJsonCode) as Record<string, unknown>;\n } catch (parseError) {\n logger(\n [\n `Could not parse`,\n formatPath(absoluteFilePath),\n `as JSON. Skipping minification for this file.`,\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n const dictionaryKey =\n typeof parsedDict.key === 'string' ? parsedDict.key : undefined;\n\n // Skip edge-case dictionaries\n if (\n pruneContext &&\n dictionaryKey &&\n pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)\n ) {\n return null; // structural issue flagged during prune – leave untouched\n }\n\n const fieldRenameMap =\n isFieldRenameEnabled && pruneContext && dictionaryKey\n ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey)\n : undefined;\n\n // Qualified groups (collections / variants) carry their payload as a\n // `content` map keyed by composite id + `qualifierTypes`. Preserve\n // `qualifierTypes` and the per-entry nodes (the coordinates live in the\n // keys); only the field-rename pass is applied to each node.\n if (isQualifiedDictionaryGroup(parsedDict)) {\n const minifiedContent: Record<string, unknown> = {};\n for (const [entryId, node] of Object.entries(parsedDict.content)) {\n minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);\n }\n\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n qualifierTypes: parsedDict.qualifierTypes,\n content: minifiedContent,\n }),\n map: null,\n };\n }\n\n // Apply field rename (property mangling)\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);\n }\n\n // Strip top-level metadata – ship only key + content. The variant\n // identity of a qualified chunk is encoded in its composite id (the chunk\n // path), so no extra per-chunk side-data is needed for selector matching.\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n content: parsedDict.content,\n }),\n map: null,\n };\n },\n };\n\n return [minifyPlugin];\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerMinifyPlugin.mjs","names":[],"sources":["../../src/intlayerMinifyPlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { NestedRenameMap, PruneContext } from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { formatPath, runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Field-rename helper\n\n/**\n * Recursively renames user-defined keys in a compiled intlayer content value\n * using the provided `NestedRenameMap`.\n *\n * Traversal rules (mirrors `buildNestedRenameMapFromContent`):\n * - Arrays → each element is recursed into with the same rename map.\n * This mirrors the array pass-through in the source-code rename walk,\n * where numeric indices (e.g. [0]) are transparent.\n * - Object with `nodeType: 'translation'` → intlayer translation node.\n * Keys at this level (`nodeType`, `translation`) are NOT renamed.\n * Recurse into each per-locale value with the same rename map.\n * - Object without `nodeType` → user-defined record.\n * Rename its keys using the current rename map level, then recurse into\n * each value with that entry's `children` map.\n * - Primitives → returned as-is.\n */\nconst renameContentRecursively = (\n value: unknown,\n renameMap: NestedRenameMap\n): unknown => {\n // Arrays: each element is renamed with the same map (indices are transparent).\n if (Array.isArray(value)) {\n return (value as unknown[]).map((element) =>\n renameContentRecursively(element, renameMap)\n );\n }\n\n if (!value || typeof value !== 'object') return value;\n\n const record = value as Record<string, unknown>;\n\n // Translation node: recurse into each locale with the same rename map\n if (\n typeof record.nodeType === 'string' &&\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const renamedTranslation: Record<string, unknown> = {};\n for (const [locale, localeValue] of Object.entries(\n record.translation as Record<string, unknown>\n )) {\n renamedTranslation[locale] = renameContentRecursively(\n localeValue,\n renameMap\n );\n }\n return { ...record, translation: renamedTranslation };\n }\n\n // User-defined record: rename keys and recurse into values\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(record)) {\n const renameEntry = renameMap.get(key);\n if (renameEntry) {\n result[renameEntry.shortName] = renameContentRecursively(\n val,\n renameEntry.children\n );\n } else {\n result[key] = val; // key not in map – keep as-is (e.g. already-pruned)\n }\n }\n return result;\n};\n\n/**\n * Applies the nested field rename map to a parsed dictionary object and\n * returns the renamed copy. The top-level dict keys (`key`, `locale`, etc.)\n * are never touched; only keys inside `content` are renamed.\n */\nconst applyFieldRenameToDict = (\n dict: Record<string, unknown>,\n renameMap: NestedRenameMap\n): Record<string, unknown> => {\n const content = dict.content;\n if (!content || typeof content !== 'object' || Array.isArray(content))\n return dict;\n\n return {\n ...dict,\n content: renameContentRecursively(content, renameMap),\n };\n};\n\n/**\n * Discriminates a compiled qualified dictionary group (collections, variants)\n * from a plain dictionary. Mirrors `isQualifiedDictionaryGroup` from\n * `@intlayer/core` but is inlined here to avoid a runtime dependency on core\n * inside the build plugin.\n */\nconst isQualifiedDictionaryGroup = (\n dict: Record<string, unknown>\n): dict is Record<string, unknown> & {\n qualifierTypes: unknown;\n content: Record<string, unknown>;\n} =>\n 'qualifierTypes' in dict &&\n Array.isArray(dict.qualifierTypes) &&\n 'content' in dict &&\n typeof dict.content === 'object' &&\n dict.content !== null;\n\n/**\n * Applies the field-rename map to a single qualified-group content node (the\n * value stored under a composite id). Coordinates are not stored on the node —\n * they live in the key — so only the node's content fields are renamed.\n */\nconst minifyGroupEntryNode = (\n node: unknown,\n fieldRenameMap: NestedRenameMap | undefined\n): unknown =>\n fieldRenameMap &&\n fieldRenameMap.size > 0 &&\n node &&\n typeof node === 'object' &&\n !Array.isArray(node)\n ? renameContentRecursively(node, fieldRenameMap)\n : node;\n\n// Plugin\n\n/**\n * Returns the Vite plugin that minifies compiled dictionary JSON files by\n * removing all unnecessary whitespace and optionally renaming content field\n * names to short alphabetic aliases (a, b, c, …).\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * The plugin is deliberately independent of the prune plugin: it can run\n * on its own when only `build.minify` is enabled. When both `purge` and\n * `minify` are active, the prune plugin runs first (it uses `enforce: 'pre'`\n * and is registered before this one); this plugin then receives the already-\n * pruned JSON, renames its field keys, and compacts it.\n *\n * Files listed in `pruneContext.dictionariesWithEdgeCases` are skipped:\n * those dictionaries encountered a structural issue during the prune phase\n * and should be left completely untouched to avoid shipping broken data.\n *\n * Field renaming (property mangling) is applied only for dictionaries that\n * have a known, finite field usage set in `pruneContext.dictionaryKeyToFieldRenameMap`,\n * and only while the visual editor is disabled: renaming rewrites the content\n * keys the interpreter walks, so the `keyPath` reported to the editor would no\n * longer resolve against the unmerged dictionaries. The whitespace compaction\n * and the top-level metadata strip are keyPath-neutral and always run.\n * The corresponding rename is also applied to source-file property accesses by\n * the babel rename pass inside `intlayerOptimize`. Internal intlayer fields\n * such as `nodeType` are never renamed.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Optional shared state from the prune plugin. When\n * provided, dictionaries flagged as edge-cases are\n * skipped and field renames are applied. Pass `null`\n * if the prune plugin is not active.\n */\nexport const intlayerMinify = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, minify } = intlayerConfig.build;\n\n // Field renaming rewrites the content keys the interpreter walks, so the\n // `keyPath` emitted by `ContentSelector` would no longer resolve against the\n // unmerged dictionaries the editor edits. Whitespace compaction and the\n // top-level metadata strip are keyPath-neutral and stay active.\n const isFieldRenameEnabled = !intlayerConfig.editor.enabled;\n\n const { dictionariesDir, dynamicDictionariesDir, baseDir } =\n intlayerConfig.system;\n\n // Fetch-mode dictionaries are served from a remote API at runtime using their\n // original field names. Minifying them (renaming fields) would create a\n // mismatch between the server response and the renamed client-side accesses.\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir));\n\n const isMinifyEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isOptimizeActive) return false;\n if (!minify) return false;\n if (!isBuildCommand) return false;\n\n if (!isFieldRenameEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-minify-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary field renaming is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n colorize('— the editor needs complete keys', ANSIColors.GREY),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n }\n\n return true;\n };\n\n const minifyPlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-minify',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM\n // conversion. Declaration order in the plugin array ensures this runs\n // after the prune plugin (which is also 'pre' but registered earlier).\n enforce: 'pre',\n apply: isMinifyEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!absoluteFilePath || !isDictionaryJsonFile(absoluteFilePath))\n return null;\n\n // Parse JSON\n let parsedDict: Record<string, unknown>;\n try {\n parsedDict = JSON.parse(rawJsonCode) as Record<string, unknown>;\n } catch (parseError) {\n logger(\n [\n `Could not parse`,\n formatPath(absoluteFilePath),\n `as JSON. Skipping minification for this file.`,\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n const dictionaryKey =\n typeof parsedDict.key === 'string' ? parsedDict.key : undefined;\n\n // Skip edge-case dictionaries\n if (\n pruneContext &&\n dictionaryKey &&\n pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)\n ) {\n return null; // structural issue flagged during prune – leave untouched\n }\n\n const fieldRenameMap =\n isFieldRenameEnabled && pruneContext && dictionaryKey\n ? pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey)\n : undefined;\n\n // Qualified groups (collections / variants) carry their payload as a\n // `content` map keyed by composite id + `qualifierTypes`. Preserve\n // `qualifierTypes` and the per-entry nodes (the coordinates live in the\n // keys); only the field-rename pass is applied to each node.\n if (isQualifiedDictionaryGroup(parsedDict)) {\n const minifiedContent: Record<string, unknown> = {};\n for (const [entryId, node] of Object.entries(parsedDict.content)) {\n minifiedContent[entryId] = minifyGroupEntryNode(node, fieldRenameMap);\n }\n\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n qualifierTypes: parsedDict.qualifierTypes,\n content: minifiedContent,\n }),\n map: null,\n };\n }\n\n // Apply field rename (property mangling)\n if (fieldRenameMap && fieldRenameMap.size > 0) {\n parsedDict = applyFieldRenameToDict(parsedDict, fieldRenameMap);\n }\n\n // Strip top-level metadata – ship only key + content. The variant\n // identity of a qualified chunk is encoded in its composite id (the chunk\n // path), so no extra per-chunk side-data is needed for selector matching.\n return {\n code: JSON.stringify({\n key: parsedDict.key,\n content: parsedDict.content,\n }),\n map: null,\n };\n },\n };\n\n return [minifyPlugin];\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,4BACJ,OACA,cACY;CAEZ,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAQ,MAAoB,KAAK,YAC/B,yBAAyB,SAAS,SAAS,CAC7C;CAGF,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAEhD,MAAM,SAAS;CAGf,IACE,OAAO,OAAO,aAAa,YAC3B,OAAO,eACP,OAAO,OAAO,gBAAgB,YAC9B,CAAC,MAAM,QAAQ,OAAO,WAAW,GACjC;EACA,MAAM,qBAA8C,CAAC;EACrD,KAAK,MAAM,CAAC,QAAQ,gBAAgB,OAAO,QACzC,OAAO,WACT,GACE,mBAAmB,UAAU,yBAC3B,aACA,SACF;EAEF,OAAO;GAAE,GAAG;GAAQ,aAAa;EAAmB;CACtD;CAGA,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,MAAM,GAAG;EAC/C,MAAM,cAAc,UAAU,IAAI,GAAG;EACrC,IAAI,aACF,OAAO,YAAY,aAAa,yBAC9B,KACA,YAAY,QACd;OAEA,OAAO,OAAO;CAElB;CACA,OAAO;AACT;;;;;;AAOA,MAAM,0BACJ,MACA,cAC4B;CAC5B,MAAM,UAAU,KAAK;CACrB,IAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAClE,OAAO;CAET,OAAO;EACL,GAAG;EACH,SAAS,yBAAyB,SAAS,SAAS;CACtD;AACF;;;;;;;AAQA,MAAM,8BACJ,SAKA,oBAAoB,QACpB,MAAM,QAAQ,KAAK,cAAc,KACjC,aAAa,QACb,OAAO,KAAK,YAAY,YACxB,KAAK,YAAY;;;;;;AAOnB,MAAM,wBACJ,MACA,mBAEA,kBACA,eAAe,OAAO,KACtB,QACA,OAAO,SAAS,YAChB,CAAC,MAAM,QAAQ,IAAI,IACf,yBAAyB,MAAM,cAAc,IAC7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCN,MAAa,kBACX,gBACA,iBACmB;CACnB,MAAM,SAAS,aAAa,cAAc;CAE1C,MAAM,EAAE,UAAU,WAAW,eAAe;CAM5C,MAAM,uBAAuB,CAAC,eAAe,OAAO;CAEpD,MAAM,EAAE,iBAAiB,wBAAwB,YAC/C,eAAe;CAKjB,MAAM,wBAAwB,qBAC5B,iBAAiB,SAAS,OAAO,MAChC,iBAAiB,WAAW,eAAe,KAC1C,iBAAiB,WAAW,sBAAsB;CAEtD,MAAM,mBACJ,SACA,QACY;EACZ,MAAM,iBAAiB,IAAI,YAAY;EAIvC,IAAI,EAFD,aAAa,UAAa,kBAAmB,aAAa,OAEtC,OAAO;EAC9B,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,gBAAgB,OAAO;EAE5B,IAAI,CAAC,sBACH,QACE,KACE,SACA,aACA,SACA,qCACF,SAEE,OAAO;GACL;GACA,SAAS,YAAY,WAAW,SAAS;GACzC;GACA,SAAS,kBAAkB,WAAW,IAAI;GAC1C;GACA,SAAS,QAAQ,WAAW,SAAS;GACrC,SAAS,oCAAoC,WAAW,IAAI;EAC9D,CAAC,GACH,EAAE,gBAAgB,IAAU,CAC9B;EAGF,OAAO;CACT;CA0FA,OAAO,CAAC;EAvFN,MAAM;EAIN,SAAS;EACT,OAAO;EAEP,YAAY,aAAa,aAAa;GACpC,MAAM,mBAAmB,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC;GAEhD,IAAI,CAAC,oBAAoB,CAAC,qBAAqB,gBAAgB,GAC7D,OAAO;GAGT,IAAI;GACJ,IAAI;IACF,aAAa,KAAK,MAAM,WAAW;GACrC,SAAS,YAAY;IACnB,OACE;KACE;KACA,WAAW,gBAAgB;KAC3B;KACA,sBAAsB,QAClB,IAAI,WAAW,QAAQ,KACvB,OAAO,UAAU;IACvB,GACA,EAAE,OAAO,OAAO,CAClB;IACA,OAAO;GACT;GAEA,MAAM,gBACJ,OAAO,WAAW,QAAQ,WAAW,WAAW,MAAM;GAGxD,IACE,gBACA,iBACA,aAAa,0BAA0B,IAAI,aAAa,GAExD,OAAO;GAGT,MAAM,iBACJ,wBAAwB,gBAAgB,gBACpC,aAAa,8BAA8B,IAAI,aAAa,IAC5D;GAMN,IAAI,2BAA2B,UAAU,GAAG;IAC1C,MAAM,kBAA2C,CAAC;IAClD,KAAK,MAAM,CAAC,SAAS,SAAS,OAAO,QAAQ,WAAW,OAAO,GAC7D,gBAAgB,WAAW,qBAAqB,MAAM,cAAc;IAGtE,OAAO;KACL,MAAM,KAAK,UAAU;MACnB,KAAK,WAAW;MAChB,gBAAgB,WAAW;MAC3B,SAAS;KACX,CAAC;KACD,KAAK;IACP;GACF;GAGA,IAAI,kBAAkB,eAAe,OAAO,GAC1C,aAAa,uBAAuB,YAAY,cAAc;GAMhE,OAAO;IACL,MAAM,KAAK,UAAU;KACnB,KAAK,WAAW;KAChB,SAAS,WAAW;IACtB,CAAC;IACD,KAAK;GACP;EACF;CAGiB,CAAC;AACtB"}
@@ -1,6 +1,5 @@
1
1
  import { getCanonicalPath, getDomainHostname, getDomainOrigin, getInternalPath, getLocaleFromDomain, getRewriteRules, isProxyStorageLocaleEnabled, localeDetector, resolveLocalizedPath, resolveProxyMode } from "@intlayer/core/localization";
2
2
  import { ROUTING_MODE } from "@intlayer/config/defaultValues";
3
- import { parse } from "node:url";
4
3
  import { getCookie, getLocaleFromStorageServer, setLocaleInStorageServer } from "@intlayer/core/utils";
5
4
 
6
5
  //#region src/intlayerProxyHandler.ts
@@ -34,6 +33,58 @@ const TANSTACK_PRERENDER_ENV_VAR = "TSS_PRERENDERING";
34
33
  */
35
34
  const isPrerenderRequest = (req) => Boolean(req.headers[NITRO_PRERENDER_HEADER]) || process.env[TANSTACK_PRERENDER_ENV_VAR] === "true";
36
35
  /**
36
+ * Decodes a request pathname so it can be matched against the rewrite rules.
37
+ *
38
+ * A rule is written with the characters the locale actually uses
39
+ * (`/doc/релизы/v8`) while the browser sends them percent-encoded, so without
40
+ * this every non-ASCII localized URL misses its own rule and is treated as an
41
+ * unlocalized path. `decodeURI` leaves the reserved escapes (`%2F`, `%3F`,
42
+ * `%23`) encoded, which keeps the segmentation of the path intact, and a
43
+ * malformed escape is handed back untouched rather than throwing.
44
+ */
45
+ const decodePathname = (pathname) => {
46
+ try {
47
+ return decodeURI(pathname);
48
+ } catch {
49
+ return pathname;
50
+ }
51
+ };
52
+ /**
53
+ * Re-encodes a decoded path before it is written back to `req.url`, leaving the
54
+ * query string as it was received.
55
+ *
56
+ * The inverse of {@link decodePathname}: `encodeURI` escapes exactly what it
57
+ * decoded, so characters that are legal in a path — `+` in
58
+ * `/doc/intlayer_with_vite+react`, for instance — survive the round trip, and
59
+ * an already-encoded path is left as is.
60
+ */
61
+ const encodePathname = (path) => {
62
+ const queryIndex = path.indexOf("?");
63
+ const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex);
64
+ const search = queryIndex === -1 ? "" : path.slice(queryIndex);
65
+ return `${encodeURI(pathname)}${search}`;
66
+ };
67
+ /**
68
+ * Splits a raw request target (`req.url`) into its pathname and search string.
69
+ *
70
+ * Replaces the deprecated `url.parse`, and unlike `new URL(requestUrl, base)`
71
+ * it never reinterprets the leading `//` of a request target as an authority,
72
+ * so `//example.com/page` stays the pathname it was sent as. The fragment is
73
+ * dropped first — a browser never sends one, but a hand-written request may.
74
+ */
75
+ const splitRequestTarget = (requestUrl) => {
76
+ const hashIndex = requestUrl.indexOf("#");
77
+ const target = hashIndex === -1 ? requestUrl : requestUrl.slice(0, hashIndex);
78
+ const queryIndex = target.indexOf("?");
79
+ return queryIndex === -1 ? {
80
+ pathname: target,
81
+ search: ""
82
+ } : {
83
+ pathname: target.slice(0, queryIndex),
84
+ search: target.slice(queryIndex)
85
+ };
86
+ };
87
+ /**
37
88
  * Creates the locale-routing middleware from an already-resolved configuration.
38
89
  *
39
90
  * This is the runtime core shared by every environment - dev server, preview
@@ -222,7 +273,8 @@ const createProxyHandler = (options) => {
222
273
  * Next.js's behaviour of propagating the detected locale downstream.
223
274
  */
224
275
  const rewriteUrl = (req, res, newUrl, locale) => {
225
- if (req.url !== newUrl) req.url = newUrl;
276
+ const encodedUrl = encodePathname(newUrl);
277
+ if (req.url !== encodedUrl) req.url = encodedUrl;
226
278
  if (locale) setLocaleInStorageServer(locale, { setHeader: (name, value) => {
227
279
  res.setHeader(name, value);
228
280
  req.headers[name] = value;
@@ -376,13 +428,16 @@ const createProxyHandler = (options) => {
376
428
  return next();
377
429
  };
378
430
  return (req, res, next) => {
379
- const parsedUrl = parse(req.url ?? "/", true);
380
- const originalPath = parsedUrl.pathname ?? "/";
381
- const searchParams = parsedUrl.search ?? "";
431
+ const { pathname, search } = splitRequestTarget(req.url ?? "/");
432
+ const originalPath = decodePathname(pathname || "/");
433
+ const searchParams = search;
382
434
  const pathLocale = getPathLocale(originalPath);
383
435
  if ((ignore?.(req) ?? false) || originalPath.startsWith("/node_modules") || originalPath.startsWith("/@") || originalPath.startsWith("/_")) return next();
384
436
  if (originalPath.match(/\.[a-zA-Z0-9]+$/)) {
385
- if (pathLocale) req.url = `${originalPath.slice(`/${pathLocale}`.length) || "/"}${searchParams}`;
437
+ if (pathLocale) {
438
+ const pathWithoutLocale = originalPath.slice(`/${pathLocale}`.length) || "/";
439
+ req.url = encodePathname(`${pathWithoutLocale}${searchParams}`);
440
+ }
386
441
  return next();
387
442
  }
388
443
  const storageLocale = getStorageLocale(req);
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerProxyHandler.mjs","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"sourcesContent":["/**\n * Runtime half of the Intlayer proxy: locale detection, redirects and rewrites.\n *\n * Nothing here may import `@intlayer/config/node`. This module is bundled into\n * production servers (see `intlayerNitroHandler`), and the node configuration\n * loader transpiles `intlayer.config.ts` with `esbuild` at runtime, which makes\n * the built server depend on `esbuild` being resolvable next to it.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { parse } from 'node:url';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport {\n getCanonicalPath,\n getDomainHostname,\n getDomainOrigin,\n getInternalPath,\n getLocaleFromDomain,\n getRewriteRules,\n isProxyStorageLocaleEnabled,\n localeDetector,\n resolveLocalizedPath,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n/* @ts-ignore - Vite types error */\nimport type { Connect } from 'vite';\n\n/**\n * Header Nitro's prerenderer sets on every request it issues to the server it\n * is generating pages from. Its value is the route being generated.\n */\nconst NITRO_PRERENDER_HEADER = 'x-nitro-prerender';\n\n/**\n * Environment flag TanStack Start sets on the build process while it prerenders\n * pages through a Vite preview server. The preview server — and therefore this\n * middleware — runs in that same process, so the flag is readable here.\n */\nconst TANSTACK_PRERENDER_ENV_VAR = 'TSS_PRERENDERING';\n\n/**\n * Detects requests issued by a prerenderer rather than by a real visitor.\n *\n * Prerendered pages are written to disk and then served to every visitor, so\n * their content must depend on the URL alone. Anything client-specific — a\n * locale cookie, a persisted locale on the response — would either be baked\n * into a static file or make the generated page depend on the machine running\n * the build.\n *\n * @param req - The incoming request.\n * @returns `true` when the request comes from a prerender pass.\n *\n * @example\n * ```ts\n * isPrerenderRequest({ headers: { 'x-nitro-prerender': '/about' } }); // true\n * ```\n */\nconst isPrerenderRequest = (req: IncomingMessage): boolean =>\n Boolean(req.headers[NITRO_PRERENDER_HEADER]) ||\n process.env[TANSTACK_PRERENDER_ENV_VAR] === 'true';\n\n/**\n * A Node.js-compatible Connect middleware function.\n * Compatible with Vite dev/preview server, Node.js http, Express, and h3's\n * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.\n */\nexport type NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\nexport type IntlayerProxyHandlerOptions = {\n /**\n * The resolved Intlayer configuration driving locale detection and routing.\n */\n configuration: IntlayerConfig;\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Whether a development or preview server is serving the app.\n *\n * It only matters in the proxy's auto mode, where a dev server keeps locale\n * routing URL-driven by ignoring the stored locale as a redirect source.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * Creates the locale-routing middleware from an already-resolved configuration.\n *\n * This is the runtime core shared by every environment - dev server, preview\n * server and the production Nitro server. It deliberately receives the\n * configuration instead of loading it: loading it would pull\n * `@intlayer/config/node` - and therefore `esbuild` - into the server bundle,\n * which then fails to boot wherever `node_modules` is not shipped next to the\n * build output.\n *\n * @param options - The resolved configuration plus optional handler behaviour.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n *\n * @see `createIntlayerProxyHandler` for the public, configuration-loading entry point.\n */\nexport const createProxyHandler = (\n options: IntlayerProxyHandlerOptions\n): NodeMiddleware => {\n const {\n configuration: intlayerConfig,\n ignore,\n isDevServer = false,\n } = options;\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const {\n basePath = '',\n mode = ROUTING_MODE,\n rewrite,\n domains,\n enableProxy,\n } = routing;\n\n // In auto mode a dev/preview server ignores the stored locale when resolving\n // which locale a request maps to, so a lingering cookie cannot keep pulling\n // navigation to another locale while developing. Prefix redirects, locale\n // persistence and `Accept-Language` detection are unaffected.\n const canUseStorageLocale = isProxyStorageLocaleEnabled(\n resolveProxyMode(enableProxy),\n isDevServer\n );\n\n type RedirectCounter = { count: number; lastSeen: number };\n const redirectCounts = new Map<string, RedirectCounter>();\n const MAX_REDIRECTS = 10;\n const REDIRECT_TTL_MS = 2_000;\n\n // Derived flags from routing.mode\n const noPrefix =\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params');\n const prefixDefault =\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all'\n ) && mode === 'prefix-all';\n\n const rewriteRules =\n process.env.INTLAYER_ROUTING_REWRITE_RULES !== 'false'\n ? getRewriteRules(rewrite, 'url')\n : undefined;\n\n /**\n * Resolves the locale a localized (\"pretty\") path belongs to.\n *\n * A path such as `/contacto` only exists because a rewrite rule maps it to\n * the Spanish locale, so it declares its locale just as explicitly as an\n * `/es` prefix would. Without this, a visitor whose `Accept-Language` (or\n * stored locale) says `en` gets `/contacto` canonicalized against the\n * English rules only, which never matches — the proxy then treats it as an\n * unlocalized path and redirects to `/en/contacto`, a URL no locale owns.\n *\n * Only patterns that differ from their canonical form are considered: a rule\n * whose localized path equals the canonical one (`en: '/contact'`) carries no\n * locale signal, and matching on it would hijack the default locale's\n * unprefixed URLs.\n *\n * @param path - The request pathname, without any locale prefix.\n * @returns The locale declared by the path, or `undefined` when it declares none.\n */\n const getRewriteLocale = (path: string): Locale | undefined =>\n rewriteRules\n ? supportedLocales.find(\n (candidate) =>\n getCanonicalPath(path, candidate, rewriteRules) !== path\n )\n : undefined;\n\n /**\n * Extracts the hostname from a request's `Host` header, handling IPv6\n * literals (`[::1]:5173` → `::1`) that a plain `split(':')` would mangle.\n */\n const getRequestHostname = (req: IncomingMessage): string => {\n const host = req.headers.host ?? '';\n const ipv6Match = host.match(/^\\[([^\\]]+)\\]/);\n if (ipv6Match) return ipv6Match[1]!;\n return host.split(':')[0] ?? '';\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n *\n * Returns `undefined` when the stored locale is not allowed to drive locale\n * resolution (auto mode on a dev/preview server, or a prerender pass, whose\n * output must stay URL-driven), which makes every caller fall through to\n * `Accept-Language` detection and then the default locale.\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n if (!canUseStorageLocale || isPrerenderRequest(req)) return undefined;\n\n const locale = getLocaleFromStorageServer({\n getCookie: (name: string) => getCookie(name, req.headers.cookie),\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 (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params') ||\n mode !== 'search-params'\n )\n return search;\n\n const params = new URLSearchParams(search ?? '');\n\n params.set('locale', locale);\n\n return `?${params.toString()}`;\n };\n\n /**\n * Checks whether a pathname starts with the given locale as a full path\n * segment (`/fr` or `/fr/...`). A bare `startsWith('/fr')` would also match\n * unrelated paths like `/friends`, causing wrong prefix stripping and\n * self-redirect loops.\n */\n const hasLocaleSegmentPrefix = (pathname: string, locale: Locale): boolean =>\n pathname === `/${locale}` || pathname.startsWith(`/${locale}/`);\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n * e.g. if pathname is /en/some/page or /en, checks if \"en\" is in supportedLocales.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\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 * Persists the resolved locale onto the outgoing response as a cookie (and\n * header, per `routing.storage`). Only the cookie survives a client redirect,\n * so this is what carries an explicitly-selected locale across a\n * prefix-stripping redirect. Enabled cookie/header targets are resolved by\n * {@link setLocaleInStorageServer} from the config; disabled ones are no-ops.\n */\n const persistLocaleOnResponse = (\n res: ServerResponse<IncomingMessage>,\n locale: Locale\n ) => {\n setLocaleInStorageServer(locale, {\n setCookieStore: (name, value, attributes) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n if (attributes.path) parts.push(`Path=${attributes.path}`);\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (typeof attributes.expires === 'number')\n parts.push(`Expires=${new Date(attributes.expires).toUTCString()}`);\n if (attributes.secure) parts.push('Secure');\n if (attributes.httpOnly) parts.push('HttpOnly');\n if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`);\n\n const cookieString = parts.join('; ');\n const existing = res.getHeader('Set-Cookie');\n const cookies = Array.isArray(existing)\n ? existing.map(String)\n : existing !== undefined\n ? [String(existing)]\n : [];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n },\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n },\n });\n };\n\n /**\n * Identifies the client issuing a request, so redirect-loop tracking is\n * scoped per client. Without this, concurrent visitors hitting the same\n * legitimate redirect (e.g. `/` → `/fr/`) would share one counter and trip\n * the loop detector under normal production traffic.\n */\n const getClientKey = (req: IncomingMessage): string => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const forwardedIp = (\n Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor\n )\n ?.split(',')[0]\n ?.trim();\n\n return forwardedIp || req.socket?.remoteAddress || 'unknown';\n };\n\n /**\n * Strips any origin from a redirect target, keeping only path, search and\n * hash. Prevents open redirects: user-controlled paths like `//evil.com/x`\n * would otherwise be echoed verbatim into the `Location` header and be\n * interpreted by browsers as a protocol-relative cross-origin URL.\n */\n const toSameOriginUrl = (url: string): string => {\n try {\n const parsed = new URL(url, 'http://intlayer-internal');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return url;\n }\n };\n\n type RedirectOptions = {\n /** Human-readable reason logged when a redirect loop is detected. */\n reason?: string;\n /** The URL of the incoming request, used for redirect-loop tracking. */\n originalUrl?: string;\n /**\n * When provided, the locale is persisted (cookie/header, per config) on\n * the redirect response so the follow-up request resolves the same locale.\n */\n persistLocale?: Locale;\n /**\n * HTTP status for the redirect. Defaults to 302: locale redirects depend\n * on mutable state (cookie, Accept-Language) and must not be cached by\n * the browser as permanent. Domain-routing redirects pass 301 explicitly.\n */\n status?: number;\n /**\n * Allows a cross-origin `Location` target. Only the domain-routing\n * redirect sets this; all other targets are sanitized to same-origin.\n */\n allowCrossOrigin?: boolean;\n };\n\n /**\n * Writes a redirect response with the given new URL.\n */\n const redirectUrl = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n options?: RedirectOptions\n ) => {\n const {\n reason,\n originalUrl,\n persistLocale,\n status = 302,\n allowCrossOrigin = false,\n } = options ?? {};\n\n const targetUrl = allowCrossOrigin ? newUrl : toSameOriginUrl(newUrl);\n\n if (originalUrl) {\n if (originalUrl === targetUrl) {\n console.error('[REDIRECT LOOP DETECTED!]', { originalUrl, reason });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${originalUrl} redirects to itself`\n );\n }\n\n const now = Date.now();\n const trackingKey = `${getClientKey(req)}|${originalUrl} -> ${targetUrl}`;\n const previousEntry = redirectCounts.get(trackingKey);\n const count =\n previousEntry && now - previousEntry.lastSeen < REDIRECT_TTL_MS\n ? previousEntry.count + 1\n : 1;\n\n redirectCounts.set(trackingKey, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: targetUrl,\n reason,\n });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${count} redirects from ${originalUrl}`\n );\n }\n\n for (const [entryKey, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS)\n redirectCounts.delete(entryKey);\n }\n }\n\n // A prerendered response is stored as a static file and replayed to every\n // visitor, so persisting the locale on it would hand the locale of the\n // build machine to everyone hitting that page.\n if (persistLocale && !isPrerenderRequest(req)) {\n persistLocaleOnResponse(res, persistLocale);\n }\n\n res.writeHead(status, { Location: targetUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url.\n * Also sets the locale in the response/request headers via storage to mimic\n * Next.js's behaviour of propagating the detected locale downstream.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n if (req.url !== newUrl) {\n req.url = newUrl;\n }\n if (locale) {\n setLocaleInStorageServer(locale, {\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n req.headers[name] = value;\n },\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 to avoid double-prefixing\n const pathWithoutPrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/' and has no trailing slash\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params')\n ) {\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 = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n let newPath = `${normalizedBasePath}${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 // Never emit a trailing slash (`/fr/` for the root path): the framework's\n // trailing-slash normalisation would redirect it back and forth with this\n // proxy, creating an infinite redirect loop.\n if (newPath.length > 1 && newPath.endsWith('/')) {\n newPath = newPath.slice(0, -1);\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 originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n const pathLocale = getPathLocale(originalPath);\n\n // Determine the best locale: prefer cookie/storage, fall back to Accept-Language detection\n let locale = storageLocale ?? defaultLocale;\n\n // Use localeDetector if no storage locale is available\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 // Guard against a stale/unsupported locale coming from storage or detection\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Without a prefix, a localized path is the only locale signal the URL\n // carries, so it takes precedence over storage / `Accept-Language`.\n if (!pathLocale) {\n locale = getRewriteLocale(originalPath) ?? locale;\n }\n\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n pathLocale,\n rewriteRules\n );\n\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n const redirectPath = search\n ? `${canonicalPath}${search}`\n : `${canonicalPath}${searchParams ?? ''}`;\n\n // Persist the explicitly-requested locale: stripping the prefix drops\n // the only locale signal from the URL, so without this the follow-up\n // request would fall back to cookie / Accept-Language detection and\n // could resolve a different locale.\n return redirectUrl(req, res, redirectPath, {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // In search-params mode, we need to redirect to add the locale search param\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\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 (existingLocale === locale) {\n // Rewrite internally — URL stays the same in the browser, but the framework\n // sees /[locale]/path so the [locale] route param is populated correctly.\n // getInternalPath collapses the root path to `/${locale}` — a trailing\n // slash (`/fr/`) would trigger the framework's trailing-slash\n // normalisation redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match — redirect to add/update it (URL changes in browser)\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n return redirectUrl(req, res, redirectPath, { originalUrl });\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n // so the framework can match the [locale] route param without exposing it in the URL.\n // getInternalPath collapses the root path to `/${locale}` — a trailing slash\n // (`/fr/`) would trigger the framework's trailing-slash normalisation\n // redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n\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 the browser\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic.\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n originalUrl,\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 originalUrl?: string;\n }) => {\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n });\n return;\n }\n\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * Detects a locale from storage / headers / default, then either redirects or rewrites.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n // Choose the best locale: cookie/storage → Accept-Language detection → defaultLocale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // If still invalid, fall back to defaultLocale\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // A localized path names its own locale, and the URL always outranks the\n // stored / negotiated one — otherwise /a-propos read as 'en' resolves to\n // no rule and gets redirected to the ownerless /en/a-propos.\n locale = getRewriteLocale(originalPath) ?? locale;\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), this resolves to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection.\n // /about + 'fr' → /a-propos\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n\n // Construct new path, preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, targetLocalizedPath, search);\n\n // If we always prefix default or if this is not the default locale,\n // do a redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(req, res, newPath, { originalUrl });\n }\n\n // If we do NOT prefix the default locale, pass through the canonical path unchanged.\n // Rewriting to `/${locale}${canonicalPath}` (e.g. /en/) causes TanStack Start to issue a\n // trailing-slash normalisation redirect (/en/ → /en), which the proxy then strips back to /,\n // creating an infinite redirect loop.\n // Because {-$locale} is an optional segment, the framework matches the un-prefixed URL with\n // locale=undefined and falls back to defaultLocale via `params.locale ?? defaultLocale`.\n // searchParams MUST be preserved here — dropping them causes the framework (e.g. TanStack Start) to\n // see a URL with no search params, trigger a validateSearch normalisation redirect to the prefixed URL\n // (e.g. /en?page=1&...), which the middleware then strips back to /?..., creating an infinite loop.\n rewriteUrl(req, res, `${canonicalPath}${searchParams}`, 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 originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n originalUrl?: string;\n }) => {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n\n // Identify the canonical path (internal path).\n // Ex: /a-propos (from URL) → /about (canonical)\n const canonicalPath = getCanonicalPath(rawPath, pathLocale, rewriteRules);\n\n // When rewrite rules are configured and the URL is already a valid localized pretty URL\n // (e.g. /fr/essais which maps to canonical /fr/tests), do NOT redirect to canonical.\n //\n // Why: the SPA router (Solid, React Router, Vue Router…) is expected to define routes using\n // the localized paths (e.g. <Route path=\"/essais\">) so the browser URL must stay as-is.\n // A 301 redirect to canonical would:\n // 1. Change the browser URL to the canonical form (/fr/tests)\n // 2. Break subsequent client-side navigation because <A> links produced by getLocalizedUrl\n // point back to the localized URL (/fr/essais) which then has no matching route.\n //\n // We set the locale header and call next() so the server serves the page at the pretty URL.\n if (canonicalPath !== rawPath) {\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n // The URL is in canonical form (e.g. /fr/about) while the locale owns a\n // different pretty URL for it (fr: '/a-propos'). Redirect so the visible\n // URL is the one the locale owns — the same one `getLocalizedUrl` emits\n // and the one the SPA router declares a route for. Skipped when the\n // default-locale prefix has to be stripped anyway: `handleDefaultLocaleRedirect`\n // resolves the localized path itself, so redirecting here first would\n // cost a second round trip.\n if (prefixDefault || pathLocale !== defaultLocale) {\n const { path: targetLocalizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n if (isRewritten && targetLocalizedPath !== rawPath) {\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n return redirectUrl(\n req,\n res,\n constructPath(pathLocale, targetLocalizedPath, search),\n { originalUrl }\n );\n }\n }\n\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\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 searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n searchParams: string;\n pathLocale: Locale;\n canonicalPath: string;\n originalUrl?: string;\n }) => {\n // If we don't prefix the default locale AND the path locale IS the default → strip the prefix\n if (!prefixDefault && pathLocale === defaultLocale) {\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n // Construct path without prefix\n const cleanBasePath = basePath.startsWith('/')\n ? basePath\n : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n let finalPath = targetLocalizedPath;\n if (finalPath.startsWith('/')) finalPath = finalPath.slice(1);\n\n const fullPath = `${normalizedBasePath}/${finalPath}`.replace(\n /\\/+/g,\n '/'\n );\n\n // Persist the explicitly-requested default locale. Stripping the prefix\n // (e.g. /es → /) drops the only locale signal from the URL, so without\n // this the follow-up request to the canonical path would fall back to\n // Accept-Language detection and could resolve a different locale.\n return redirectUrl(req, res, fullPath + (searchParams ?? ''), {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally.\n // Never emit a trailing slash (`/fr/` for the bare `/fr` URL): rewriting\n // `/fr` to `/fr/` makes the framework issue a trailing-slash normalisation\n // redirect back to `/fr`, which this proxy rewrites again — an infinite\n // redirect loop. `getInternalPath` collapses the root path to `/${locale}`.\n const internalUrl = getInternalPath(canonicalPath, pathLocale);\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return (req, res, next) => {\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 // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (ignore?.(req) ?? false) ||\n originalPath.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 originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = `${pathWithoutLocale}${searchParams}`;\n }\n return next();\n }\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 // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = getRequestHostname(req);\n const domainHost = getDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = getDomainOrigin(localeDomain);\n // Domain mapping is stable config, so a cacheable 301 is intended\n // here — unlike locale-detection redirects, which use 302.\n redirectUrl(req, res, `${targetOrigin}${rawPath}${searchParams}`, {\n reason: 'domain-routing',\n originalUrl,\n status: 301,\n allowCrossOrigin: true,\n });\n return;\n }\n }\n }\n\n // Domain routing: if the current hostname is exclusively mapped to one locale,\n // treat it as that locale without a URL prefix.\n // e.g. intlayer.zh/about → internally rewrite to /zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n !pathLocale\n ) {\n const reqHost = getRequestHostname(req);\n const domainLocale = getLocaleFromDomain(reqHost, domains);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n\n // Default locale without forced prefixing: pass the canonical path\n // through unchanged, mirroring handleMissingPathLocale. Rewriting `/`\n // to `/en/` makes TanStack Start issue a trailing-slash normalisation\n // redirect (/en/ → /en), whose prefix the proxy then strips back to /,\n // creating an infinite redirect loop.\n if (!prefixDefault && domainLocale === defaultLocale) {\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n `${canonicalPath}${searchParams}`,\n domainLocale\n );\n return next();\n }\n\n // Never emit a trailing slash (`/zh/`): the framework would redirect\n // it to `/zh`, which this proxy rewrites to `/zh/` again — a loop.\n const internalPath = getInternalPath(canonicalPath, domainLocale);\n\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n if (noPrefix) {\n handleNoPrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n handlePrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerProxyHandler.mjs","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"sourcesContent":["/**\n * Runtime half of the Intlayer proxy: locale detection, redirects and rewrites.\n *\n * Nothing here may import `@intlayer/config/node`. This module is bundled into\n * production servers (see `intlayerNitroHandler`), and the node configuration\n * loader transpiles `intlayer.config.ts` with `esbuild` at runtime, which makes\n * the built server depend on `esbuild` being resolvable next to it.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport {\n getCanonicalPath,\n getDomainHostname,\n getDomainOrigin,\n getInternalPath,\n getLocaleFromDomain,\n getRewriteRules,\n isProxyStorageLocaleEnabled,\n localeDetector,\n resolveLocalizedPath,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n/* @ts-ignore - Vite types error */\nimport type { Connect } from 'vite';\n\n/**\n * Header Nitro's prerenderer sets on every request it issues to the server it\n * is generating pages from. Its value is the route being generated.\n */\nconst NITRO_PRERENDER_HEADER = 'x-nitro-prerender';\n\n/**\n * Environment flag TanStack Start sets on the build process while it prerenders\n * pages through a Vite preview server. The preview server — and therefore this\n * middleware — runs in that same process, so the flag is readable here.\n */\nconst TANSTACK_PRERENDER_ENV_VAR = 'TSS_PRERENDERING';\n\n/**\n * Detects requests issued by a prerenderer rather than by a real visitor.\n *\n * Prerendered pages are written to disk and then served to every visitor, so\n * their content must depend on the URL alone. Anything client-specific — a\n * locale cookie, a persisted locale on the response — would either be baked\n * into a static file or make the generated page depend on the machine running\n * the build.\n *\n * @param req - The incoming request.\n * @returns `true` when the request comes from a prerender pass.\n *\n * @example\n * ```ts\n * isPrerenderRequest({ headers: { 'x-nitro-prerender': '/about' } }); // true\n * ```\n */\nconst isPrerenderRequest = (req: IncomingMessage): boolean =>\n Boolean(req.headers[NITRO_PRERENDER_HEADER]) ||\n process.env[TANSTACK_PRERENDER_ENV_VAR] === 'true';\n\n/**\n * Decodes a request pathname so it can be matched against the rewrite rules.\n *\n * A rule is written with the characters the locale actually uses\n * (`/doc/релизы/v8`) while the browser sends them percent-encoded, so without\n * this every non-ASCII localized URL misses its own rule and is treated as an\n * unlocalized path. `decodeURI` leaves the reserved escapes (`%2F`, `%3F`,\n * `%23`) encoded, which keeps the segmentation of the path intact, and a\n * malformed escape is handed back untouched rather than throwing.\n */\nconst decodePathname = (pathname: string): string => {\n try {\n return decodeURI(pathname);\n } catch {\n return pathname;\n }\n};\n\n/**\n * Re-encodes a decoded path before it is written back to `req.url`, leaving the\n * query string as it was received.\n *\n * The inverse of {@link decodePathname}: `encodeURI` escapes exactly what it\n * decoded, so characters that are legal in a path — `+` in\n * `/doc/intlayer_with_vite+react`, for instance — survive the round trip, and\n * an already-encoded path is left as is.\n */\nconst encodePathname = (path: string): string => {\n const queryIndex = path.indexOf('?');\n const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex);\n const search = queryIndex === -1 ? '' : path.slice(queryIndex);\n\n return `${encodeURI(pathname)}${search}`;\n};\n\n/**\n * Splits a raw request target (`req.url`) into its pathname and search string.\n *\n * Replaces the deprecated `url.parse`, and unlike `new URL(requestUrl, base)`\n * it never reinterprets the leading `//` of a request target as an authority,\n * so `//example.com/page` stays the pathname it was sent as. The fragment is\n * dropped first — a browser never sends one, but a hand-written request may.\n */\nconst splitRequestTarget = (\n requestUrl: string\n): { pathname: string; search: string } => {\n const hashIndex = requestUrl.indexOf('#');\n const target = hashIndex === -1 ? requestUrl : requestUrl.slice(0, hashIndex);\n const queryIndex = target.indexOf('?');\n\n return queryIndex === -1\n ? { pathname: target, search: '' }\n : {\n pathname: target.slice(0, queryIndex),\n search: target.slice(queryIndex),\n };\n};\n\n/**\n * A Node.js-compatible Connect middleware function.\n * Compatible with Vite dev/preview server, Node.js http, Express, and h3's\n * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.\n */\nexport type NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\nexport type IntlayerProxyHandlerOptions = {\n /**\n * The resolved Intlayer configuration driving locale detection and routing.\n */\n configuration: IntlayerConfig;\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Whether a development or preview server is serving the app.\n *\n * It only matters in the proxy's auto mode, where a dev server keeps locale\n * routing URL-driven by ignoring the stored locale as a redirect source.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * Creates the locale-routing middleware from an already-resolved configuration.\n *\n * This is the runtime core shared by every environment - dev server, preview\n * server and the production Nitro server. It deliberately receives the\n * configuration instead of loading it: loading it would pull\n * `@intlayer/config/node` - and therefore `esbuild` - into the server bundle,\n * which then fails to boot wherever `node_modules` is not shipped next to the\n * build output.\n *\n * @param options - The resolved configuration plus optional handler behaviour.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n *\n * @see `createIntlayerProxyHandler` for the public, configuration-loading entry point.\n */\nexport const createProxyHandler = (\n options: IntlayerProxyHandlerOptions\n): NodeMiddleware => {\n const {\n configuration: intlayerConfig,\n ignore,\n isDevServer = false,\n } = options;\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const {\n basePath = '',\n mode = ROUTING_MODE,\n rewrite,\n domains,\n enableProxy,\n } = routing;\n\n // In auto mode a dev/preview server ignores the stored locale when resolving\n // which locale a request maps to, so a lingering cookie cannot keep pulling\n // navigation to another locale while developing. Prefix redirects, locale\n // persistence and `Accept-Language` detection are unaffected.\n const canUseStorageLocale = isProxyStorageLocaleEnabled(\n resolveProxyMode(enableProxy),\n isDevServer\n );\n\n type RedirectCounter = { count: number; lastSeen: number };\n const redirectCounts = new Map<string, RedirectCounter>();\n const MAX_REDIRECTS = 10;\n const REDIRECT_TTL_MS = 2_000;\n\n // Derived flags from routing.mode\n const noPrefix =\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params');\n const prefixDefault =\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all'\n ) && mode === 'prefix-all';\n\n const rewriteRules =\n process.env.INTLAYER_ROUTING_REWRITE_RULES !== 'false'\n ? getRewriteRules(rewrite, 'url')\n : undefined;\n\n /**\n * Resolves the locale a localized (\"pretty\") path belongs to.\n *\n * A path such as `/contacto` only exists because a rewrite rule maps it to\n * the Spanish locale, so it declares its locale just as explicitly as an\n * `/es` prefix would. Without this, a visitor whose `Accept-Language` (or\n * stored locale) says `en` gets `/contacto` canonicalized against the\n * English rules only, which never matches — the proxy then treats it as an\n * unlocalized path and redirects to `/en/contacto`, a URL no locale owns.\n *\n * Only patterns that differ from their canonical form are considered: a rule\n * whose localized path equals the canonical one (`en: '/contact'`) carries no\n * locale signal, and matching on it would hijack the default locale's\n * unprefixed URLs.\n *\n * @param path - The request pathname, without any locale prefix.\n * @returns The locale declared by the path, or `undefined` when it declares none.\n */\n const getRewriteLocale = (path: string): Locale | undefined =>\n rewriteRules\n ? supportedLocales.find(\n (candidate) =>\n getCanonicalPath(path, candidate, rewriteRules) !== path\n )\n : undefined;\n\n /**\n * Extracts the hostname from a request's `Host` header, handling IPv6\n * literals (`[::1]:5173` → `::1`) that a plain `split(':')` would mangle.\n */\n const getRequestHostname = (req: IncomingMessage): string => {\n const host = req.headers.host ?? '';\n const ipv6Match = host.match(/^\\[([^\\]]+)\\]/);\n if (ipv6Match) return ipv6Match[1]!;\n return host.split(':')[0] ?? '';\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n *\n * Returns `undefined` when the stored locale is not allowed to drive locale\n * resolution (auto mode on a dev/preview server, or a prerender pass, whose\n * output must stay URL-driven), which makes every caller fall through to\n * `Accept-Language` detection and then the default locale.\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n if (!canUseStorageLocale || isPrerenderRequest(req)) return undefined;\n\n const locale = getLocaleFromStorageServer({\n getCookie: (name: string) => getCookie(name, req.headers.cookie),\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 (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params') ||\n mode !== 'search-params'\n )\n return search;\n\n const params = new URLSearchParams(search ?? '');\n\n params.set('locale', locale);\n\n return `?${params.toString()}`;\n };\n\n /**\n * Checks whether a pathname starts with the given locale as a full path\n * segment (`/fr` or `/fr/...`). A bare `startsWith('/fr')` would also match\n * unrelated paths like `/friends`, causing wrong prefix stripping and\n * self-redirect loops.\n */\n const hasLocaleSegmentPrefix = (pathname: string, locale: Locale): boolean =>\n pathname === `/${locale}` || pathname.startsWith(`/${locale}/`);\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n * e.g. if pathname is /en/some/page or /en, checks if \"en\" is in supportedLocales.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\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 * Persists the resolved locale onto the outgoing response as a cookie (and\n * header, per `routing.storage`). Only the cookie survives a client redirect,\n * so this is what carries an explicitly-selected locale across a\n * prefix-stripping redirect. Enabled cookie/header targets are resolved by\n * {@link setLocaleInStorageServer} from the config; disabled ones are no-ops.\n */\n const persistLocaleOnResponse = (\n res: ServerResponse<IncomingMessage>,\n locale: Locale\n ) => {\n setLocaleInStorageServer(locale, {\n setCookieStore: (name, value, attributes) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n if (attributes.path) parts.push(`Path=${attributes.path}`);\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (typeof attributes.expires === 'number')\n parts.push(`Expires=${new Date(attributes.expires).toUTCString()}`);\n if (attributes.secure) parts.push('Secure');\n if (attributes.httpOnly) parts.push('HttpOnly');\n if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`);\n\n const cookieString = parts.join('; ');\n const existing = res.getHeader('Set-Cookie');\n const cookies = Array.isArray(existing)\n ? existing.map(String)\n : existing !== undefined\n ? [String(existing)]\n : [];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n },\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n },\n });\n };\n\n /**\n * Identifies the client issuing a request, so redirect-loop tracking is\n * scoped per client. Without this, concurrent visitors hitting the same\n * legitimate redirect (e.g. `/` → `/fr/`) would share one counter and trip\n * the loop detector under normal production traffic.\n */\n const getClientKey = (req: IncomingMessage): string => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const forwardedIp = (\n Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor\n )\n ?.split(',')[0]\n ?.trim();\n\n return forwardedIp || req.socket?.remoteAddress || 'unknown';\n };\n\n /**\n * Strips any origin from a redirect target, keeping only path, search and\n * hash. Prevents open redirects: user-controlled paths like `//evil.com/x`\n * would otherwise be echoed verbatim into the `Location` header and be\n * interpreted by browsers as a protocol-relative cross-origin URL.\n */\n const toSameOriginUrl = (url: string): string => {\n try {\n const parsed = new URL(url, 'http://intlayer-internal');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return url;\n }\n };\n\n type RedirectOptions = {\n /** Human-readable reason logged when a redirect loop is detected. */\n reason?: string;\n /** The URL of the incoming request, used for redirect-loop tracking. */\n originalUrl?: string;\n /**\n * When provided, the locale is persisted (cookie/header, per config) on\n * the redirect response so the follow-up request resolves the same locale.\n */\n persistLocale?: Locale;\n /**\n * HTTP status for the redirect. Defaults to 302: locale redirects depend\n * on mutable state (cookie, Accept-Language) and must not be cached by\n * the browser as permanent. Domain-routing redirects pass 301 explicitly.\n */\n status?: number;\n /**\n * Allows a cross-origin `Location` target. Only the domain-routing\n * redirect sets this; all other targets are sanitized to same-origin.\n */\n allowCrossOrigin?: boolean;\n };\n\n /**\n * Writes a redirect response with the given new URL.\n */\n const redirectUrl = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n options?: RedirectOptions\n ) => {\n const {\n reason,\n originalUrl,\n persistLocale,\n status = 302,\n allowCrossOrigin = false,\n } = options ?? {};\n\n const targetUrl = allowCrossOrigin ? newUrl : toSameOriginUrl(newUrl);\n\n if (originalUrl) {\n if (originalUrl === targetUrl) {\n console.error('[REDIRECT LOOP DETECTED!]', { originalUrl, reason });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${originalUrl} redirects to itself`\n );\n }\n\n const now = Date.now();\n const trackingKey = `${getClientKey(req)}|${originalUrl} -> ${targetUrl}`;\n const previousEntry = redirectCounts.get(trackingKey);\n const count =\n previousEntry && now - previousEntry.lastSeen < REDIRECT_TTL_MS\n ? previousEntry.count + 1\n : 1;\n\n redirectCounts.set(trackingKey, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: targetUrl,\n reason,\n });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${count} redirects from ${originalUrl}`\n );\n }\n\n for (const [entryKey, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS)\n redirectCounts.delete(entryKey);\n }\n }\n\n // A prerendered response is stored as a static file and replayed to every\n // visitor, so persisting the locale on it would hand the locale of the\n // build machine to everyone hitting that page.\n if (persistLocale && !isPrerenderRequest(req)) {\n persistLocaleOnResponse(res, persistLocale);\n }\n\n res.writeHead(status, { Location: targetUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url.\n * Also sets the locale in the response/request headers via storage to mimic\n * Next.js's behaviour of propagating the detected locale downstream.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n const encodedUrl = encodePathname(newUrl);\n\n if (req.url !== encodedUrl) {\n req.url = encodedUrl;\n }\n if (locale) {\n setLocaleInStorageServer(locale, {\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n req.headers[name] = value;\n },\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 to avoid double-prefixing\n const pathWithoutPrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/' and has no trailing slash\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params')\n ) {\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 = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n let newPath = `${normalizedBasePath}${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 // Never emit a trailing slash (`/fr/` for the root path): the framework's\n // trailing-slash normalisation would redirect it back and forth with this\n // proxy, creating an infinite redirect loop.\n if (newPath.length > 1 && newPath.endsWith('/')) {\n newPath = newPath.slice(0, -1);\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 originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n const pathLocale = getPathLocale(originalPath);\n\n // Determine the best locale: prefer cookie/storage, fall back to Accept-Language detection\n let locale = storageLocale ?? defaultLocale;\n\n // Use localeDetector if no storage locale is available\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 // Guard against a stale/unsupported locale coming from storage or detection\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Without a prefix, a localized path is the only locale signal the URL\n // carries, so it takes precedence over storage / `Accept-Language`.\n if (!pathLocale) {\n locale = getRewriteLocale(originalPath) ?? locale;\n }\n\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n pathLocale,\n rewriteRules\n );\n\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n const redirectPath = search\n ? `${canonicalPath}${search}`\n : `${canonicalPath}${searchParams ?? ''}`;\n\n // Persist the explicitly-requested locale: stripping the prefix drops\n // the only locale signal from the URL, so without this the follow-up\n // request would fall back to cookie / Accept-Language detection and\n // could resolve a different locale.\n return redirectUrl(req, res, redirectPath, {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // In search-params mode, we need to redirect to add the locale search param\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\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 (existingLocale === locale) {\n // Rewrite internally — URL stays the same in the browser, but the framework\n // sees /[locale]/path so the [locale] route param is populated correctly.\n // getInternalPath collapses the root path to `/${locale}` — a trailing\n // slash (`/fr/`) would trigger the framework's trailing-slash\n // normalisation redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match — redirect to add/update it (URL changes in browser)\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n return redirectUrl(req, res, redirectPath, { originalUrl });\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n // so the framework can match the [locale] route param without exposing it in the URL.\n // getInternalPath collapses the root path to `/${locale}` — a trailing slash\n // (`/fr/`) would trigger the framework's trailing-slash normalisation\n // redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n\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 the browser\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic.\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n originalUrl,\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 originalUrl?: string;\n }) => {\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n });\n return;\n }\n\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * Detects a locale from storage / headers / default, then either redirects or rewrites.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n // Choose the best locale: cookie/storage → Accept-Language detection → defaultLocale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // If still invalid, fall back to defaultLocale\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // A localized path names its own locale, and the URL always outranks the\n // stored / negotiated one — otherwise /a-propos read as 'en' resolves to\n // no rule and gets redirected to the ownerless /en/a-propos.\n locale = getRewriteLocale(originalPath) ?? locale;\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), this resolves to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection.\n // /about + 'fr' → /a-propos\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n\n // Construct new path, preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, targetLocalizedPath, search);\n\n // If we always prefix default or if this is not the default locale,\n // do a redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(req, res, newPath, { originalUrl });\n }\n\n // If we do NOT prefix the default locale, pass through the canonical path unchanged.\n // Rewriting to `/${locale}${canonicalPath}` (e.g. /en/) causes TanStack Start to issue a\n // trailing-slash normalisation redirect (/en/ → /en), which the proxy then strips back to /,\n // creating an infinite redirect loop.\n // Because {-$locale} is an optional segment, the framework matches the un-prefixed URL with\n // locale=undefined and falls back to defaultLocale via `params.locale ?? defaultLocale`.\n // searchParams MUST be preserved here — dropping them causes the framework (e.g. TanStack Start) to\n // see a URL with no search params, trigger a validateSearch normalisation redirect to the prefixed URL\n // (e.g. /en?page=1&...), which the middleware then strips back to /?..., creating an infinite loop.\n rewriteUrl(req, res, `${canonicalPath}${searchParams}`, 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 originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n originalUrl?: string;\n }) => {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n\n // Identify the canonical path (internal path).\n // Ex: /a-propos (from URL) → /about (canonical)\n const canonicalPath = getCanonicalPath(rawPath, pathLocale, rewriteRules);\n\n // When rewrite rules are configured and the URL is already a valid localized pretty URL\n // (e.g. /fr/essais which maps to canonical /fr/tests), do NOT redirect to canonical.\n //\n // Why: the SPA router (Solid, React Router, Vue Router…) is expected to define routes using\n // the localized paths (e.g. <Route path=\"/essais\">) so the browser URL must stay as-is.\n // A 301 redirect to canonical would:\n // 1. Change the browser URL to the canonical form (/fr/tests)\n // 2. Break subsequent client-side navigation because <A> links produced by getLocalizedUrl\n // point back to the localized URL (/fr/essais) which then has no matching route.\n //\n // We set the locale header and call next() so the server serves the page at the pretty URL.\n if (canonicalPath !== rawPath) {\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n // The URL is in canonical form (e.g. /fr/about) while the locale owns a\n // different pretty URL for it (fr: '/a-propos'). Redirect so the visible\n // URL is the one the locale owns — the same one `getLocalizedUrl` emits\n // and the one the SPA router declares a route for. Skipped when the\n // default-locale prefix has to be stripped anyway: `handleDefaultLocaleRedirect`\n // resolves the localized path itself, so redirecting here first would\n // cost a second round trip.\n if (prefixDefault || pathLocale !== defaultLocale) {\n const { path: targetLocalizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n if (isRewritten && targetLocalizedPath !== rawPath) {\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n return redirectUrl(\n req,\n res,\n constructPath(pathLocale, targetLocalizedPath, search),\n { originalUrl }\n );\n }\n }\n\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\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 searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n searchParams: string;\n pathLocale: Locale;\n canonicalPath: string;\n originalUrl?: string;\n }) => {\n // If we don't prefix the default locale AND the path locale IS the default → strip the prefix\n if (!prefixDefault && pathLocale === defaultLocale) {\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n // Construct path without prefix\n const cleanBasePath = basePath.startsWith('/')\n ? basePath\n : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n let finalPath = targetLocalizedPath;\n if (finalPath.startsWith('/')) finalPath = finalPath.slice(1);\n\n const fullPath = `${normalizedBasePath}/${finalPath}`.replace(\n /\\/+/g,\n '/'\n );\n\n // Persist the explicitly-requested default locale. Stripping the prefix\n // (e.g. /es → /) drops the only locale signal from the URL, so without\n // this the follow-up request to the canonical path would fall back to\n // Accept-Language detection and could resolve a different locale.\n return redirectUrl(req, res, fullPath + (searchParams ?? ''), {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally.\n // Never emit a trailing slash (`/fr/` for the bare `/fr` URL): rewriting\n // `/fr` to `/fr/` makes the framework issue a trailing-slash normalisation\n // redirect back to `/fr`, which this proxy rewrites again — an infinite\n // redirect loop. `getInternalPath` collapses the root path to `/${locale}`.\n const internalUrl = getInternalPath(canonicalPath, pathLocale);\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return (req, res, next) => {\n // Parse original URL for path and query\n const { pathname, search } = splitRequestTarget(req.url ?? '/');\n // Decoded so the rewrite rules — written with the locale's own characters —\n // match; `rewriteUrl` re-encodes whatever is written back to `req.url`.\n const originalPath = decodePathname(pathname || '/');\n const searchParams = search;\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\n\n // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (ignore?.(req) ?? false) ||\n originalPath.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 originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = encodePathname(`${pathWithoutLocale}${searchParams}`);\n }\n return next();\n }\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 // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = getRequestHostname(req);\n const domainHost = getDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = getDomainOrigin(localeDomain);\n // Domain mapping is stable config, so a cacheable 301 is intended\n // here — unlike locale-detection redirects, which use 302.\n redirectUrl(req, res, `${targetOrigin}${rawPath}${searchParams}`, {\n reason: 'domain-routing',\n originalUrl,\n status: 301,\n allowCrossOrigin: true,\n });\n return;\n }\n }\n }\n\n // Domain routing: if the current hostname is exclusively mapped to one locale,\n // treat it as that locale without a URL prefix.\n // e.g. intlayer.zh/about → internally rewrite to /zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n !pathLocale\n ) {\n const reqHost = getRequestHostname(req);\n const domainLocale = getLocaleFromDomain(reqHost, domains);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n\n // Default locale without forced prefixing: pass the canonical path\n // through unchanged, mirroring handleMissingPathLocale. Rewriting `/`\n // to `/en/` makes TanStack Start issue a trailing-slash normalisation\n // redirect (/en/ → /en), whose prefix the proxy then strips back to /,\n // creating an infinite redirect loop.\n if (!prefixDefault && domainLocale === defaultLocale) {\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n `${canonicalPath}${searchParams}`,\n domainLocale\n );\n return next();\n }\n\n // Never emit a trailing slash (`/zh/`): the framework would redirect\n // it to `/zh`, which this proxy rewrites to `/zh/` again — a loop.\n const internalPath = getInternalPath(canonicalPath, domainLocale);\n\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n if (noPrefix) {\n handleNoPrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n handlePrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n };\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerProxyHandler.d.ts","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"mappings":";;;;;;;;KAyEY,kBACV,KAAK,iBACL,KAAK,eAAe,kBACpB;KAGU;;;;EAIV,eAAe;;;;;;;EAOf,UAAU,KAAK;;;;;;;;;EASf;;;;;;;;;;;;;;;;;cAkBW,qBAAkB,SACpB,gCACR"}
1
+ {"version":3,"file":"intlayerProxyHandler.d.ts","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"mappings":";;;;;;;;KAkIY,kBACV,KAAK,iBACL,KAAK,eAAe,kBACpB;KAGU;;;;EAIV,eAAe;;;;;;;EAOf,UAAU,KAAK;;;;;;;;;EASf;;;;;;;;;;;;;;;;;cAkBW,qBAAkB,SACpB,gCACR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-intlayer",
3
- "version": "9.4.0",
3
+ "version": "9.4.2",
4
4
  "private": false,
5
5
  "description": "A Vite plugin for seamless internationalization (i18n), providing locale detection, redirection, and environment-based configuration",
6
6
  "keywords": [
@@ -81,15 +81,15 @@
81
81
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
82
82
  },
83
83
  "dependencies": {
84
- "@intlayer/babel": "9.4.0",
85
- "@intlayer/config": "9.4.0",
86
- "@intlayer/core": "9.4.0",
87
- "@intlayer/dictionaries-entry": "9.4.0",
88
- "@intlayer/engine": "9.4.0",
89
- "@intlayer/types": "9.4.0"
84
+ "@intlayer/babel": "9.4.2",
85
+ "@intlayer/config": "9.4.2",
86
+ "@intlayer/core": "9.4.2",
87
+ "@intlayer/dictionaries-entry": "9.4.2",
88
+ "@intlayer/engine": "9.4.2",
89
+ "@intlayer/types": "9.4.2"
90
90
  },
91
91
  "devDependencies": {
92
- "@types/node": "26.3.0",
92
+ "@types/node": "26.4.1",
93
93
  "@utils/ts-config": "1.0.4",
94
94
  "@utils/ts-config-types": "1.0.4",
95
95
  "@utils/tsdown-config": "1.0.4",
@@ -100,8 +100,8 @@
100
100
  },
101
101
  "peerDependencies": {
102
102
  "@babel/core": ">=6.0.0",
103
- "@intlayer/svelte-compiler": "9.4.0",
104
- "@intlayer/vue-compiler": "9.4.0",
103
+ "@intlayer/svelte-compiler": "9.4.2",
104
+ "@intlayer/vue-compiler": "9.4.2",
105
105
  "h3": ">=1.0.0",
106
106
  "vite": ">=4.0.0"
107
107
  },