@intlayer/core 9.3.1 → 9.3.3

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.
Files changed (211) hide show
  1. package/dist/cjs/dictionaryManipulator/index.cjs +5 -1
  2. package/dist/cjs/dictionaryManipulator/plainDynamicLoader.cjs +71 -0
  3. package/dist/cjs/dictionaryManipulator/plainDynamicLoader.cjs.map +1 -0
  4. package/dist/cjs/dictionaryManipulator/preloadedDynamicLoader.cjs +29 -0
  5. package/dist/cjs/dictionaryManipulator/preloadedDynamicLoader.cjs.map +1 -0
  6. package/dist/cjs/dictionaryManipulator/qualifiedDynamicLoader.cjs +4 -10
  7. package/dist/cjs/dictionaryManipulator/qualifiedDynamicLoader.cjs.map +1 -1
  8. package/dist/cjs/index.cjs +13 -2
  9. package/dist/cjs/localization/comparePaths.cjs +1 -1
  10. package/dist/cjs/localization/comparePaths.cjs.map +1 -1
  11. package/dist/cjs/localization/domainUtils.cjs.map +1 -1
  12. package/dist/cjs/localization/getHTMLTextDir.cjs +11 -10
  13. package/dist/cjs/localization/getHTMLTextDir.cjs.map +1 -1
  14. package/dist/cjs/localization/getLocale.cjs.map +1 -1
  15. package/dist/cjs/localization/getLocaleFromPath.cjs +3 -3
  16. package/dist/cjs/localization/getLocaleFromPath.cjs.map +1 -1
  17. package/dist/cjs/localization/getLocalizedPath.cjs +74 -0
  18. package/dist/cjs/localization/getLocalizedPath.cjs.map +1 -0
  19. package/dist/cjs/localization/getLocalizedPath.test-d.cjs +83 -0
  20. package/dist/cjs/localization/getLocalizedPath.test-d.cjs.map +1 -0
  21. package/dist/cjs/localization/getLocalizedUrl.cjs +9 -25
  22. package/dist/cjs/localization/getLocalizedUrl.cjs.map +1 -1
  23. package/dist/cjs/localization/getLocalizedUrl.test-d.cjs +42 -0
  24. package/dist/cjs/localization/getLocalizedUrl.test-d.cjs.map +1 -0
  25. package/dist/cjs/localization/getPathWithoutLocale.cjs +2 -2
  26. package/dist/cjs/localization/getPathWithoutLocale.cjs.map +1 -1
  27. package/dist/cjs/localization/getPrefix.cjs +15 -1
  28. package/dist/cjs/localization/getPrefix.cjs.map +1 -1
  29. package/dist/cjs/localization/index.cjs +7 -1
  30. package/dist/cjs/localization/localeDetector.cjs +5 -3
  31. package/dist/cjs/localization/localeDetector.cjs.map +1 -1
  32. package/dist/cjs/localization/localeMapper.cjs.map +1 -1
  33. package/dist/cjs/localization/localeResolver.cjs.map +1 -1
  34. package/dist/cjs/localization/resolveBrowserLocale.cjs +63 -0
  35. package/dist/cjs/localization/resolveBrowserLocale.cjs.map +1 -0
  36. package/dist/cjs/localization/rewriteUtils.cjs +26 -14
  37. package/dist/cjs/localization/rewriteUtils.cjs.map +1 -1
  38. package/dist/cjs/localization/rewriteUtils.test-d.cjs +48 -0
  39. package/dist/cjs/localization/rewriteUtils.test-d.cjs.map +1 -0
  40. package/dist/cjs/markdown/compiler.cjs +2 -1
  41. package/dist/cjs/markdown/compiler.cjs.map +1 -1
  42. package/dist/cjs/markdown/constants.cjs +3 -0
  43. package/dist/cjs/markdown/constants.cjs.map +1 -1
  44. package/dist/cjs/markdown/index.cjs +1 -0
  45. package/dist/cjs/markdown/utils.cjs +23 -1
  46. package/dist/cjs/markdown/utils.cjs.map +1 -1
  47. package/dist/esm/dictionaryManipulator/index.mjs +2 -1
  48. package/dist/esm/dictionaryManipulator/plainDynamicLoader.mjs +70 -0
  49. package/dist/esm/dictionaryManipulator/plainDynamicLoader.mjs.map +1 -0
  50. package/dist/esm/dictionaryManipulator/preloadedDynamicLoader.mjs +27 -0
  51. package/dist/esm/dictionaryManipulator/preloadedDynamicLoader.mjs.map +1 -0
  52. package/dist/esm/dictionaryManipulator/qualifiedDynamicLoader.mjs +2 -8
  53. package/dist/esm/dictionaryManipulator/qualifiedDynamicLoader.mjs.map +1 -1
  54. package/dist/esm/index.mjs +7 -4
  55. package/dist/esm/localization/comparePaths.mjs +1 -1
  56. package/dist/esm/localization/comparePaths.mjs.map +1 -1
  57. package/dist/esm/localization/domainUtils.mjs.map +1 -1
  58. package/dist/esm/localization/getHTMLTextDir.mjs +11 -10
  59. package/dist/esm/localization/getHTMLTextDir.mjs.map +1 -1
  60. package/dist/esm/localization/getLocale.mjs.map +1 -1
  61. package/dist/esm/localization/getLocaleFromPath.mjs +4 -4
  62. package/dist/esm/localization/getLocaleFromPath.mjs.map +1 -1
  63. package/dist/esm/localization/getLocalizedPath.mjs +73 -0
  64. package/dist/esm/localization/getLocalizedPath.mjs.map +1 -0
  65. package/dist/esm/localization/getLocalizedPath.test-d.mjs +83 -0
  66. package/dist/esm/localization/getLocalizedPath.test-d.mjs.map +1 -0
  67. package/dist/esm/localization/getLocalizedUrl.mjs +10 -26
  68. package/dist/esm/localization/getLocalizedUrl.mjs.map +1 -1
  69. package/dist/esm/localization/getLocalizedUrl.test-d.mjs +42 -0
  70. package/dist/esm/localization/getLocalizedUrl.test-d.mjs.map +1 -0
  71. package/dist/esm/localization/getPathWithoutLocale.mjs +2 -2
  72. package/dist/esm/localization/getPathWithoutLocale.mjs.map +1 -1
  73. package/dist/esm/localization/getPrefix.mjs +15 -2
  74. package/dist/esm/localization/getPrefix.mjs.map +1 -1
  75. package/dist/esm/localization/index.mjs +5 -3
  76. package/dist/esm/localization/localeDetector.mjs +5 -3
  77. package/dist/esm/localization/localeDetector.mjs.map +1 -1
  78. package/dist/esm/localization/localeMapper.mjs.map +1 -1
  79. package/dist/esm/localization/localeResolver.mjs.map +1 -1
  80. package/dist/esm/localization/resolveBrowserLocale.mjs +61 -0
  81. package/dist/esm/localization/resolveBrowserLocale.mjs.map +1 -0
  82. package/dist/esm/localization/rewriteUtils.mjs +26 -14
  83. package/dist/esm/localization/rewriteUtils.mjs.map +1 -1
  84. package/dist/esm/localization/rewriteUtils.test-d.mjs +48 -0
  85. package/dist/esm/localization/rewriteUtils.test-d.mjs.map +1 -0
  86. package/dist/esm/markdown/compiler.mjs +2 -1
  87. package/dist/esm/markdown/compiler.mjs.map +1 -1
  88. package/dist/esm/markdown/constants.mjs +3 -1
  89. package/dist/esm/markdown/constants.mjs.map +1 -1
  90. package/dist/esm/markdown/index.mjs +2 -2
  91. package/dist/esm/markdown/utils.mjs +24 -2
  92. package/dist/esm/markdown/utils.mjs.map +1 -1
  93. package/dist/types/@intlayer/core/dist/types/formatters/compact.d.ts +1 -0
  94. package/dist/types/@intlayer/core/dist/types/formatters/currency.d.ts +1 -0
  95. package/dist/types/@intlayer/core/dist/types/formatters/date.d.ts +1 -0
  96. package/dist/types/@intlayer/core/dist/types/formatters/index.d.ts +9 -0
  97. package/dist/types/@intlayer/core/dist/types/formatters/list.d.ts +1 -0
  98. package/dist/types/@intlayer/core/dist/types/formatters/number.d.ts +1 -0
  99. package/dist/types/@intlayer/core/dist/types/formatters/percentage.d.ts +1 -0
  100. package/dist/types/@intlayer/core/dist/types/formatters/relativeTime.d.ts +1 -0
  101. package/dist/types/@intlayer/core/dist/types/formatters/units.d.ts +1 -0
  102. package/dist/types/@intlayer/core/dist/types/interpreter/getCondition.d.ts +2 -0
  103. package/dist/types/@intlayer/core/dist/types/interpreter/getContent/deepTransform.d.ts +1 -0
  104. package/dist/types/@intlayer/core/dist/types/interpreter/getContent/getContent.d.ts +3 -0
  105. package/dist/types/@intlayer/core/dist/types/interpreter/getContent/index.d.ts +3 -0
  106. package/dist/types/@intlayer/core/dist/types/interpreter/getContent/plugins.d.ts +7 -0
  107. package/dist/types/@intlayer/core/dist/types/interpreter/getDictionary.d.ts +4 -0
  108. package/dist/types/@intlayer/core/dist/types/interpreter/getEnumeration.d.ts +2 -0
  109. package/dist/types/@intlayer/core/dist/types/interpreter/getGender.d.ts +2 -0
  110. package/dist/types/@intlayer/core/dist/types/interpreter/getInsertion.d.ts +2 -0
  111. package/dist/types/@intlayer/core/dist/types/interpreter/getIntlayer.d.ts +4 -0
  112. package/dist/types/@intlayer/core/dist/types/interpreter/getNesting.d.ts +6 -0
  113. package/dist/types/@intlayer/core/dist/types/interpreter/getPlural.d.ts +3 -0
  114. package/dist/types/@intlayer/core/dist/types/interpreter/getSelect.d.ts +2 -0
  115. package/dist/types/@intlayer/core/dist/types/interpreter/getTranslation.d.ts +1 -0
  116. package/dist/types/@intlayer/core/dist/types/interpreter/index.d.ts +15 -0
  117. package/dist/types/@intlayer/core/dist/types/interpreter/interpolableNode.d.ts +2 -0
  118. package/dist/types/@intlayer/core/dist/types/localization/comparePaths.d.ts +1 -0
  119. package/dist/types/@intlayer/core/dist/types/localization/domainUtils.d.ts +2 -0
  120. package/dist/types/@intlayer/core/dist/types/localization/generateSitemap.d.ts +2 -0
  121. package/dist/types/@intlayer/core/dist/types/localization/getBrowserLocale.d.ts +1 -0
  122. package/dist/types/@intlayer/core/dist/types/localization/getHTMLTextDir.d.ts +1 -0
  123. package/dist/types/@intlayer/core/dist/types/localization/getLocale.d.ts +1 -0
  124. package/dist/types/@intlayer/core/dist/types/localization/getLocaleFromPath.d.ts +2 -0
  125. package/dist/types/@intlayer/core/dist/types/localization/getLocaleLang.d.ts +1 -0
  126. package/dist/types/@intlayer/core/dist/types/localization/getLocaleName.d.ts +1 -0
  127. package/dist/types/@intlayer/core/dist/types/localization/getLocalizedPath.d.ts +2 -0
  128. package/dist/types/@intlayer/core/dist/types/localization/getLocalizedUrl.d.ts +2 -0
  129. package/dist/types/@intlayer/core/dist/types/localization/getMultilingualUrls.d.ts +2 -0
  130. package/dist/types/@intlayer/core/dist/types/localization/getPathWithoutLocale.d.ts +1 -0
  131. package/dist/types/@intlayer/core/dist/types/localization/getPrefix.d.ts +2 -0
  132. package/dist/types/@intlayer/core/dist/types/localization/index.d.ts +20 -0
  133. package/dist/types/@intlayer/core/dist/types/localization/localeDetector.d.ts +2 -0
  134. package/dist/types/@intlayer/core/dist/types/localization/localeMapper.d.ts +1 -0
  135. package/dist/types/@intlayer/core/dist/types/localization/localeResolver.d.ts +1 -0
  136. package/dist/types/@intlayer/core/dist/types/localization/resolveBrowserLocale.d.ts +2 -0
  137. package/dist/types/@intlayer/core/dist/types/localization/rewriteUtils.d.ts +2 -0
  138. package/dist/types/@intlayer/core/dist/types/localization/validatePrefix.d.ts +2 -0
  139. package/dist/types/@intlayer/core/dist/types/markdown/index.d.ts +2 -0
  140. package/dist/types/@intlayer/core/dist/types/transpiler/condition/condition.d.ts +1 -0
  141. package/dist/types/@intlayer/core/dist/types/transpiler/condition/index.d.ts +1 -0
  142. package/dist/types/@intlayer/core/dist/types/transpiler/enumeration/enumeration.d.ts +1 -0
  143. package/dist/types/@intlayer/core/dist/types/transpiler/enumeration/index.d.ts +1 -0
  144. package/dist/types/@intlayer/core/dist/types/transpiler/file/file.d.ts +1 -0
  145. package/dist/types/@intlayer/core/dist/types/transpiler/file/index.d.ts +1 -0
  146. package/dist/types/@intlayer/core/dist/types/transpiler/gender/gender.d.ts +1 -0
  147. package/dist/types/@intlayer/core/dist/types/transpiler/gender/index.d.ts +1 -0
  148. package/dist/types/@intlayer/core/dist/types/transpiler/html/html.d.ts +1 -0
  149. package/dist/types/@intlayer/core/dist/types/transpiler/html/index.d.ts +1 -0
  150. package/dist/types/@intlayer/core/dist/types/transpiler/index.d.ts +22 -0
  151. package/dist/types/@intlayer/core/dist/types/transpiler/insertion/index.d.ts +1 -0
  152. package/dist/types/@intlayer/core/dist/types/transpiler/insertion/insertion.d.ts +1 -0
  153. package/dist/types/@intlayer/core/dist/types/transpiler/markdown/index.d.ts +1 -0
  154. package/dist/types/@intlayer/core/dist/types/transpiler/markdown/markdown.d.ts +1 -0
  155. package/dist/types/@intlayer/core/dist/types/transpiler/nesting/index.d.ts +1 -0
  156. package/dist/types/@intlayer/core/dist/types/transpiler/nesting/nesting.d.ts +4 -0
  157. package/dist/types/@intlayer/core/dist/types/transpiler/plural/index.d.ts +1 -0
  158. package/dist/types/@intlayer/core/dist/types/transpiler/plural/plural.d.ts +1 -0
  159. package/dist/types/@intlayer/core/dist/types/transpiler/select/index.d.ts +1 -0
  160. package/dist/types/@intlayer/core/dist/types/transpiler/select/select.d.ts +1 -0
  161. package/dist/types/@intlayer/core/dist/types/transpiler/translation/index.d.ts +1 -0
  162. package/dist/types/@intlayer/core/dist/types/transpiler/translation/translation.d.ts +2 -0
  163. package/dist/types/@intlayer/core/dist/types/utils/index.d.ts +3 -0
  164. package/dist/types/@intlayer/core/dist/types/utils/intl.d.ts +1 -0
  165. package/dist/types/@intlayer/core/dist/types/utils/isSameKeyPath.d.ts +1 -0
  166. package/dist/types/@intlayer/core/dist/types/utils/localeStorage.d.ts +2 -0
  167. package/dist/types/dictionaryManipulator/index.d.ts +2 -1
  168. package/dist/types/dictionaryManipulator/plainDynamicLoader.d.ts +50 -0
  169. package/dist/types/dictionaryManipulator/plainDynamicLoader.d.ts.map +1 -0
  170. package/dist/types/dictionaryManipulator/preloadedDynamicLoader.d.ts +38 -0
  171. package/dist/types/dictionaryManipulator/preloadedDynamicLoader.d.ts.map +1 -0
  172. package/dist/types/dictionaryManipulator/qualifiedDynamicLoader.d.ts +1 -7
  173. package/dist/types/dictionaryManipulator/qualifiedDynamicLoader.d.ts.map +1 -1
  174. package/dist/types/index.d.ts +8 -5
  175. package/dist/types/interpreter/getPlural.d.ts +1 -1
  176. package/dist/types/intlayer/dist/types/index.d.ts +14 -0
  177. package/dist/types/localization/domainUtils.d.ts +2 -3
  178. package/dist/types/localization/domainUtils.d.ts.map +1 -1
  179. package/dist/types/localization/getHTMLTextDir.d.ts.map +1 -1
  180. package/dist/types/localization/getLocale.d.ts +2 -2
  181. package/dist/types/localization/getLocale.d.ts.map +1 -1
  182. package/dist/types/localization/getLocaleFromPath.d.ts +2 -2
  183. package/dist/types/localization/getLocaleFromPath.d.ts.map +1 -1
  184. package/dist/types/localization/getLocalizedPath.d.ts +47 -0
  185. package/dist/types/localization/getLocalizedPath.d.ts.map +1 -0
  186. package/dist/types/localization/getLocalizedPath.test-d.d.ts +1 -0
  187. package/dist/types/localization/getLocalizedUrl.d.ts +5 -1
  188. package/dist/types/localization/getLocalizedUrl.d.ts.map +1 -1
  189. package/dist/types/localization/getLocalizedUrl.test-d.d.ts +1 -0
  190. package/dist/types/localization/getPrefix.d.ts +20 -8
  191. package/dist/types/localization/getPrefix.d.ts.map +1 -1
  192. package/dist/types/localization/index.d.ts +5 -3
  193. package/dist/types/localization/localeDetector.d.ts +2 -1
  194. package/dist/types/localization/localeDetector.d.ts.map +1 -1
  195. package/dist/types/localization/localeMapper.d.ts +4 -5
  196. package/dist/types/localization/localeMapper.d.ts.map +1 -1
  197. package/dist/types/localization/localeResolver.d.ts +2 -3
  198. package/dist/types/localization/localeResolver.d.ts.map +1 -1
  199. package/dist/types/localization/resolveBrowserLocale.d.ts +35 -0
  200. package/dist/types/localization/resolveBrowserLocale.d.ts.map +1 -0
  201. package/dist/types/localization/rewriteUtils.d.ts +51 -11
  202. package/dist/types/localization/rewriteUtils.d.ts.map +1 -1
  203. package/dist/types/localization/rewriteUtils.test-d.d.ts +1 -0
  204. package/dist/types/markdown/compiler.d.ts +12 -1
  205. package/dist/types/markdown/compiler.d.ts.map +1 -1
  206. package/dist/types/markdown/constants.d.ts +3 -1
  207. package/dist/types/markdown/constants.d.ts.map +1 -1
  208. package/dist/types/markdown/index.d.ts +3 -3
  209. package/dist/types/markdown/utils.d.ts +6 -0
  210. package/dist/types/markdown/utils.d.ts.map +1 -1
  211. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"domainUtils.mjs","names":[],"sources":["../../../src/localization/domainUtils.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport type { RoutingConfig } from '@intlayer/types/config';\nimport type { LocalesValues } from '@intlayer/types/module_augmentation';\n\n/**\n * The locale → domain map configured under `routing.domains`\n * (e.g. `{ en: 'intlayer.org', zh: 'intlayer.cn' }`).\n */\nexport type LocaleDomainMap = RoutingConfig['domains'];\n\n/**\n * Strips the protocol from a domain string and returns only the hostname.\n *\n * Example:\n *\n * ```ts\n * getDomainHostname('https://intlayer.cn') // 'intlayer.cn'\n * getDomainHostname('intlayer.cn') // 'intlayer.cn'\n * ```\n *\n * @param domain - A domain value from `routing.domains`, with or without protocol.\n * @returns The bare hostname.\n */\nexport const getDomainHostname = (domain: string): string => {\n try {\n return /^https?:\\/\\//.test(domain) ? new URL(domain).hostname : domain;\n } catch {\n return domain;\n }\n};\n\n/**\n * Returns the absolute origin for a domain value, prepending `https://`\n * when no protocol is present.\n *\n * Example:\n *\n * ```ts\n * getDomainOrigin('intlayer.cn') // 'https://intlayer.cn'\n * getDomainOrigin('http://intlayer.cn') // 'http://intlayer.cn'\n * ```\n *\n * @param domain - A domain value from `routing.domains`, with or without protocol.\n * @returns The origin usable as a URL base.\n */\nexport const getDomainOrigin = (domain: string): string =>\n /^https?:\\/\\//.test(domain) ? domain : `https://${domain}`;\n\n/**\n * Returns the locale exclusively mapped to a given hostname via `routing.domains`,\n * or `undefined` when zero or more than one locale share that hostname.\n *\n * Example: with `domains = { zh: 'intlayer.cn', en: 'intlayer.org', fr: 'intlayer.org' }`\n *\n * ```ts\n * getLocaleFromDomain('intlayer.cn', domains) // 'zh'\n * getLocaleFromDomain('intlayer.org', domains) // undefined (en and fr share it)\n * getLocaleFromDomain('example.com', domains) // undefined (not mapped)\n * ```\n *\n * @param hostname - The bare hostname of the incoming request.\n * @param domains - The configured locale → domain map.\n * @returns The exclusively mapped locale, or `undefined`.\n */\nexport const getLocaleFromDomain = (\n hostname: string,\n domains: LocaleDomainMap\n): Locale | undefined => {\n if (!domains) return undefined;\n\n const matchingLocales = Object.entries(domains).filter(\n ([, domain]) =>\n typeof domain === 'string' && getDomainHostname(domain) === hostname\n );\n\n return matchingLocales.length === 1\n ? (matchingLocales[0]?.[0] as Locale)\n : undefined;\n};\n\n/**\n * Checks whether a locale is the only locale mapped to its configured domain.\n * When true, the domain alone identifies the locale and no URL prefix is needed.\n * Hostnames are compared after protocol stripping, so\n * `{ en: 'https://intlayer.org', fr: 'intlayer.org' }` counts as a shared domain.\n *\n * @param locale - The locale to check.\n * @param domains - The configured locale → domain map.\n * @returns `true` when the locale has a domain shared with no other locale.\n */\nexport const isLocaleExclusiveOnDomain = (\n locale: LocalesValues,\n domains: LocaleDomainMap\n): boolean => {\n const localeDomain = domains?.[locale];\n\n if (!localeDomain) return false;\n\n const localeHostname = getDomainHostname(localeDomain);\n\n const localesOnSameDomain = Object.values(domains).filter(\n (domain) =>\n typeof domain === 'string' && getDomainHostname(domain) === localeHostname\n ).length;\n\n return localesOnSameDomain === 1;\n};\n"],"mappings":";;;;;;;;;;;;;;AAuBA,MAAa,qBAAqB,WAA2B;CAC3D,IAAI;EACF,OAAO,eAAe,KAAK,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,CAAC,WAAW;CAClE,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;AAgBA,MAAa,mBAAmB,WAC9B,eAAe,KAAK,MAAM,IAAI,SAAS,WAAW;;;;;;;;;;;;;;;;;AAkBpD,MAAa,uBACX,UACA,YACuB;CACvB,IAAI,CAAC,SAAS,OAAO;CAErB,MAAM,kBAAkB,OAAO,QAAQ,OAAO,CAAC,CAAC,QAC7C,GAAG,YACF,OAAO,WAAW,YAAY,kBAAkB,MAAM,MAAM,QAChE;CAEA,OAAO,gBAAgB,WAAW,IAC7B,gBAAgB,EAAE,GAAG,KACtB;AACN;;;;;;;;;;;AAYA,MAAa,6BACX,QACA,YACY;CACZ,MAAM,eAAe,UAAU;CAE/B,IAAI,CAAC,cAAc,OAAO;CAE1B,MAAM,iBAAiB,kBAAkB,YAAY;CAOrD,OAL4B,OAAO,OAAO,OAAO,CAAC,CAAC,QAChD,WACC,OAAO,WAAW,YAAY,kBAAkB,MAAM,MAAM,cAChE,CAAC,CAAC,WAE6B;AACjC"}
1
+ {"version":3,"file":"domainUtils.mjs","names":[],"sources":["../../../src/localization/domainUtils.ts"],"sourcesContent":["import type { RoutingConfig } from '@intlayer/types/config';\nimport type {\n DeclaredLocales,\n LocalesValues,\n} from '@intlayer/types/module_augmentation';\n\n/**\n * The locale → domain map configured under `routing.domains`\n * (e.g. `{ en: 'intlayer.org', zh: 'intlayer.cn' }`).\n */\nexport type LocaleDomainMap = RoutingConfig['domains'];\n\n/**\n * Strips the protocol from a domain string and returns only the hostname.\n *\n * Example:\n *\n * ```ts\n * getDomainHostname('https://intlayer.cn') // 'intlayer.cn'\n * getDomainHostname('intlayer.cn') // 'intlayer.cn'\n * ```\n *\n * @param domain - A domain value from `routing.domains`, with or without protocol.\n * @returns The bare hostname.\n */\nexport const getDomainHostname = (domain: string): string => {\n try {\n return /^https?:\\/\\//.test(domain) ? new URL(domain).hostname : domain;\n } catch {\n return domain;\n }\n};\n\n/**\n * Returns the absolute origin for a domain value, prepending `https://`\n * when no protocol is present.\n *\n * Example:\n *\n * ```ts\n * getDomainOrigin('intlayer.cn') // 'https://intlayer.cn'\n * getDomainOrigin('http://intlayer.cn') // 'http://intlayer.cn'\n * ```\n *\n * @param domain - A domain value from `routing.domains`, with or without protocol.\n * @returns The origin usable as a URL base.\n */\nexport const getDomainOrigin = (domain: string): string =>\n /^https?:\\/\\//.test(domain) ? domain : `https://${domain}`;\n\n/**\n * Returns the locale exclusively mapped to a given hostname via `routing.domains`,\n * or `undefined` when zero or more than one locale share that hostname.\n *\n * Example: with `domains = { zh: 'intlayer.cn', en: 'intlayer.org', fr: 'intlayer.org' }`\n *\n * ```ts\n * getLocaleFromDomain('intlayer.cn', domains) // 'zh'\n * getLocaleFromDomain('intlayer.org', domains) // undefined (en and fr share it)\n * getLocaleFromDomain('example.com', domains) // undefined (not mapped)\n * ```\n *\n * @param hostname - The bare hostname of the incoming request.\n * @param domains - The configured locale → domain map.\n * @returns The exclusively mapped locale, or `undefined`.\n */\nexport const getLocaleFromDomain = (\n hostname: string,\n domains: LocaleDomainMap\n): DeclaredLocales | undefined => {\n if (!domains) return undefined;\n\n const matchingLocales = Object.entries(domains).filter(\n ([, domain]) =>\n typeof domain === 'string' && getDomainHostname(domain) === hostname\n );\n\n return matchingLocales.length === 1\n ? (matchingLocales[0]?.[0] as DeclaredLocales)\n : undefined;\n};\n\n/**\n * Checks whether a locale is the only locale mapped to its configured domain.\n * When true, the domain alone identifies the locale and no URL prefix is needed.\n * Hostnames are compared after protocol stripping, so\n * `{ en: 'https://intlayer.org', fr: 'intlayer.org' }` counts as a shared domain.\n *\n * @param locale - The locale to check.\n * @param domains - The configured locale → domain map.\n * @returns `true` when the locale has a domain shared with no other locale.\n */\nexport const isLocaleExclusiveOnDomain = (\n locale: LocalesValues,\n domains: LocaleDomainMap\n): boolean => {\n const localeDomain = domains?.[locale];\n\n if (!localeDomain) return false;\n\n const localeHostname = getDomainHostname(localeDomain);\n\n const localesOnSameDomain = Object.values(domains).filter(\n (domain) =>\n typeof domain === 'string' && getDomainHostname(domain) === localeHostname\n ).length;\n\n return localesOnSameDomain === 1;\n};\n"],"mappings":";;;;;;;;;;;;;;AAyBA,MAAa,qBAAqB,WAA2B;CAC3D,IAAI;EACF,OAAO,eAAe,KAAK,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,CAAC,WAAW;CAClE,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;AAgBA,MAAa,mBAAmB,WAC9B,eAAe,KAAK,MAAM,IAAI,SAAS,WAAW;;;;;;;;;;;;;;;;;AAkBpD,MAAa,uBACX,UACA,YACgC;CAChC,IAAI,CAAC,SAAS,OAAO;CAErB,MAAM,kBAAkB,OAAO,QAAQ,OAAO,CAAC,CAAC,QAC7C,GAAG,YACF,OAAO,WAAW,YAAY,kBAAkB,MAAM,MAAM,QAChE;CAEA,OAAO,gBAAgB,WAAW,IAC7B,gBAAgB,EAAE,GAAG,KACtB;AACN;;;;;;;;;;;AAYA,MAAa,6BACX,QACA,YACY;CACZ,MAAM,eAAe,UAAU;CAE/B,IAAI,CAAC,cAAc,OAAO;CAE1B,MAAM,iBAAiB,kBAAkB,YAAY;CAOrD,OAL4B,OAAO,OAAO,OAAO,CAAC,CAAC,QAChD,WACC,OAAO,WAAW,YAAY,kBAAkB,MAAM,MAAM,cAChE,CAAC,CAAC,WAE6B;AACjC"}
@@ -1,4 +1,14 @@
1
1
  //#region src/localization/getHTMLTextDir.ts
2
+ const rtlScripts = [
3
+ "Arab",
4
+ "Hebr",
5
+ "Thaa",
6
+ "Syrc",
7
+ "Mand",
8
+ "Adlm",
9
+ "Rohg",
10
+ "Nkoo"
11
+ ];
2
12
  /**
3
13
  * Returns the text direction of the given locale.
4
14
  *
@@ -19,16 +29,7 @@ const getHTMLTextDir = (locale) => {
19
29
  if ("getTextInfo" in localeInfo) return localeInfo.getTextInfo().direction;
20
30
  if ("textInfo" in localeInfo) return localeInfo.textInfo.direction;
21
31
  const maximized = localeInfo.maximize();
22
- return [
23
- "Arab",
24
- "Hebr",
25
- "Thaa",
26
- "Syrc",
27
- "Mand",
28
- "Adlm",
29
- "Rohg",
30
- "Nkoo"
31
- ].includes(maximized.script ?? "") ? "rtl" : "ltr";
32
+ return rtlScripts.includes(maximized.script ?? "") ? "rtl" : "ltr";
32
33
  } catch {
33
34
  return "ltr";
34
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"getHTMLTextDir.mjs","names":[],"sources":["../../../src/localization/getHTMLTextDir.ts"],"sourcesContent":["import type { LocalesValues } from '@intlayer/types/module_augmentation';\n\ntype Dir = 'ltr' | 'rtl' | 'auto';\n\n/**\n * Returns the text direction of the given locale.\n *\n * Example:\n *\n * getHTMLTextDir('en-US') // 'ltr'\n * getHTMLTextDir('en') // 'ltr'\n * getHTMLTextDir('fr-CA') // 'ltr'\n * getHTMLTextDir('fr') // 'ltr'\n *\n * @param locale The locale to get the text direction for.\n * @returns The text direction of the given locale.\n */\nexport const getHTMLTextDir = (locale?: LocalesValues): Dir => {\n if (!locale) return 'ltr';\n\n try {\n const localeInfo = new Intl.Locale(locale);\n\n // Check for the 'textInfo' property (part of the Intl Enumeration API)\n // Most modern browsers support 'direction' via 'getTextInfo()' or 'textInfo'\n if ('getTextInfo' in localeInfo) {\n return (localeInfo as any).getTextInfo().direction as Dir;\n }\n\n // Fallback for environments supporting 'textInfo' property\n if ('textInfo' in localeInfo) {\n return (localeInfo as any).textInfo.direction as Dir;\n }\n\n // Manual fallback for older environments using script detection\n const maximized = localeInfo.maximize();\n const rtlScripts = [\n 'Arab',\n 'Hebr',\n 'Thaa',\n 'Syrc',\n 'Mand',\n 'Adlm',\n 'Rohg',\n 'Nkoo',\n ];\n\n return rtlScripts.includes(maximized.script ?? '') ? 'rtl' : 'ltr';\n } catch {\n return 'ltr';\n }\n};\n"],"mappings":";;;;;;;;;;;;;;AAiBA,MAAa,kBAAkB,WAAgC;CAC7D,IAAI,CAAC,QAAQ,OAAO;CAEpB,IAAI;EACF,MAAM,aAAa,IAAI,KAAK,OAAO,MAAM;EAIzC,IAAI,iBAAiB,YACnB,OAAQ,WAAmB,YAAY,CAAC,CAAC;EAI3C,IAAI,cAAc,YAChB,OAAQ,WAAmB,SAAS;EAItC,MAAM,YAAY,WAAW,SAAS;EAYtC,OAAO;GAVL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAGc,CAAC,CAAC,SAAS,UAAU,UAAU,EAAE,IAAI,QAAQ;CAC/D,QAAQ;EACN,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"getHTMLTextDir.mjs","names":[],"sources":["../../../src/localization/getHTMLTextDir.ts"],"sourcesContent":["import type { LocalesValues } from '@intlayer/types/module_augmentation';\n\ntype Dir = 'ltr' | 'rtl' | 'auto';\n\nconst rtlScripts = [\n 'Arab',\n 'Hebr',\n 'Thaa',\n 'Syrc',\n 'Mand',\n 'Adlm',\n 'Rohg',\n 'Nkoo',\n];\n\n/**\n * Returns the text direction of the given locale.\n *\n * Example:\n *\n * getHTMLTextDir('en-US') // 'ltr'\n * getHTMLTextDir('en') // 'ltr'\n * getHTMLTextDir('fr-CA') // 'ltr'\n * getHTMLTextDir('fr') // 'ltr'\n *\n * @param locale The locale to get the text direction for.\n * @returns The text direction of the given locale.\n */\nexport const getHTMLTextDir = (locale?: LocalesValues): Dir => {\n if (!locale) return 'ltr';\n\n try {\n const localeInfo = new Intl.Locale(locale);\n\n // Check for the 'textInfo' property (part of the Intl Enumeration API)\n // Most modern browsers support 'direction' via 'getTextInfo()' or 'textInfo'\n if ('getTextInfo' in localeInfo) {\n return (localeInfo as any).getTextInfo().direction as Dir;\n }\n\n // Fallback for environments supporting 'textInfo' property\n if ('textInfo' in localeInfo) {\n return (localeInfo as any).textInfo.direction as Dir;\n }\n\n // Manual fallback for older environments using script detection\n const maximized = (localeInfo as Intl.Locale).maximize();\n\n return rtlScripts.includes(maximized.script ?? '') ? 'rtl' : 'ltr';\n } catch {\n return 'ltr';\n }\n};\n"],"mappings":";AAIA,MAAM,aAAa;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;AAeA,MAAa,kBAAkB,WAAgC;CAC7D,IAAI,CAAC,QAAQ,OAAO;CAEpB,IAAI;EACF,MAAM,aAAa,IAAI,KAAK,OAAO,MAAM;EAIzC,IAAI,iBAAiB,YACnB,OAAQ,WAAmB,YAAY,CAAC,CAAC;EAI3C,IAAI,cAAc,YAChB,OAAQ,WAAmB,SAAS;EAItC,MAAM,YAAa,WAA2B,SAAS;EAEvD,OAAO,WAAW,SAAS,UAAU,UAAU,EAAE,IAAI,QAAQ;CAC/D,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"getLocale.mjs","names":[],"sources":["../../../src/localization/getLocale.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport { DEFAULT_LOCALE, LOCALES } from '@intlayer/config/defaultValues';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { getLocaleFromStorageServer } from '../utils/localeStorage';\nimport { getPreferredLanguages } from './localeDetector';\nimport { localeResolver } from './localeResolver';\n\nexport type RequestContext = {\n getHeader?: (name: string) => string | null | undefined;\n getCookie?: (name: string) => string | null | undefined;\n};\n\nexport const getLocale = async (ctx: RequestContext = {}): Promise<Locale> => {\n const defaultLocale = internationalization?.defaultLocale ?? DEFAULT_LOCALE;\n const availableLocales = internationalization?.locales ?? LOCALES;\n\n // Try locale from storage (cookie or header)\n const storedLocale = getLocaleFromStorageServer({\n getCookie: ctx.getCookie,\n getHeader: ctx.getHeader,\n });\n\n if (storedLocale) {\n return storedLocale;\n }\n\n // Fallback to Accept-Language negotiation\n const acceptLanguageHeader = ctx.getHeader?.('accept-language');\n\n if (!acceptLanguageHeader) {\n return defaultLocale;\n }\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales\n );\n\n const userFallbackLocale = localeResolver(\n preferredLocaleStrings,\n availableLocales,\n defaultLocale\n );\n\n if (userFallbackLocale) {\n return userFallbackLocale;\n }\n\n // Default locale\n return defaultLocale;\n};\n"],"mappings":";;;;;;;AAYA,MAAa,YAAY,OAAO,MAAsB,CAAC,MAAuB;CAC5E,MAAM,gBAAgB,sBAAsB,iBAAiB;CAC7D,MAAM,mBAAmB,sBAAsB,WAAW;CAG1D,MAAM,eAAe,2BAA2B;EAC9C,WAAW,IAAI;EACf,WAAW,IAAI;CACjB,CAAC;CAED,IAAI,cACF,OAAO;CAIT,MAAM,uBAAuB,IAAI,YAAY,iBAAiB;CAE9D,IAAI,CAAC,sBACH,OAAO;CAGT,MAAM,yBAAyB,sBAC7B,sBACA,gBACF;CAEA,MAAM,qBAAqB,eACzB,wBACA,kBACA,aACF;CAEA,IAAI,oBACF,OAAO;CAIT,OAAO;AACT"}
1
+ {"version":3,"file":"getLocale.mjs","names":[],"sources":["../../../src/localization/getLocale.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport { DEFAULT_LOCALE, LOCALES } from '@intlayer/config/defaultValues';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport { getLocaleFromStorageServer } from '../utils/localeStorage';\nimport { getPreferredLanguages } from './localeDetector';\nimport { localeResolver } from './localeResolver';\n\nexport type RequestContext = {\n getHeader?: (name: string) => string | null | undefined;\n getCookie?: (name: string) => string | null | undefined;\n};\n\nexport const getLocale = async (\n ctx: RequestContext = {}\n): Promise<DeclaredLocales> => {\n const defaultLocale = internationalization?.defaultLocale ?? DEFAULT_LOCALE;\n const availableLocales = internationalization?.locales ?? LOCALES;\n\n // Try locale from storage (cookie or header)\n const storedLocale = getLocaleFromStorageServer({\n getCookie: ctx.getCookie,\n getHeader: ctx.getHeader,\n });\n\n if (storedLocale) {\n return storedLocale;\n }\n\n // Fallback to Accept-Language negotiation\n const acceptLanguageHeader = ctx.getHeader?.('accept-language');\n\n if (!acceptLanguageHeader) {\n return defaultLocale;\n }\n\n const preferredLocaleStrings = getPreferredLanguages(\n acceptLanguageHeader,\n availableLocales\n );\n\n const userFallbackLocale = localeResolver(\n preferredLocaleStrings,\n availableLocales,\n defaultLocale\n );\n\n if (userFallbackLocale) {\n return userFallbackLocale;\n }\n\n // Default locale\n return defaultLocale;\n};\n"],"mappings":";;;;;;;AAYA,MAAa,YAAY,OACvB,MAAsB,CAAC,MACM;CAC7B,MAAM,gBAAgB,sBAAsB,iBAAiB;CAC7D,MAAM,mBAAmB,sBAAsB,WAAW;CAG1D,MAAM,eAAe,2BAA2B;EAC9C,WAAW,IAAI;EACf,WAAW,IAAI;CACjB,CAAC;CAED,IAAI,cACF,OAAO;CAIT,MAAM,uBAAuB,IAAI,YAAY,iBAAiB;CAE9D,IAAI,CAAC,sBACH,OAAO;CAGT,MAAM,yBAAyB,sBAC7B,sBACA,gBACF;CAEA,MAAM,qBAAqB,eACzB,wBACA,kBACA,aACF;CAEA,IAAI,oBACF,OAAO;CAIT,OAAO;AACT"}
@@ -1,5 +1,5 @@
1
1
  import { checkIsURLAbsolute } from "../utils/checkIsURLAbsolute.mjs";
2
- import { resolveRoutingConfig } from "./getPrefix.mjs";
2
+ import { isDeclaredLocale, resolveRoutingConfig } from "./getPrefix.mjs";
3
3
 
4
4
  //#region src/localization/getLocaleFromPath.ts
5
5
  /**
@@ -19,15 +19,15 @@ const getLocaleFromPath = (inputUrl = "/", options) => {
19
19
  if (!defaultLocale || !locales) return defaultLocale;
20
20
  const isAbsoluteUrl = checkIsURLAbsolute(inputUrl);
21
21
  const fixedInputUrl = inputUrl?.endsWith("/") && inputUrl.length > 1 ? inputUrl.slice(0, -1) : inputUrl;
22
- const url = isAbsoluteUrl ? new URL(fixedInputUrl) : new URL(fixedInputUrl, "http://example.com");
22
+ const url = isAbsoluteUrl ? new URL(fixedInputUrl) : new URL(fixedInputUrl, "http://e.com");
23
23
  if (mode === "search-params") {
24
24
  const localeParam = url.searchParams.get("locale");
25
- if (localeParam && locales.includes(localeParam)) return localeParam;
25
+ if (isDeclaredLocale(localeParam, locales)) return localeParam;
26
26
  return defaultLocale;
27
27
  }
28
28
  if (mode === "no-prefix") return defaultLocale;
29
29
  const firstSegment = url.pathname.split("/")[1];
30
- if (firstSegment && locales.includes(firstSegment)) return firstSegment;
30
+ if (isDeclaredLocale(firstSegment, locales)) return firstSegment;
31
31
  if (mode === "prefix-no-default") return defaultLocale;
32
32
  };
33
33
 
@@ -1 +1 @@
1
- {"version":3,"file":"getLocaleFromPath.mjs","names":[],"sources":["../../../src/localization/getLocaleFromPath.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { type RoutingOptions, resolveRoutingConfig } from './getPrefix';\n\n/**\n * Extracts the locale segment from the given URL or pathname based on the routing mode.\n *\n * Mode Behaviors:\n * - 'prefix-no-default': Checks path prefix. If no prefix found, assumes default locale.\n * - 'prefix-all': Checks path prefix.\n * - 'search-params': Checks for 'locale' query parameter.\n * - 'no-prefix': Returns the default locale.\n *\n * @param inputUrl - The complete URL string or pathname to process.\n * @returns The detected locale, default locale (if fallback/implicit), or undefined.\n */\nexport const getLocaleFromPath = (\n inputUrl: string = '/',\n options?: RoutingOptions\n): Locale | undefined => {\n const { defaultLocale, locales, mode } = resolveRoutingConfig(options);\n\n if (!defaultLocale || !locales) {\n return defaultLocale as Locale | undefined;\n }\n\n const isAbsoluteUrl = checkIsURLAbsolute(inputUrl);\n const fixedInputUrl =\n inputUrl?.endsWith('/') && inputUrl.length > 1\n ? inputUrl.slice(0, -1)\n : inputUrl;\n\n const url = isAbsoluteUrl\n ? new URL(fixedInputUrl)\n : new URL(fixedInputUrl, 'http://example.com');\n\n // Handle 'search-params' mode — locale is in query string\n if (mode === 'search-params') {\n const localeParam = url.searchParams.get('locale');\n if (localeParam && locales.includes(localeParam)) {\n return localeParam as Locale;\n }\n return defaultLocale as Locale;\n }\n\n // Handle 'no-prefix' mode — locale is not in the URL\n if (mode === 'no-prefix') {\n return defaultLocale as Locale;\n }\n\n // Handle prefix modes ('prefix-all' | 'prefix-no-default')\n const firstSegment = url.pathname.split('/')[1];\n if (firstSegment && locales.includes(firstSegment)) {\n return firstSegment as Locale;\n }\n\n // In 'prefix-no-default', no prefix implies the default locale\n if (mode === 'prefix-no-default') {\n return defaultLocale as Locale;\n }\n\n return undefined;\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,MAAa,qBACX,WAAmB,KACnB,YACuB;CACvB,MAAM,EAAE,eAAe,SAAS,SAAS,qBAAqB,OAAO;CAErE,IAAI,CAAC,iBAAiB,CAAC,SACrB,OAAO;CAGT,MAAM,gBAAgB,mBAAmB,QAAQ;CACjD,MAAM,gBACJ,UAAU,SAAS,GAAG,KAAK,SAAS,SAAS,IACzC,SAAS,MAAM,GAAG,EAAE,IACpB;CAEN,MAAM,MAAM,gBACR,IAAI,IAAI,aAAa,IACrB,IAAI,IAAI,eAAe,oBAAoB;CAG/C,IAAI,SAAS,iBAAiB;EAC5B,MAAM,cAAc,IAAI,aAAa,IAAI,QAAQ;EACjD,IAAI,eAAe,QAAQ,SAAS,WAAW,GAC7C,OAAO;EAET,OAAO;CACT;CAGA,IAAI,SAAS,aACX,OAAO;CAIT,MAAM,eAAe,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC;CAC7C,IAAI,gBAAgB,QAAQ,SAAS,YAAY,GAC/C,OAAO;CAIT,IAAI,SAAS,qBACX,OAAO;AAIX"}
1
+ {"version":3,"file":"getLocaleFromPath.mjs","names":[],"sources":["../../../src/localization/getLocaleFromPath.ts"],"sourcesContent":["import type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport {\n isDeclaredLocale,\n type RoutingOptions,\n resolveRoutingConfig,\n} from './getPrefix';\n\n/**\n * Extracts the locale segment from the given URL or pathname based on the routing mode.\n *\n * Mode Behaviors:\n * - 'prefix-no-default': Checks path prefix. If no prefix found, assumes default locale.\n * - 'prefix-all': Checks path prefix.\n * - 'search-params': Checks for 'locale' query parameter.\n * - 'no-prefix': Returns the default locale.\n *\n * @param inputUrl - The complete URL string or pathname to process.\n * @returns The detected locale, default locale (if fallback/implicit), or undefined.\n */\nexport const getLocaleFromPath = (\n inputUrl: string = '/',\n options?: RoutingOptions\n): DeclaredLocales | undefined => {\n const { defaultLocale, locales, mode } = resolveRoutingConfig(options);\n\n if (!defaultLocale || !locales) {\n return defaultLocale as DeclaredLocales | undefined;\n }\n\n const isAbsoluteUrl = checkIsURLAbsolute(inputUrl);\n const fixedInputUrl =\n inputUrl?.endsWith('/') && inputUrl.length > 1\n ? inputUrl.slice(0, -1)\n : inputUrl;\n\n const url = isAbsoluteUrl\n ? new URL(fixedInputUrl)\n : new URL(fixedInputUrl, 'http://e.com');\n\n // Handle 'search-params' mode — locale is in query string\n if (mode === 'search-params') {\n const localeParam = url.searchParams.get('locale');\n if (isDeclaredLocale(localeParam, locales)) {\n return localeParam;\n }\n return defaultLocale as DeclaredLocales;\n }\n\n // Handle 'no-prefix' mode — locale is not in the URL\n if (mode === 'no-prefix') {\n return defaultLocale as DeclaredLocales;\n }\n\n // Handle prefix modes ('prefix-all' | 'prefix-no-default')\n const firstSegment = url.pathname.split('/')[1];\n if (isDeclaredLocale(firstSegment, locales)) {\n return firstSegment;\n }\n\n // In 'prefix-no-default', no prefix implies the default locale\n if (mode === 'prefix-no-default') {\n return defaultLocale as DeclaredLocales;\n }\n\n return undefined;\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,MAAa,qBACX,WAAmB,KACnB,YACgC;CAChC,MAAM,EAAE,eAAe,SAAS,SAAS,qBAAqB,OAAO;CAErE,IAAI,CAAC,iBAAiB,CAAC,SACrB,OAAO;CAGT,MAAM,gBAAgB,mBAAmB,QAAQ;CACjD,MAAM,gBACJ,UAAU,SAAS,GAAG,KAAK,SAAS,SAAS,IACzC,SAAS,MAAM,GAAG,EAAE,IACpB;CAEN,MAAM,MAAM,gBACR,IAAI,IAAI,aAAa,IACrB,IAAI,IAAI,eAAe,cAAc;CAGzC,IAAI,SAAS,iBAAiB;EAC5B,MAAM,cAAc,IAAI,aAAa,IAAI,QAAQ;EACjD,IAAI,iBAAiB,aAAa,OAAO,GACvC,OAAO;EAET,OAAO;CACT;CAGA,IAAI,SAAS,aACX,OAAO;CAIT,MAAM,eAAe,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC;CAC7C,IAAI,iBAAiB,cAAc,OAAO,GACxC,OAAO;CAIT,IAAI,SAAS,qBACX,OAAO;AAIX"}
@@ -0,0 +1,73 @@
1
+ import { getPathWithoutLocale } from "./getPathWithoutLocale.mjs";
2
+ import { getPrefix, resolveRoutingConfig } from "./getPrefix.mjs";
3
+ import { getCanonicalPath, getRewriteRules, resolveLocalizedPath } from "./rewriteUtils.mjs";
4
+ import { internationalization } from "@intlayer/config/built";
5
+
6
+ //#region src/localization/getLocalizedPath.ts
7
+ /**
8
+ * Localizes an application path for a locale: resolves the `routing.rewrite`
9
+ * rules, then applies the locale representation of the routing mode — a path
10
+ * prefix, a `locale` search parameter, or nothing at all.
11
+ *
12
+ * Any query string and hash of the input are preserved.
13
+ *
14
+ * This is the relative half of {@link getLocalizedUrl}, which builds on it to
15
+ * add an origin and cross-domain routing. For a relative input both return the
16
+ * same value; this one never returns an absolute URL — the origin of an
17
+ * absolute input is dropped and only its path is localized.
18
+ *
19
+ * Use `resolveLocalizedPath` instead to apply the rewrite rules alone, without
20
+ * any locale representation.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // routing: { mode: 'prefix-no-default', rewrite: { '/about': { fr: '/a-propos' } } }
25
+ * getLocalizedPath('/about', 'fr'); // '/fr/a-propos'
26
+ * getLocalizedPath('/about', 'en'); // '/about' (default locale, no prefix)
27
+ * getLocalizedPath('/', 'fr'); // '/fr'
28
+ * getLocalizedPath('/contact', 'fr'); // '/fr/contact' (no rewrite rule matches)
29
+ * getLocalizedPath('https://intlayer.org/about', 'fr'); // '/fr/a-propos' (origin dropped)
30
+ *
31
+ * // Manual rules, bypassing the configuration
32
+ * getLocalizedPath('/contact', 'fr', {
33
+ * rewrite: { '/contact': { fr: '/contactez-nous' } },
34
+ * });
35
+ * // '/fr/contactez-nous'
36
+ * ```
37
+ *
38
+ * @param canonicalPath - The internal application path (e.g. `/about`, `/product/123`).
39
+ * An absolute URL is accepted; only its path, query and hash are kept.
40
+ * @param locale - The target locale. Defaults to the configured default locale.
41
+ * @param options - Routing overrides (`locales`, `defaultLocale`, `mode`, `rewrite`,
42
+ * `domains`). Each one defaults to the project configuration. `domains` only
43
+ * suppresses the prefix of a locale served from its own domain — the origin
44
+ * itself is never emitted.
45
+ * @returns The localized path for the target locale.
46
+ */
47
+ const getLocalizedPath = (canonicalPath, locale = internationalization?.defaultLocale, options = {}) => {
48
+ const { defaultLocale, mode, locales, rewrite, domains } = resolveRoutingConfig(options);
49
+ const pathWithoutLocale = getPathWithoutLocale(canonicalPath, locales);
50
+ const rewriteRules = getRewriteRules(rewrite, "url");
51
+ const parsedPath = new URL(pathWithoutLocale, "http://e.com");
52
+ const translatedPathname = resolveLocalizedPath(getCanonicalPath(parsedPath.pathname, void 0, rewriteRules), locale, rewriteRules).path;
53
+ if (!(process.env.INTLAYER_ROUTING_MODE && process.env.INTLAYER_ROUTING_MODE !== "no-prefix") && mode === "no-prefix") return `${translatedPathname}${parsedPath.search}${parsedPath.hash}`;
54
+ if (!(process.env.INTLAYER_ROUTING_MODE && process.env.INTLAYER_ROUTING_MODE !== "search-params") && mode === "search-params") {
55
+ const searchParams = new URLSearchParams(parsedPath.search);
56
+ searchParams.set("locale", locale.toString());
57
+ const queryParams = searchParams.toString();
58
+ return `${translatedPathname}${queryParams ? `?${queryParams}` : ""}${parsedPath.hash}`;
59
+ }
60
+ const { prefix } = getPrefix(locale, {
61
+ defaultLocale,
62
+ mode,
63
+ locales,
64
+ domains
65
+ });
66
+ let localizedPath = `/${prefix}${translatedPathname}`.replace(/\/+/g, "/");
67
+ if (localizedPath.length > 1 && localizedPath.endsWith("/")) localizedPath = localizedPath.slice(0, -1);
68
+ return `${localizedPath}${parsedPath.search}${parsedPath.hash}`;
69
+ };
70
+
71
+ //#endregion
72
+ export { getLocalizedPath };
73
+ //# sourceMappingURL=getLocalizedPath.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getLocalizedPath.mjs","names":[],"sources":["../../../src/localization/getLocalizedPath.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type {\n LocalesValues,\n LocalizedPathname,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\nimport { getPathWithoutLocale } from './getPathWithoutLocale';\nimport {\n getPrefix,\n type RoutingOptions,\n resolveRoutingConfig,\n} from './getPrefix';\nimport {\n getCanonicalPath,\n getRewriteRules,\n resolveLocalizedPath,\n} from './rewriteUtils';\n\n/**\n * Localizes an application path for a locale: resolves the `routing.rewrite`\n * rules, then applies the locale representation of the routing mode — a path\n * prefix, a `locale` search parameter, or nothing at all.\n *\n * Any query string and hash of the input are preserved.\n *\n * This is the relative half of {@link getLocalizedUrl}, which builds on it to\n * add an origin and cross-domain routing. For a relative input both return the\n * same value; this one never returns an absolute URL — the origin of an\n * absolute input is dropped and only its path is localized.\n *\n * Use `resolveLocalizedPath` instead to apply the rewrite rules alone, without\n * any locale representation.\n *\n * @example\n * ```ts\n * // routing: { mode: 'prefix-no-default', rewrite: { '/about': { fr: '/a-propos' } } }\n * getLocalizedPath('/about', 'fr'); // '/fr/a-propos'\n * getLocalizedPath('/about', 'en'); // '/about' (default locale, no prefix)\n * getLocalizedPath('/', 'fr'); // '/fr'\n * getLocalizedPath('/contact', 'fr'); // '/fr/contact' (no rewrite rule matches)\n * getLocalizedPath('https://intlayer.org/about', 'fr'); // '/fr/a-propos' (origin dropped)\n *\n * // Manual rules, bypassing the configuration\n * getLocalizedPath('/contact', 'fr', {\n * rewrite: { '/contact': { fr: '/contactez-nous' } },\n * });\n * // '/fr/contactez-nous'\n * ```\n *\n * @param canonicalPath - The internal application path (e.g. `/about`, `/product/123`).\n * An absolute URL is accepted; only its path, query and hash are kept.\n * @param locale - The target locale. Defaults to the configured default locale.\n * @param options - Routing overrides (`locales`, `defaultLocale`, `mode`, `rewrite`,\n * `domains`). Each one defaults to the project configuration. `domains` only\n * suppresses the prefix of a locale served from its own domain — the origin\n * itself is never emitted.\n * @returns The localized path for the target locale.\n */\nexport const getLocalizedPath = <\n const CanonicalPath extends string,\n const CurrentLocale extends LocalesValues = ResolvedDefaultLocale,\n>(\n canonicalPath: CanonicalPath,\n locale: CurrentLocale = internationalization?.defaultLocale as CurrentLocale,\n options: RoutingOptions = {}\n): LocalizedPathname<CanonicalPath, CurrentLocale> => {\n const { defaultLocale, mode, locales, rewrite, domains } =\n resolveRoutingConfig(options);\n\n const pathWithoutLocale = getPathWithoutLocale(canonicalPath, locales);\n const rewriteRules = getRewriteRules(rewrite, 'url');\n\n // Parsed against a dummy origin to split the path from its query and hash.\n // An absolute input parses on its own, and only its path is kept.\n const parsedPath = new URL(pathWithoutLocale, 'http://e.com');\n\n const translatedPathname = resolveLocalizedPath(\n getCanonicalPath(parsedPath.pathname, undefined, rewriteRules),\n locale as Locale,\n rewriteRules\n ).path;\n\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix'\n ) {\n return `${translatedPathname}${parsedPath.search}${parsedPath.hash}` as LocalizedPathname<\n CanonicalPath,\n CurrentLocale\n >;\n }\n\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n const searchParams = new URLSearchParams(parsedPath.search);\n\n searchParams.set('locale', locale.toString());\n\n const queryParams = searchParams.toString();\n\n return `${translatedPathname}${queryParams ? `?${queryParams}` : ''}${parsedPath.hash}` as LocalizedPathname<\n CanonicalPath,\n CurrentLocale\n >;\n }\n\n const { prefix } = getPrefix(locale, {\n defaultLocale,\n mode,\n locales,\n domains,\n });\n\n let localizedPath = `/${prefix}${translatedPathname}`.replace(/\\/+/g, '/');\n\n if (localizedPath.length > 1 && localizedPath.endsWith('/')) {\n localizedPath = localizedPath.slice(0, -1);\n }\n\n return `${localizedPath}${parsedPath.search}${parsedPath.hash}` as LocalizedPathname<\n CanonicalPath,\n CurrentLocale\n >;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,oBAIX,eACA,SAAwB,sBAAsB,eAC9C,UAA0B,CAAC,MACyB;CACpD,MAAM,EAAE,eAAe,MAAM,SAAS,SAAS,YAC7C,qBAAqB,OAAO;CAE9B,MAAM,oBAAoB,qBAAqB,eAAe,OAAO;CACrE,MAAM,eAAe,gBAAgB,SAAS,KAAK;CAInD,MAAM,aAAa,IAAI,IAAI,mBAAmB,cAAc;CAE5D,MAAM,qBAAqB,qBACzB,iBAAiB,WAAW,UAAU,QAAW,YAAY,GAC7D,QACA,YACF,CAAC,CAAC;CAEF,IACE,EACE,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,gBAExC,SAAS,aAET,OAAO,GAAG,qBAAqB,WAAW,SAAS,WAAW;CAMhE,IACE,EACE,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,oBAExC,SAAS,iBACT;EACA,MAAM,eAAe,IAAI,gBAAgB,WAAW,MAAM;EAE1D,aAAa,IAAI,UAAU,OAAO,SAAS,CAAC;EAE5C,MAAM,cAAc,aAAa,SAAS;EAE1C,OAAO,GAAG,qBAAqB,cAAc,IAAI,gBAAgB,KAAK,WAAW;CAInF;CAEA,MAAM,EAAE,WAAW,UAAU,QAAQ;EACnC;EACA;EACA;EACA;CACF,CAAC;CAED,IAAI,gBAAgB,IAAI,SAAS,qBAAqB,QAAQ,QAAQ,GAAG;CAEzE,IAAI,cAAc,SAAS,KAAK,cAAc,SAAS,GAAG,GACxD,gBAAgB,cAAc,MAAM,GAAG,EAAE;CAG3C,OAAO,GAAG,gBAAgB,WAAW,SAAS,WAAW;AAI3D"}
@@ -0,0 +1,83 @@
1
+ import { getLocalizedPath } from "./getLocalizedPath.mjs";
2
+ import { getLocalizedUrl } from "./getLocalizedUrl.mjs";
3
+ import { describe, expectTypeOf, it } from "vitest";
4
+
5
+ //#region src/localization/getLocalizedPath.test-d.ts
6
+ /**
7
+ * `getLocalizedPath` prefixes the rewritten path, so it reports the very type
8
+ * its sibling reports for a relative URL. This package declares neither rewrite
9
+ * rules nor a default locale, so the registry leaves both steps as no-ops.
10
+ */
11
+ describe("getLocalizedPath", () => {
12
+ it("should report the same type as getLocalizedUrl", () => {
13
+ expectTypeOf(getLocalizedPath("/about", "fr")).toEqualTypeOf();
14
+ expectTypeOf(getLocalizedPath("/about", "fr")).toEqualTypeOf(getLocalizedUrl("/about", "fr"));
15
+ });
16
+ it("should keep the literal path when nothing rewrites or prefixes it", () => {
17
+ expectTypeOf(getLocalizedPath("/about", "fr")).toEqualTypeOf();
18
+ });
19
+ it("should drop the origin of an absolute URL", () => {
20
+ expectTypeOf(getLocalizedPath("https://intlayer.org/about", "fr")).toEqualTypeOf();
21
+ });
22
+ it("should widen to string when the path is not a literal", () => {
23
+ expectTypeOf(getLocalizedPath("/about", "fr")).toEqualTypeOf();
24
+ });
25
+ });
26
+ /**
27
+ * The resolution itself, exercised against explicit rules — the same shape the
28
+ * generated module augmentation feeds it as `__RoutingRegistry['rewrite']`.
29
+ */
30
+ describe("LocalizedPath", () => {
31
+ it("should rewrite a static path", () => {
32
+ expectTypeOf().toEqualTypeOf();
33
+ });
34
+ it("should keep a path no rule matches", () => {
35
+ expectTypeOf().toEqualTypeOf();
36
+ });
37
+ it("should substitute a route parameter", () => {
38
+ expectTypeOf().toEqualTypeOf();
39
+ });
40
+ it("should substitute several route parameters", () => {
41
+ expectTypeOf().toEqualTypeOf();
42
+ });
43
+ it("should not match a parameter across a segment boundary", () => {
44
+ expectTypeOf().toEqualTypeOf();
45
+ });
46
+ it("should keep the path when the locale declares no pattern", () => {
47
+ expectTypeOf().toEqualTypeOf();
48
+ });
49
+ it("should distribute over a union of locales", () => {
50
+ expectTypeOf().toEqualTypeOf();
51
+ });
52
+ it("should widen a variadic rule to string, but only for the paths it could match", () => {
53
+ expectTypeOf().toEqualTypeOf();
54
+ expectTypeOf().toEqualTypeOf();
55
+ });
56
+ it("should keep every path when the project declares no rule", () => {
57
+ expectTypeOf().toEqualTypeOf();
58
+ });
59
+ });
60
+ /**
61
+ * The return type of `getLocalizedPath`, exercised against an explicit routing
62
+ * mode, default locale and locale set — this package declares none of them.
63
+ */
64
+ describe("LocalizedPathname", () => {
65
+ it("should report the same type as LocalizedUrl for a relative path", () => {
66
+ expectTypeOf().toEqualTypeOf();
67
+ expectTypeOf().toEqualTypeOf();
68
+ });
69
+ it("should drop the origin of an absolute URL", () => {
70
+ expectTypeOf().toEqualTypeOf();
71
+ expectTypeOf().toEqualTypeOf();
72
+ });
73
+ it("should collapse an origin without a path to the root", () => {
74
+ expectTypeOf().toEqualTypeOf();
75
+ expectTypeOf().toEqualTypeOf();
76
+ });
77
+ it("should replace a locale segment carried by an absolute URL", () => {
78
+ expectTypeOf().toEqualTypeOf();
79
+ });
80
+ });
81
+
82
+ //#endregion
83
+ //# sourceMappingURL=getLocalizedPath.test-d.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getLocalizedPath.test-d.mjs","names":[],"sources":["../../../src/localization/getLocalizedPath.test-d.ts"],"sourcesContent":["import type {\n LocalizedPath,\n LocalizedPathname,\n LocalizedUrl,\n} from '@intlayer/types/module_augmentation';\nimport { describe, expectTypeOf, it } from 'vitest';\nimport { getLocalizedPath } from './getLocalizedPath';\nimport { getLocalizedUrl } from './getLocalizedUrl';\n\n/**\n * `getLocalizedPath` prefixes the rewritten path, so it reports the very type\n * its sibling reports for a relative URL. This package declares neither rewrite\n * rules nor a default locale, so the registry leaves both steps as no-ops.\n */\ndescribe('getLocalizedPath', () => {\n it('should report the same type as getLocalizedUrl', () => {\n expectTypeOf(getLocalizedPath('/about', 'fr')).toEqualTypeOf<\n LocalizedUrl<'/about', 'fr'>\n >();\n expectTypeOf(getLocalizedPath('/about', 'fr')).toEqualTypeOf(\n getLocalizedUrl('/about', 'fr')\n );\n });\n\n it('should keep the literal path when nothing rewrites or prefixes it', () => {\n expectTypeOf(getLocalizedPath('/about', 'fr')).toEqualTypeOf<'/about'>();\n });\n\n it('should drop the origin of an absolute URL', () => {\n expectTypeOf(\n getLocalizedPath('https://intlayer.org/about', 'fr')\n ).toEqualTypeOf<'/about'>();\n });\n\n it('should widen to string when the path is not a literal', () => {\n const dynamicPath = '/about' as string;\n\n expectTypeOf(getLocalizedPath(dynamicPath, 'fr')).toEqualTypeOf<string>();\n });\n});\n\n/**\n * The resolution itself, exercised against explicit rules — the same shape the\n * generated module augmentation feeds it as `__RoutingRegistry['rewrite']`.\n */\ndescribe('LocalizedPath', () => {\n type Rules = {\n '/about': { en: '/about'; fr: '/a-propos' };\n '/product/:id': { en: '/product/:id'; fr: '/produit/:id' };\n '/shop/:category/:id': { fr: '/boutique/:category/:id' };\n '/docs/:path+': { fr: '/documentation/:path+' };\n };\n\n it('should rewrite a static path', () => {\n expectTypeOf<\n LocalizedPath<'/about', 'fr', Rules>\n >().toEqualTypeOf<'/a-propos'>();\n });\n\n it('should keep a path no rule matches', () => {\n expectTypeOf<\n LocalizedPath<'/contact', 'fr', Rules>\n >().toEqualTypeOf<'/contact'>();\n });\n\n it('should substitute a route parameter', () => {\n expectTypeOf<\n LocalizedPath<'/product/123', 'fr', Rules>\n >().toEqualTypeOf<'/produit/123'>();\n });\n\n it('should substitute several route parameters', () => {\n expectTypeOf<\n LocalizedPath<'/shop/shoes/123', 'fr', Rules>\n >().toEqualTypeOf<'/boutique/shoes/123'>();\n });\n\n it('should not match a parameter across a segment boundary', () => {\n expectTypeOf<\n LocalizedPath<'/product/123/reviews', 'fr', Rules>\n >().toEqualTypeOf<'/product/123/reviews'>();\n });\n\n it('should keep the path when the locale declares no pattern', () => {\n expectTypeOf<\n LocalizedPath<'/shop/shoes/123', 'en', Rules>\n >().toEqualTypeOf<'/shop/shoes/123'>();\n });\n\n it('should distribute over a union of locales', () => {\n expectTypeOf<LocalizedPath<'/about', 'en' | 'fr', Rules>>().toEqualTypeOf<\n '/about' | '/a-propos'\n >();\n });\n\n it('should widen a variadic rule to string, but only for the paths it could match', () => {\n expectTypeOf<\n LocalizedPath<'/docs/getting-started', 'fr', Rules>\n >().toEqualTypeOf<string>();\n expectTypeOf<\n LocalizedPath<'/about', 'fr', Rules>\n >().toEqualTypeOf<'/a-propos'>();\n });\n\n it('should keep every path when the project declares no rule', () => {\n expectTypeOf<LocalizedPath<'/about', 'fr', {}>>().toEqualTypeOf<'/about'>();\n });\n});\n\n/**\n * The return type of `getLocalizedPath`, exercised against an explicit routing\n * mode, default locale and locale set — this package declares none of them.\n */\ndescribe('LocalizedPathname', () => {\n type Locales = 'en' | 'fr' | 'es';\n type Path<P extends string, L extends Locales> = LocalizedPathname<\n P,\n L,\n 'prefix-no-default',\n 'en',\n Locales\n >;\n\n it('should report the same type as LocalizedUrl for a relative path', () => {\n expectTypeOf<Path<'/about', 'es'>>().toEqualTypeOf<\n LocalizedUrl<'/about', 'es', 'prefix-no-default', 'en', Locales>\n >();\n expectTypeOf<Path<'/about', 'es'>>().toEqualTypeOf<'/es/about'>();\n });\n\n it('should drop the origin of an absolute URL', () => {\n expectTypeOf<\n Path<'https://intlayer.org/about', 'es'>\n >().toEqualTypeOf<'/es/about'>();\n expectTypeOf<\n Path<'https://intlayer.org/about', 'en'>\n >().toEqualTypeOf<'/about'>();\n });\n\n it('should collapse an origin without a path to the root', () => {\n expectTypeOf<Path<'https://intlayer.org', 'en'>>().toEqualTypeOf<'/'>();\n expectTypeOf<Path<'https://intlayer.org/', 'es'>>().toEqualTypeOf<'/es'>();\n });\n\n it('should replace a locale segment carried by an absolute URL', () => {\n expectTypeOf<\n Path<'https://intlayer.org/fr/about', 'es'>\n >().toEqualTypeOf<'/es/about'>();\n });\n});\n"],"mappings":";;;;;;;;;;AAcA,SAAS,0BAA0B;CACjC,GAAG,wDAAwD;EACzD,aAAa,iBAAiB,UAAU,IAAI,CAAC,CAAC,CAAC,cAE7C;EACF,aAAa,iBAAiB,UAAU,IAAI,CAAC,CAAC,CAAC,cAC7C,gBAAgB,UAAU,IAAI,CAChC;CACF,CAAC;CAED,GAAG,2EAA2E;EAC5E,aAAa,iBAAiB,UAAU,IAAI,CAAC,CAAC,CAAC,cAAwB;CACzE,CAAC;CAED,GAAG,mDAAmD;EACpD,aACE,iBAAiB,8BAA8B,IAAI,CACrD,CAAC,CAAC,cAAwB;CAC5B,CAAC;CAED,GAAG,+DAA+D;EAGhE,aAAa,iBAAiB,UAAa,IAAI,CAAC,CAAC,CAAC,cAAsB;CAC1E,CAAC;AACH,CAAC;;;;;AAMD,SAAS,uBAAuB;CAQ9B,GAAG,sCAAsC;EACvC,aAEE,CAAC,CAAC,cAA2B;CACjC,CAAC;CAED,GAAG,4CAA4C;EAC7C,aAEE,CAAC,CAAC,cAA0B;CAChC,CAAC;CAED,GAAG,6CAA6C;EAC9C,aAEE,CAAC,CAAC,cAA8B;CACpC,CAAC;CAED,GAAG,oDAAoD;EACrD,aAEE,CAAC,CAAC,cAAqC;CAC3C,CAAC;CAED,GAAG,gEAAgE;EACjE,aAEE,CAAC,CAAC,cAAsC;CAC5C,CAAC;CAED,GAAG,kEAAkE;EACnE,aAEE,CAAC,CAAC,cAAiC;CACvC,CAAC;CAED,GAAG,mDAAmD;EACpD,aAA0D,CAAC,CAAC,cAE1D;CACJ,CAAC;CAED,GAAG,uFAAuF;EACxF,aAEE,CAAC,CAAC,cAAsB;EAC1B,aAEE,CAAC,CAAC,cAA2B;CACjC,CAAC;CAED,GAAG,kEAAkE;EACnE,aAAgD,CAAC,CAAC,cAAwB;CAC5E,CAAC;AACH,CAAC;;;;;AAMD,SAAS,2BAA2B;CAUlC,GAAG,yEAAyE;EAC1E,aAAmC,CAAC,CAAC,cAEnC;EACF,aAAmC,CAAC,CAAC,cAA2B;CAClE,CAAC;CAED,GAAG,mDAAmD;EACpD,aAEE,CAAC,CAAC,cAA2B;EAC/B,aAEE,CAAC,CAAC,cAAwB;CAC9B,CAAC;CAED,GAAG,8DAA8D;EAC/D,aAAiD,CAAC,CAAC,cAAmB;EACtE,aAAkD,CAAC,CAAC,cAAqB;CAC3E,CAAC;CAED,GAAG,oEAAoE;EACrE,aAEE,CAAC,CAAC,cAA2B;CACjC,CAAC;AACH,CAAC"}
@@ -1,8 +1,7 @@
1
1
  import { checkIsURLAbsolute } from "../utils/checkIsURLAbsolute.mjs";
2
- import { getPathWithoutLocale } from "./getPathWithoutLocale.mjs";
3
2
  import { getDomainHostname, getDomainOrigin } from "./domainUtils.mjs";
4
- import { getPrefix, resolveRoutingConfig } from "./getPrefix.mjs";
5
- import { getCanonicalPath, getLocalizedPath, getRewriteRules } from "./rewriteUtils.mjs";
3
+ import { resolveRoutingConfig } from "./getPrefix.mjs";
4
+ import { getLocalizedPath } from "./getLocalizedPath.mjs";
6
5
  import { internationalization } from "@intlayer/config/built";
7
6
 
8
7
  //#region src/localization/getLocalizedUrl.ts
@@ -12,6 +11,10 @@ import { internationalization } from "@intlayer/config/built";
12
11
  *
13
12
  * This function gets the locales, default locale, and routing mode from the configuration if not provided.
14
13
  *
14
+ * The path itself is localized by {@link getLocalizedPath}; this function only
15
+ * resolves the origin to put in front of it — the one of an absolute input, or
16
+ * the domain mapped to the target locale.
17
+ *
15
18
  * Example:
16
19
  *
17
20
  * ```ts
@@ -51,33 +54,14 @@ import { internationalization } from "@intlayer/config/built";
51
54
  * not overridden via `options`.
52
55
  */
53
56
  const getLocalizedUrl = (url, currentLocale = internationalization?.defaultLocale, options = {}) => {
54
- const { defaultLocale, mode, locales, rewrite, domains, currentDomain } = resolveRoutingConfig(options);
55
- const urlWithoutLocale = getPathWithoutLocale(url, locales);
56
- const rewriteRules = getRewriteRules(rewrite, "url");
57
- if (!(process.env.INTLAYER_ROUTING_MODE && process.env.INTLAYER_ROUTING_MODE !== "no-prefix") && mode === "no-prefix") return getLocalizedPath(getCanonicalPath(urlWithoutLocale, void 0, rewriteRules), currentLocale, rewriteRules).path;
58
- const isAbsoluteUrl = checkIsURLAbsolute(urlWithoutLocale);
59
- const parsedUrl = isAbsoluteUrl ? new URL(urlWithoutLocale) : new URL(urlWithoutLocale, "http://example.com");
60
- const translatedPathname = getLocalizedPath(getCanonicalPath(parsedUrl.pathname, void 0, rewriteRules), currentLocale, rewriteRules).path;
57
+ const { domains, currentDomain } = resolveRoutingConfig(options);
58
+ const isAbsoluteUrl = checkIsURLAbsolute(url);
59
+ const parsedUrl = isAbsoluteUrl ? new URL(url) : new URL(url, "http://e.com");
61
60
  const detectedCurrentHostname = process.env.INTLAYER_ROUTING_DOMAINS !== "false" ? getDomainHostname(currentDomain ?? (isAbsoluteUrl ? parsedUrl.hostname : void 0) ?? (typeof window !== "undefined" ? window?.location?.hostname : void 0) ?? "") || null : null;
62
61
  const localeDomain = process.env.INTLAYER_ROUTING_DOMAINS !== "false" ? domains?.[currentLocale] : void 0;
63
62
  const localeDomainHostname = localeDomain ? getDomainHostname(localeDomain) : null;
64
63
  const normalizedDomain = localeDomainHostname !== null && (detectedCurrentHostname === null || localeDomainHostname !== detectedCurrentHostname) && localeDomain ? getDomainOrigin(localeDomain) : null;
65
- const baseUrl = normalizedDomain ? normalizedDomain : isAbsoluteUrl ? `${parsedUrl.protocol}//${parsedUrl.host}` : "";
66
- if (!(process.env.INTLAYER_ROUTING_MODE && process.env.INTLAYER_ROUTING_MODE !== "search-params") && mode === "search-params") {
67
- const searchParams = new URLSearchParams(parsedUrl.search);
68
- searchParams.set("locale", currentLocale.toString());
69
- const queryParams = searchParams.toString();
70
- return `${baseUrl}${queryParams ? `${translatedPathname}?${queryParams}` : translatedPathname}${parsedUrl.hash}`;
71
- }
72
- const { prefix } = getPrefix(currentLocale, {
73
- defaultLocale,
74
- mode,
75
- locales,
76
- domains
77
- });
78
- let localizedPath = `/${prefix}${translatedPathname}`.replace(/\/+/g, "/");
79
- if (localizedPath.length > 1 && localizedPath.endsWith("/")) localizedPath = localizedPath.slice(0, -1);
80
- return `${baseUrl}${localizedPath}${parsedUrl.search}${parsedUrl.hash}`;
64
+ return `${normalizedDomain ? normalizedDomain : isAbsoluteUrl ? `${parsedUrl.protocol}//${parsedUrl.host}` : ""}${getLocalizedPath(`${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`, currentLocale, options)}`;
81
65
  };
82
66
 
83
67
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"getLocalizedUrl.mjs","names":[],"sources":["../../../src/localization/getLocalizedUrl.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type {\n LocalesValues,\n LocalizedUrl,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getDomainHostname, getDomainOrigin } from './domainUtils';\nimport { getPathWithoutLocale } from './getPathWithoutLocale';\nimport {\n getPrefix,\n type RoutingOptions,\n resolveRoutingConfig,\n} from './getPrefix';\nimport {\n getCanonicalPath,\n getLocalizedPath,\n getRewriteRules,\n} from './rewriteUtils';\n\nexport type { RoutingOptions };\n\n/**\n * Generate URL by prefixing the given URL with the referenced locale or adding search parameters\n * based on the routing mode. Handles both absolute and relative URLs appropriately.\n *\n * This function gets the locales, default locale, and routing mode from the configuration if not provided.\n *\n * Example:\n *\n * ```ts\n * // prefix-no-default mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'prefix-no-default' });\n * // Returns '/fr/about' for the French locale\n * // Returns '/about' for the English locale (default)\n *\n * // prefix-all mode\n * getLocalizedUrl('/about', 'en', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'prefix-all' });\n * // Returns '/en/about' for the English locale\n * // Returns '/fr/about' for the French locale\n *\n * // search-params mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'search-params' });\n * // Returns '/about?locale=fr' for the French locale\n *\n * // no-prefix mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'no-prefix' });\n * // Returns '/about' for any locale\n * ```\n *\n * @param url - The original URL string to be processed.\n * @param currentLocale - The current locale.\n * @param options - Configuration options\n * @param options.locales - Optional array of supported locales. Defaults to configured locales.\n * @param options.defaultLocale - The default locale. Defaults to configured default locale.\n * @param options.mode - URL routing mode for locale handling. Defaults to configured mode.\n * @param options.currentDomain - Hostname of the page being rendered. Used to decide\n * whether to emit a relative URL (same domain) or an absolute URL (cross-domain).\n * Auto-detected from the input URL or `window.location` when omitted.\n * @returns The localized URL for the current locale.\n */\n/**\n * The return type is narrowed to a template-literal type when both `url` and\n * `currentLocale` are string literals and the routing mode / defaultLocale are\n * not overridden via `options`.\n */\nexport const getLocalizedUrl = <\n const T extends string,\n const L extends LocalesValues = ResolvedDefaultLocale,\n>(\n url: T,\n currentLocale: L = internationalization?.defaultLocale as L,\n options: RoutingOptions = {}\n): LocalizedUrl<T, L> => {\n const { defaultLocale, mode, locales, rewrite, domains, currentDomain } =\n resolveRoutingConfig(options);\n\n const urlWithoutLocale = getPathWithoutLocale(url, locales);\n const rewriteRules = getRewriteRules(rewrite, 'url');\n\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix'\n ) {\n return getLocalizedPath(\n getCanonicalPath(urlWithoutLocale, undefined, rewriteRules),\n currentLocale as Locale,\n rewriteRules\n ).path as LocalizedUrl<T, L>;\n }\n\n const isAbsoluteUrl = checkIsURLAbsolute(urlWithoutLocale);\n const parsedUrl = isAbsoluteUrl\n ? new URL(urlWithoutLocale)\n : new URL(urlWithoutLocale, 'http://example.com');\n\n const translatedPathname = getLocalizedPath(\n getCanonicalPath(parsedUrl.pathname, undefined, rewriteRules),\n currentLocale as Locale,\n rewriteRules\n ).path;\n\n // ── Domain routing ────────────────────────────────────────────────────────\n // Resolve the \"current\" hostname so we can choose between a relative URL\n // (same domain) and an absolute URL (cross-domain).\n //\n // Detection priority:\n // 1. Explicit `currentDomain` option passed by the caller.\n // 2. Hostname extracted from an absolute input URL.\n // 3. `window.location.hostname` in browser environments.\n // When none of these is available we fall back to generating an absolute\n // URL for domain-mapped locales (safe for SSR/static generation): the\n // locale prefix may already be stripped for domain-exclusive locales, so a\n // relative URL would silently point to the wrong locale on the current\n // domain.\n const detectedCurrentHostname =\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false'\n ? getDomainHostname(\n currentDomain ??\n (isAbsoluteUrl ? parsedUrl.hostname : undefined) ??\n (typeof window !== 'undefined'\n ? window?.location?.hostname\n : undefined) ??\n ''\n ) || null\n : null;\n\n const localeDomain =\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false'\n ? domains?.[currentLocale as LocalesValues]\n : undefined;\n\n const localeDomainHostname = localeDomain\n ? getDomainHostname(localeDomain)\n : null;\n\n // Prepend the locale's domain when it differs from the current hostname,\n // or when the current hostname is unknown (SSR without `currentDomain`).\n const isCrossDomain =\n localeDomainHostname !== null &&\n (detectedCurrentHostname === null ||\n localeDomainHostname !== detectedCurrentHostname);\n\n const normalizedDomain =\n isCrossDomain && localeDomain ? getDomainOrigin(localeDomain) : null;\n\n const baseUrl = normalizedDomain\n ? normalizedDomain\n : isAbsoluteUrl\n ? `${parsedUrl.protocol}//${parsedUrl.host}`\n : '';\n // ─────────────────────────────────────────────────────────────────────────\n\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n const searchParams = new URLSearchParams(parsedUrl.search);\n\n searchParams.set('locale', currentLocale.toString());\n\n const queryParams = searchParams.toString();\n const path = queryParams\n ? `${translatedPathname}?${queryParams}`\n : translatedPathname;\n\n return `${baseUrl}${path}${parsedUrl.hash}` as LocalizedUrl<T, L>;\n }\n\n const { prefix } = getPrefix(currentLocale, {\n defaultLocale,\n mode,\n locales,\n domains,\n });\n\n let localizedPath = `/${prefix}${translatedPathname}`.replace(/\\/+/g, '/');\n\n if (localizedPath.length > 1 && localizedPath.endsWith('/')) {\n localizedPath = localizedPath.slice(0, -1);\n }\n\n return `${baseUrl}${localizedPath}${parsedUrl.search}${parsedUrl.hash}` as LocalizedUrl<\n T,\n L\n >;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,MAAa,mBAIX,KACA,gBAAmB,sBAAsB,eACzC,UAA0B,CAAC,MACJ;CACvB,MAAM,EAAE,eAAe,MAAM,SAAS,SAAS,SAAS,kBACtD,qBAAqB,OAAO;CAE9B,MAAM,mBAAmB,qBAAqB,KAAK,OAAO;CAC1D,MAAM,eAAe,gBAAgB,SAAS,KAAK;CAEnD,IACE,EACE,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,gBAExC,SAAS,aAET,OAAO,iBACL,iBAAiB,kBAAkB,QAAW,YAAY,GAC1D,eACA,YACF,CAAC,CAAC;CAGJ,MAAM,gBAAgB,mBAAmB,gBAAgB;CACzD,MAAM,YAAY,gBACd,IAAI,IAAI,gBAAgB,IACxB,IAAI,IAAI,kBAAkB,oBAAoB;CAElD,MAAM,qBAAqB,iBACzB,iBAAiB,UAAU,UAAU,QAAW,YAAY,GAC5D,eACA,YACF,CAAC,CAAC;CAeF,MAAM,0BACJ,QAAQ,IAAI,6BAA6B,UACrC,kBACE,kBACG,gBAAgB,UAAU,WAAW,YACrC,OAAO,WAAW,cACf,QAAQ,UAAU,WAClB,WACJ,EACJ,KAAK,OACL;CAEN,MAAM,eACJ,QAAQ,IAAI,6BAA6B,UACrC,UAAU,iBACV;CAEN,MAAM,uBAAuB,eACzB,kBAAkB,YAAY,IAC9B;CASJ,MAAM,mBAJJ,yBAAyB,SACxB,4BAA4B,QAC3B,yBAAyB,4BAGV,eAAe,gBAAgB,YAAY,IAAI;CAElE,MAAM,UAAU,mBACZ,mBACA,gBACE,GAAG,UAAU,SAAS,IAAI,UAAU,SACpC;CAGN,IACE,EACE,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,oBAExC,SAAS,iBACT;EACA,MAAM,eAAe,IAAI,gBAAgB,UAAU,MAAM;EAEzD,aAAa,IAAI,UAAU,cAAc,SAAS,CAAC;EAEnD,MAAM,cAAc,aAAa,SAAS;EAK1C,OAAO,GAAG,UAJG,cACT,GAAG,mBAAmB,GAAG,gBACzB,qBAEuB,UAAU;CACvC;CAEA,MAAM,EAAE,WAAW,UAAU,eAAe;EAC1C;EACA;EACA;EACA;CACF,CAAC;CAED,IAAI,gBAAgB,IAAI,SAAS,qBAAqB,QAAQ,QAAQ,GAAG;CAEzE,IAAI,cAAc,SAAS,KAAK,cAAc,SAAS,GAAG,GACxD,gBAAgB,cAAc,MAAM,GAAG,EAAE;CAG3C,OAAO,GAAG,UAAU,gBAAgB,UAAU,SAAS,UAAU;AAInE"}
1
+ {"version":3,"file":"getLocalizedUrl.mjs","names":[],"sources":["../../../src/localization/getLocalizedUrl.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport type {\n LocalesValues,\n LocalizedUrl,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getDomainHostname, getDomainOrigin } from './domainUtils';\nimport { getLocalizedPath } from './getLocalizedPath';\nimport { type RoutingOptions, resolveRoutingConfig } from './getPrefix';\n\n/**\n * Generate URL by prefixing the given URL with the referenced locale or adding search parameters\n * based on the routing mode. Handles both absolute and relative URLs appropriately.\n *\n * This function gets the locales, default locale, and routing mode from the configuration if not provided.\n *\n * The path itself is localized by {@link getLocalizedPath}; this function only\n * resolves the origin to put in front of it — the one of an absolute input, or\n * the domain mapped to the target locale.\n *\n * Example:\n *\n * ```ts\n * // prefix-no-default mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'prefix-no-default' });\n * // Returns '/fr/about' for the French locale\n * // Returns '/about' for the English locale (default)\n *\n * // prefix-all mode\n * getLocalizedUrl('/about', 'en', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'prefix-all' });\n * // Returns '/en/about' for the English locale\n * // Returns '/fr/about' for the French locale\n *\n * // search-params mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'search-params' });\n * // Returns '/about?locale=fr' for the French locale\n *\n * // no-prefix mode\n * getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'no-prefix' });\n * // Returns '/about' for any locale\n * ```\n *\n * @param url - The original URL string to be processed.\n * @param currentLocale - The current locale.\n * @param options - Configuration options\n * @param options.locales - Optional array of supported locales. Defaults to configured locales.\n * @param options.defaultLocale - The default locale. Defaults to configured default locale.\n * @param options.mode - URL routing mode for locale handling. Defaults to configured mode.\n * @param options.currentDomain - Hostname of the page being rendered. Used to decide\n * whether to emit a relative URL (same domain) or an absolute URL (cross-domain).\n * Auto-detected from the input URL or `window.location` when omitted.\n * @returns The localized URL for the current locale.\n */\n/**\n * The return type is narrowed to a template-literal type when both `url` and\n * `currentLocale` are string literals and the routing mode / defaultLocale are\n * not overridden via `options`.\n */\nexport const getLocalizedUrl = <\n const T extends string,\n const L extends LocalesValues = ResolvedDefaultLocale,\n>(\n url: T,\n currentLocale: L = internationalization?.defaultLocale as L,\n options: RoutingOptions = {}\n): LocalizedUrl<T, L> => {\n const { domains, currentDomain } = resolveRoutingConfig(options);\n\n const isAbsoluteUrl = checkIsURLAbsolute(url);\n const parsedUrl = isAbsoluteUrl ? new URL(url) : new URL(url, 'http://e.com');\n\n // ── Domain routing ────────────────────────────────────────────────────────\n // Resolve the \"current\" hostname so we can choose between a relative URL\n // (same domain) and an absolute URL (cross-domain).\n //\n // Detection priority:\n // 1. Explicit `currentDomain` option passed by the caller.\n // 2. Hostname extracted from an absolute input URL.\n // 3. `window.location.hostname` in browser environments.\n // When none of these is available we fall back to generating an absolute\n // URL for domain-mapped locales (safe for SSR/static generation): the\n // locale prefix may already be stripped for domain-exclusive locales, so a\n // relative URL would silently point to the wrong locale on the current\n // domain.\n const detectedCurrentHostname =\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false'\n ? getDomainHostname(\n currentDomain ??\n (isAbsoluteUrl ? parsedUrl.hostname : undefined) ??\n (typeof window !== 'undefined'\n ? window?.location?.hostname\n : undefined) ??\n ''\n ) || null\n : null;\n\n const localeDomain =\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false'\n ? domains?.[currentLocale as LocalesValues]\n : undefined;\n\n const localeDomainHostname = localeDomain\n ? getDomainHostname(localeDomain)\n : null;\n\n // Prepend the locale's domain when it differs from the current hostname,\n // or when the current hostname is unknown (SSR without `currentDomain`).\n const isCrossDomain =\n localeDomainHostname !== null &&\n (detectedCurrentHostname === null ||\n localeDomainHostname !== detectedCurrentHostname);\n\n const normalizedDomain =\n isCrossDomain && localeDomain ? getDomainOrigin(localeDomain) : null;\n\n const baseUrl = normalizedDomain\n ? normalizedDomain\n : isAbsoluteUrl\n ? `${parsedUrl.protocol}//${parsedUrl.host}`\n : '';\n // ─────────────────────────────────────────────────────────────────────────\n\n const localizedPath = getLocalizedPath(\n `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`,\n currentLocale,\n options\n );\n\n return `${baseUrl}${localizedPath}` as LocalizedUrl<T, L>;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,mBAIX,KACA,gBAAmB,sBAAsB,eACzC,UAA0B,CAAC,MACJ;CACvB,MAAM,EAAE,SAAS,kBAAkB,qBAAqB,OAAO;CAE/D,MAAM,gBAAgB,mBAAmB,GAAG;CAC5C,MAAM,YAAY,gBAAgB,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,cAAc;CAe5E,MAAM,0BACJ,QAAQ,IAAI,6BAA6B,UACrC,kBACE,kBACG,gBAAgB,UAAU,WAAW,YACrC,OAAO,WAAW,cACf,QAAQ,UAAU,WAClB,WACJ,EACJ,KAAK,OACL;CAEN,MAAM,eACJ,QAAQ,IAAI,6BAA6B,UACrC,UAAU,iBACV;CAEN,MAAM,uBAAuB,eACzB,kBAAkB,YAAY,IAC9B;CASJ,MAAM,mBAJJ,yBAAyB,SACxB,4BAA4B,QAC3B,yBAAyB,4BAGV,eAAe,gBAAgB,YAAY,IAAI;CAelE,OAAO,GAbS,mBACZ,mBACA,gBACE,GAAG,UAAU,SAAS,IAAI,UAAU,SACpC,KAGgB,iBACpB,GAAG,UAAU,WAAW,UAAU,SAAS,UAAU,QACrD,eACA,OAG8B;AAClC"}
@@ -0,0 +1,42 @@
1
+ import { describe, expectTypeOf, it } from "vitest";
2
+
3
+ //#region src/localization/getLocalizedUrl.test-d.ts
4
+ /**
5
+ * `LocalizedUrl` mirrors what `getLocalizedUrl` builds at runtime. The routing
6
+ * mode, default locale and declared locales are passed explicitly here — this
7
+ * package declares none of them, so the registry defaults would collapse every
8
+ * locale onto the default one.
9
+ */
10
+ describe("LocalizedUrl", () => {
11
+ it("should prefix a path with a non-default locale", () => {
12
+ expectTypeOf().toEqualTypeOf();
13
+ });
14
+ it("should collapse the root to a bare prefix, never a trailing slash", () => {
15
+ expectTypeOf().toEqualTypeOf();
16
+ });
17
+ it("should leave the default locale unprefixed", () => {
18
+ expectTypeOf().toEqualTypeOf();
19
+ expectTypeOf().toEqualTypeOf();
20
+ });
21
+ it("should drop the trailing slash of a non-root path", () => {
22
+ expectTypeOf().toEqualTypeOf();
23
+ });
24
+ it("should replace an existing locale segment", () => {
25
+ expectTypeOf().toEqualTypeOf();
26
+ expectTypeOf().toEqualTypeOf();
27
+ });
28
+ it("should prefix the pathname of an absolute URL, not its origin", () => {
29
+ expectTypeOf().toEqualTypeOf();
30
+ expectTypeOf().toEqualTypeOf();
31
+ });
32
+ it("should prefix every locale in prefix-all mode", () => {
33
+ expectTypeOf().toEqualTypeOf();
34
+ expectTypeOf().toEqualTypeOf();
35
+ });
36
+ it("should strip the locale in no-prefix mode", () => {
37
+ expectTypeOf().toEqualTypeOf();
38
+ });
39
+ });
40
+
41
+ //#endregion
42
+ //# sourceMappingURL=getLocalizedUrl.test-d.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getLocalizedUrl.test-d.mjs","names":[],"sources":["../../../src/localization/getLocalizedUrl.test-d.ts"],"sourcesContent":["import type { LocalizedUrl } from '@intlayer/types/module_augmentation';\nimport { describe, expectTypeOf, it } from 'vitest';\n\n/**\n * `LocalizedUrl` mirrors what `getLocalizedUrl` builds at runtime. The routing\n * mode, default locale and declared locales are passed explicitly here — this\n * package declares none of them, so the registry defaults would collapse every\n * locale onto the default one.\n */\ndescribe('LocalizedUrl', () => {\n type Locales = 'en' | 'fr' | 'es';\n type Url<Path extends string, L extends Locales> = LocalizedUrl<\n Path,\n L,\n 'prefix-no-default',\n 'en',\n Locales\n >;\n\n it('should prefix a path with a non-default locale', () => {\n expectTypeOf<Url<'/about', 'es'>>().toEqualTypeOf<'/es/about'>();\n });\n\n it('should collapse the root to a bare prefix, never a trailing slash', () => {\n expectTypeOf<Url<'/', 'es'>>().toEqualTypeOf<'/es'>();\n });\n\n it('should leave the default locale unprefixed', () => {\n expectTypeOf<Url<'/about', 'en'>>().toEqualTypeOf<'/about'>();\n expectTypeOf<Url<'/', 'en'>>().toEqualTypeOf<'/'>();\n });\n\n it('should drop the trailing slash of a non-root path', () => {\n expectTypeOf<Url<'/about/', 'fr'>>().toEqualTypeOf<'/fr/about'>();\n });\n\n it('should replace an existing locale segment', () => {\n expectTypeOf<Url<'/en/about', 'fr'>>().toEqualTypeOf<'/fr/about'>();\n expectTypeOf<Url<'/fr', 'es'>>().toEqualTypeOf<'/es'>();\n });\n\n it('should prefix the pathname of an absolute URL, not its origin', () => {\n expectTypeOf<\n Url<'https://example.com/about', 'fr'>\n >().toEqualTypeOf<'https://example.com/fr/about'>();\n expectTypeOf<\n Url<'https://example.com/', 'fr'>\n >().toEqualTypeOf<'https://example.com/fr'>();\n });\n\n it('should prefix every locale in prefix-all mode', () => {\n expectTypeOf<\n LocalizedUrl<'/', 'en', 'prefix-all', 'en', Locales>\n >().toEqualTypeOf<'/en'>();\n expectTypeOf<\n LocalizedUrl<'/about', 'en', 'prefix-all', 'en', Locales>\n >().toEqualTypeOf<'/en/about'>();\n });\n\n it('should strip the locale in no-prefix mode', () => {\n expectTypeOf<\n LocalizedUrl<'/fr/about', 'fr', 'no-prefix', 'en', Locales>\n >().toEqualTypeOf<'/about'>();\n });\n});\n"],"mappings":";;;;;;;;;AASA,SAAS,sBAAsB;CAU7B,GAAG,wDAAwD;EACzD,aAAkC,CAAC,CAAC,cAA2B;CACjE,CAAC;CAED,GAAG,2EAA2E;EAC5E,aAA6B,CAAC,CAAC,cAAqB;CACtD,CAAC;CAED,GAAG,oDAAoD;EACrD,aAAkC,CAAC,CAAC,cAAwB;EAC5D,aAA6B,CAAC,CAAC,cAAmB;CACpD,CAAC;CAED,GAAG,2DAA2D;EAC5D,aAAmC,CAAC,CAAC,cAA2B;CAClE,CAAC;CAED,GAAG,mDAAmD;EACpD,aAAqC,CAAC,CAAC,cAA2B;EAClE,aAA+B,CAAC,CAAC,cAAqB;CACxD,CAAC;CAED,GAAG,uEAAuE;EACxE,aAEE,CAAC,CAAC,cAA8C;EAClD,aAEE,CAAC,CAAC,cAAwC;CAC9C,CAAC;CAED,GAAG,uDAAuD;EACxD,aAEE,CAAC,CAAC,cAAqB;EACzB,aAEE,CAAC,CAAC,cAA2B;CACjC,CAAC;CAED,GAAG,mDAAmD;EACpD,aAEE,CAAC,CAAC,cAAwB;CAC9B,CAAC;AACH,CAAC"}
@@ -44,7 +44,7 @@ const getPathWithoutLocale = (inputUrl, locales = internationalization?.locales)
44
44
  const isAbsoluteUrl = checkIsURLAbsolute(inputUrl);
45
45
  let fixedInputUrl = inputUrl;
46
46
  if (inputUrl?.endsWith("/")) fixedInputUrl = inputUrl.slice(0, -1);
47
- const url = isAbsoluteUrl ? new URL(fixedInputUrl) : new URL(fixedInputUrl, "http://example.com");
47
+ const url = isAbsoluteUrl ? new URL(fixedInputUrl) : new URL(fixedInputUrl, "http://e.com");
48
48
  const pathname = url.pathname;
49
49
  if (!pathname.startsWith("/")) url.pathname = `/${pathname}`;
50
50
  if (!TREE_SHAKE_PREFIX_MODES) {
@@ -63,7 +63,7 @@ const getPathWithoutLocale = (inputUrl, locales = internationalization?.locales)
63
63
  }
64
64
  }
65
65
  if (isAbsoluteUrl) return url.toString();
66
- return url.toString().replace("http://example.com", "");
66
+ return url.toString().replace("http://e.com", "");
67
67
  };
68
68
 
69
69
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"getPathWithoutLocale.mjs","names":[],"sources":["../../../src/localization/getPathWithoutLocale.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\n/**\n * True when the build-time routing mode is known and is not a prefix-based\n * mode (neither 'prefix-all' nor 'prefix-no-default').\n */\nconst TREE_SHAKE_PREFIX_MODES =\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all' &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-no-default';\n\n/**\n * True when the build-time routing mode is known and is NOT 'search-params'.\n */\nconst TREE_SHAKE_SEARCH_PARAMS =\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params';\n\nimport type {\n DeclaredLocales,\n LocalesValues,\n PathWithoutLocale,\n} from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\n\n/**\n * Removes the locale segment from the given URL or pathname if present.\n * Also removes locale from search parameters if present.\n *\n * This function get the locales from the configuration if not provided.\n *\n * Example:\n *\n * ```ts\n * getPathWithoutLocale('/en/dashboard') // Returns '/dashboard'\n * getPathWithoutLocale('/fr/dashboard') // Returns '/dashboard'\n * getPathWithoutLocale('/dashboard') // Returns '/dashboard'\n * getPathWithoutLocale('dashboard') // Returns 'dashboard'\n * getPathWithoutLocale('/dashboard?locale=fr') // Returns '/dashboard'\n * getPathWithoutLocale('https://example.com/en/dashboard') // Returns 'https://example.com/dashboard'\n * getPathWithoutLocale('https://example.com/fr/dashboard') // Returns 'https://example.com/dashboard'\n * getPathWithoutLocale('https://example.com/dashboard') // Returns 'https://example.com/dashboard'\n * getPathWithoutLocale('https://example.com/dashboard?locale=fr') // Returns 'https://example.com/dashboard'\n * ```\n *\n * @param inputUrl - The complete URL string or pathname to process.\n * @param locales - Optional array of supported locales. Defaults to `localesDefault`.\n * @returns The URL string or pathname without the locale segment or locale search parameter.\n */\n/**\n * The return type is narrowed to a template-literal type when both `inputUrl`\n * and `locales` are string literals known at compile time. Absolute URLs fall\n * back to `string`.\n */\nexport const getPathWithoutLocale = <\n const T extends string,\n const L extends LocalesValues = DeclaredLocales,\n>(\n inputUrl: T,\n locales: L[] = internationalization?.locales as L[]\n): PathWithoutLocale<T, L> => {\n // Determine if the original URL is absolute (includes protocol)\n const isAbsoluteUrl = checkIsURLAbsolute(inputUrl);\n\n let fixedInputUrl = inputUrl;\n\n if (inputUrl?.endsWith('/')) {\n fixedInputUrl = inputUrl.slice(0, -1);\n }\n\n // Initialize a URL object if the URL is absolute\n // For relative URLs, use a dummy base to leverage the URL API\n const url = isAbsoluteUrl\n ? new URL(fixedInputUrl)\n : new URL(fixedInputUrl, 'http://example.com');\n\n const pathname = url.pathname;\n\n // Ensure the pathname starts with '/'\n if (!pathname.startsWith('/')) {\n // If not, return the URL as is\n url.pathname = `/${pathname}`;\n }\n\n // Only strip locale path prefix in prefix-based routing modes\n if (!TREE_SHAKE_PREFIX_MODES) {\n // Split the pathname to extract the first segment\n const pathSegments = pathname.split('/');\n const firstSegment = pathSegments[1]; // The segment after the first '/'\n\n // Check if the first segment is a supported locale\n if (locales?.includes(firstSegment as L)) {\n // Remove the locale segment from the pathname\n pathSegments.splice(1, 1); // Remove the first segment\n\n // Reconstruct the pathname\n const newPathname = pathSegments.join('/') ?? '/';\n url.pathname = newPathname;\n }\n }\n\n // Only strip locale from search parameters in search-params routing mode\n if (!TREE_SHAKE_SEARCH_PARAMS) {\n const searchParams = new URLSearchParams(url.search);\n if (searchParams.has('locale')) {\n searchParams.delete('locale');\n url.search = searchParams.toString();\n }\n }\n\n if (isAbsoluteUrl) {\n return url.toString() as PathWithoutLocale<T, L>;\n }\n\n return url.toString().replace('http://example.com', '') as PathWithoutLocale<\n T,\n L\n >;\n};\n"],"mappings":";;;;;;;;AAUA,MAAM,0BACJ,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,gBACtC,QAAQ,IAAI,0BAA0B;;;;AAKxC,MAAM,2BACJ,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCxC,MAAa,wBAIX,UACA,UAAe,sBAAsB,YACT;CAE5B,MAAM,gBAAgB,mBAAmB,QAAQ;CAEjD,IAAI,gBAAgB;CAEpB,IAAI,UAAU,SAAS,GAAG,GACxB,gBAAgB,SAAS,MAAM,GAAG,EAAE;CAKtC,MAAM,MAAM,gBACR,IAAI,IAAI,aAAa,IACrB,IAAI,IAAI,eAAe,oBAAoB;CAE/C,MAAM,WAAW,IAAI;CAGrB,IAAI,CAAC,SAAS,WAAW,GAAG,GAE1B,IAAI,WAAW,IAAI;CAIrB,IAAI,CAAC,yBAAyB;EAE5B,MAAM,eAAe,SAAS,MAAM,GAAG;EACvC,MAAM,eAAe,aAAa;EAGlC,IAAI,SAAS,SAAS,YAAiB,GAAG;GAExC,aAAa,OAAO,GAAG,CAAC;GAIxB,IAAI,WADgB,aAAa,KAAK,GAAG,KAAK;EAEhD;CACF;CAGA,IAAI,CAAC,0BAA0B;EAC7B,MAAM,eAAe,IAAI,gBAAgB,IAAI,MAAM;EACnD,IAAI,aAAa,IAAI,QAAQ,GAAG;GAC9B,aAAa,OAAO,QAAQ;GAC5B,IAAI,SAAS,aAAa,SAAS;EACrC;CACF;CAEA,IAAI,eACF,OAAO,IAAI,SAAS;CAGtB,OAAO,IAAI,SAAS,CAAC,CAAC,QAAQ,sBAAsB,EAAE;AAIxD"}
1
+ {"version":3,"file":"getPathWithoutLocale.mjs","names":[],"sources":["../../../src/localization/getPathWithoutLocale.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\n/**\n * True when the build-time routing mode is known and is not a prefix-based\n * mode (neither 'prefix-all' nor 'prefix-no-default').\n */\nconst TREE_SHAKE_PREFIX_MODES =\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all' &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-no-default';\n\n/**\n * True when the build-time routing mode is known and is NOT 'search-params'.\n */\nconst TREE_SHAKE_SEARCH_PARAMS =\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params';\n\nimport type {\n DeclaredLocales,\n LocalesValues,\n PathWithoutLocale,\n} from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\n\n/**\n * Removes the locale segment from the given URL or pathname if present.\n * Also removes locale from search parameters if present.\n *\n * This function get the locales from the configuration if not provided.\n *\n * Example:\n *\n * ```ts\n * getPathWithoutLocale('/en/dashboard') // Returns '/dashboard'\n * getPathWithoutLocale('/fr/dashboard') // Returns '/dashboard'\n * getPathWithoutLocale('/dashboard') // Returns '/dashboard'\n * getPathWithoutLocale('dashboard') // Returns 'dashboard'\n * getPathWithoutLocale('/dashboard?locale=fr') // Returns '/dashboard'\n * getPathWithoutLocale('https://example.com/en/dashboard') // Returns 'https://example.com/dashboard'\n * getPathWithoutLocale('https://example.com/fr/dashboard') // Returns 'https://example.com/dashboard'\n * getPathWithoutLocale('https://example.com/dashboard') // Returns 'https://example.com/dashboard'\n * getPathWithoutLocale('https://example.com/dashboard?locale=fr') // Returns 'https://example.com/dashboard'\n * ```\n *\n * @param inputUrl - The complete URL string or pathname to process.\n * @param locales - Optional array of supported locales. Defaults to `localesDefault`.\n * @returns The URL string or pathname without the locale segment or locale search parameter.\n */\n/**\n * The return type is narrowed to a template-literal type when both `inputUrl`\n * and `locales` are string literals known at compile time. Absolute URLs fall\n * back to `string`.\n */\nexport const getPathWithoutLocale = <\n const T extends string,\n const L extends LocalesValues = DeclaredLocales,\n>(\n inputUrl: T,\n locales: L[] = internationalization?.locales as L[]\n): PathWithoutLocale<T, L> => {\n // Determine if the original URL is absolute (includes protocol)\n const isAbsoluteUrl = checkIsURLAbsolute(inputUrl);\n\n let fixedInputUrl = inputUrl;\n\n if (inputUrl?.endsWith('/')) {\n fixedInputUrl = inputUrl.slice(0, -1);\n }\n\n // Initialize a URL object if the URL is absolute\n // For relative URLs, use a dummy base to leverage the URL API\n const url = isAbsoluteUrl\n ? new URL(fixedInputUrl)\n : new URL(fixedInputUrl, 'http://e.com');\n\n const pathname = url.pathname;\n\n // Ensure the pathname starts with '/'\n if (!pathname.startsWith('/')) {\n // If not, return the URL as is\n url.pathname = `/${pathname}`;\n }\n\n // Only strip locale path prefix in prefix-based routing modes\n if (!TREE_SHAKE_PREFIX_MODES) {\n // Split the pathname to extract the first segment\n const pathSegments = pathname.split('/');\n const firstSegment = pathSegments[1]; // The segment after the first '/'\n\n // Check if the first segment is a supported locale\n if (locales?.includes(firstSegment as L)) {\n // Remove the locale segment from the pathname\n pathSegments.splice(1, 1); // Remove the first segment\n\n // Reconstruct the pathname\n const newPathname = pathSegments.join('/') ?? '/';\n url.pathname = newPathname;\n }\n }\n\n // Only strip locale from search parameters in search-params routing mode\n if (!TREE_SHAKE_SEARCH_PARAMS) {\n const searchParams = new URLSearchParams(url.search);\n if (searchParams.has('locale')) {\n searchParams.delete('locale');\n url.search = searchParams.toString();\n }\n }\n\n if (isAbsoluteUrl) {\n return url.toString() as PathWithoutLocale<T, L>;\n }\n\n return url.toString().replace('http://e.com', '') as PathWithoutLocale<T, L>;\n};\n"],"mappings":";;;;;;;;AAUA,MAAM,0BACJ,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B,gBACtC,QAAQ,IAAI,0BAA0B;;;;AAKxC,MAAM,2BACJ,QAAQ,IAAI,yBACZ,QAAQ,IAAI,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCxC,MAAa,wBAIX,UACA,UAAe,sBAAsB,YACT;CAE5B,MAAM,gBAAgB,mBAAmB,QAAQ;CAEjD,IAAI,gBAAgB;CAEpB,IAAI,UAAU,SAAS,GAAG,GACxB,gBAAgB,SAAS,MAAM,GAAG,EAAE;CAKtC,MAAM,MAAM,gBACR,IAAI,IAAI,aAAa,IACrB,IAAI,IAAI,eAAe,cAAc;CAEzC,MAAM,WAAW,IAAI;CAGrB,IAAI,CAAC,SAAS,WAAW,GAAG,GAE1B,IAAI,WAAW,IAAI;CAIrB,IAAI,CAAC,yBAAyB;EAE5B,MAAM,eAAe,SAAS,MAAM,GAAG;EACvC,MAAM,eAAe,aAAa;EAGlC,IAAI,SAAS,SAAS,YAAiB,GAAG;GAExC,aAAa,OAAO,GAAG,CAAC;GAIxB,IAAI,WADgB,aAAa,KAAK,GAAG,KAAK;EAEhD;CACF;CAGA,IAAI,CAAC,0BAA0B;EAC7B,MAAM,eAAe,IAAI,gBAAgB,IAAI,MAAM;EACnD,IAAI,aAAa,IAAI,QAAQ,GAAG;GAC9B,aAAa,OAAO,QAAQ;GAC5B,IAAI,SAAS,aAAa,SAAS;EACrC;CACF;CAEA,IAAI,eACF,OAAO,IAAI,SAAS;CAGtB,OAAO,IAAI,SAAS,CAAC,CAAC,QAAQ,gBAAgB,EAAE;AAClD"}
@@ -16,6 +16,19 @@ const resolveRoutingConfig = (options = {}) => ({
16
16
  ...options
17
17
  });
18
18
  /**
19
+ * Narrows an arbitrary string to a declared locale by membership test.
20
+ *
21
+ * `locales` is typed as `DeclaredLocales[]`, so a plain `includes` rejects the
22
+ * `string` values that actually need testing (a URL segment, a query param, a
23
+ * stored value). This keeps the check honest and hands back a narrowed type
24
+ * instead of forcing a cast at every call site.
25
+ *
26
+ * @param value - The candidate locale, from a URL segment / storage / header.
27
+ * @param locales - The locales declared by the configuration.
28
+ * @returns Whether `value` is one of the declared locales.
29
+ */
30
+ const isDeclaredLocale = (value, locales) => !!value && (locales ?? internationalization.locales).includes(value);
31
+ /**
19
32
  * Determines the URL prefix for a given locale based on the routing mode configuration.
20
33
  *
21
34
  * Example:
@@ -50,7 +63,7 @@ const resolveRoutingConfig = (options = {}) => ({
50
63
  */
51
64
  const getPrefix = (locale, options = {}) => {
52
65
  const { defaultLocale, mode, locales, domains } = resolveRoutingConfig(options);
53
- if (process.env.INTLAYER_ROUTING_MODE && process.env.INTLAYER_ROUTING_MODE !== "prefix-all" && process.env.INTLAYER_ROUTING_MODE !== "prefix-no-default" || !locale || !locales.includes(locale)) return {
66
+ if (process.env.INTLAYER_ROUTING_MODE && process.env.INTLAYER_ROUTING_MODE !== "prefix-all" && process.env.INTLAYER_ROUTING_MODE !== "prefix-no-default" || !locale || !isDeclaredLocale(locale, locales)) return {
54
67
  prefix: "",
55
68
  localePrefix: void 0
56
69
  };
@@ -69,5 +82,5 @@ const getPrefix = (locale, options = {}) => {
69
82
  };
70
83
 
71
84
  //#endregion
72
- export { getPrefix, resolveRoutingConfig };
85
+ export { getPrefix, isDeclaredLocale, resolveRoutingConfig };
73
86
  //# sourceMappingURL=getPrefix.mjs.map