@intlayer/core 9.4.0-canary.0 → 9.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cjs/dictionaryManipulator/index.cjs +5 -2
- package/dist/cjs/index.cjs +15 -3
- package/dist/cjs/interpreter/dictionaryTransformCache.cjs +111 -0
- package/dist/cjs/interpreter/dictionaryTransformCache.cjs.map +1 -0
- package/dist/cjs/interpreter/getDictionary.cjs +8 -8
- package/dist/cjs/interpreter/getDictionary.cjs.map +1 -1
- package/dist/cjs/interpreter/getDictionaryAsync.cjs +79 -0
- package/dist/cjs/interpreter/getDictionaryAsync.cjs.map +1 -0
- package/dist/cjs/interpreter/getIntlayer.cjs +22 -15
- package/dist/cjs/interpreter/getIntlayer.cjs.map +1 -1
- package/dist/cjs/interpreter/getIntlayerAsync.cjs +36 -0
- package/dist/cjs/interpreter/getIntlayerAsync.cjs.map +1 -0
- package/dist/cjs/interpreter/index.cjs +10 -1
- package/dist/cjs/localization/generateSitemap.cjs +57 -27
- package/dist/cjs/localization/generateSitemap.cjs.map +1 -1
- package/dist/cjs/localization/getLocalizedPath.cjs +3 -0
- package/dist/cjs/localization/getLocalizedPath.cjs.map +1 -1
- package/dist/cjs/localization/getLocalizedPath.test-d.cjs +16 -0
- package/dist/cjs/localization/getLocalizedPath.test-d.cjs.map +1 -1
- package/dist/cjs/localization/getLocalizedUrl.cjs +8 -0
- package/dist/cjs/localization/getLocalizedUrl.cjs.map +1 -1
- package/dist/cjs/localization/getLocalizedUrl.test-d.cjs +36 -0
- package/dist/cjs/localization/getLocalizedUrl.test-d.cjs.map +1 -1
- package/dist/cjs/localization/getPrefix.cjs.map +1 -1
- package/dist/cjs/markdown/constants.cjs +24 -3
- package/dist/cjs/markdown/constants.cjs.map +1 -1
- package/dist/esm/dictionaryManipulator/index.mjs +4 -3
- package/dist/esm/index.mjs +7 -3
- package/dist/esm/interpreter/dictionaryTransformCache.mjs +107 -0
- package/dist/esm/interpreter/dictionaryTransformCache.mjs.map +1 -0
- package/dist/esm/interpreter/getDictionary.mjs +9 -9
- package/dist/esm/interpreter/getDictionary.mjs.map +1 -1
- package/dist/esm/interpreter/getDictionaryAsync.mjs +78 -0
- package/dist/esm/interpreter/getDictionaryAsync.mjs.map +1 -0
- package/dist/esm/interpreter/getIntlayer.mjs +22 -15
- package/dist/esm/interpreter/getIntlayer.mjs.map +1 -1
- package/dist/esm/interpreter/getIntlayerAsync.mjs +35 -0
- package/dist/esm/interpreter/getIntlayerAsync.mjs.map +1 -0
- package/dist/esm/interpreter/index.mjs +4 -1
- package/dist/esm/localization/generateSitemap.mjs +57 -27
- package/dist/esm/localization/generateSitemap.mjs.map +1 -1
- package/dist/esm/localization/getLocalizedPath.mjs +3 -0
- package/dist/esm/localization/getLocalizedPath.mjs.map +1 -1
- package/dist/esm/localization/getLocalizedPath.test-d.mjs +16 -0
- package/dist/esm/localization/getLocalizedPath.test-d.mjs.map +1 -1
- package/dist/esm/localization/getLocalizedUrl.mjs +8 -0
- package/dist/esm/localization/getLocalizedUrl.mjs.map +1 -1
- package/dist/esm/localization/getLocalizedUrl.test-d.mjs +36 -0
- package/dist/esm/localization/getLocalizedUrl.test-d.mjs.map +1 -1
- package/dist/esm/localization/getPrefix.mjs.map +1 -1
- package/dist/esm/markdown/constants.mjs +24 -3
- package/dist/esm/markdown/constants.mjs.map +1 -1
- package/dist/types/dictionaryManipulator/index.d.ts +3 -2
- package/dist/types/index.d.ts +7 -3
- package/dist/types/interpreter/dictionaryTransformCache.d.ts +42 -0
- package/dist/types/interpreter/dictionaryTransformCache.d.ts.map +1 -0
- package/dist/types/interpreter/getContent/plugins.d.ts +2 -2
- package/dist/types/interpreter/getDictionary.d.ts +0 -5
- package/dist/types/interpreter/getDictionary.d.ts.map +1 -1
- package/dist/types/interpreter/getDictionaryAsync.d.ts +34 -0
- package/dist/types/interpreter/getDictionaryAsync.d.ts.map +1 -0
- package/dist/types/interpreter/getIntlayer.d.ts.map +1 -1
- package/dist/types/interpreter/getIntlayerAsync.d.ts +36 -0
- package/dist/types/interpreter/getIntlayerAsync.d.ts.map +1 -0
- package/dist/types/interpreter/index.d.ts +5 -2
- package/dist/types/localization/generateSitemap.d.ts +34 -9
- package/dist/types/localization/generateSitemap.d.ts.map +1 -1
- package/dist/types/localization/getLocalizedPath.d.ts +3 -0
- package/dist/types/localization/getLocalizedPath.d.ts.map +1 -1
- package/dist/types/localization/getLocalizedUrl.d.ts +8 -0
- package/dist/types/localization/getLocalizedUrl.d.ts.map +1 -1
- package/dist/types/localization/getPrefix.d.ts +9 -4
- package/dist/types/localization/getPrefix.d.ts.map +1 -1
- package/dist/types/markdown/constants.d.ts +0 -1
- package/dist/types/markdown/constants.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getDictionaryTransformCacheKey, getPluginsCacheKey, readTransformCache, writeTransformCache } from "./dictionaryTransformCache.mjs";
|
|
1
2
|
import { getCondition } from "./getCondition.mjs";
|
|
2
3
|
import { deepTransformNode } from "./getContent/deepTransform.mjs";
|
|
3
4
|
import { findMatchingCondition, getEnumeration } from "./getEnumeration.mjs";
|
|
@@ -12,7 +13,9 @@ import { getTranslation } from "./getTranslation.mjs";
|
|
|
12
13
|
import { getInterpolableContent, isInterpolableWrapperNode, rebuildInterpolableContent, transformInterpolableNode } from "./interpolableNode.mjs";
|
|
13
14
|
import { conditionPlugin, enumerationPlugin, fallbackPlugin, filePlugin, genderPlugin, insertionPlugin, nestedPlugin, pluralPlugin, selectPlugin, translationPlugin } from "./getContent/plugins.mjs";
|
|
14
15
|
import { getBasePlugins, getContent } from "./getContent/getContent.mjs";
|
|
16
|
+
import { getDictionaryAsync } from "./getDictionaryAsync.mjs";
|
|
15
17
|
import { getHTML } from "./getHTML.mjs";
|
|
18
|
+
import { getIntlayerAsync } from "./getIntlayerAsync.mjs";
|
|
16
19
|
import { splitInsertionTemplate } from "./splitAndJoinInsertion.mjs";
|
|
17
20
|
|
|
18
|
-
export { conditionPlugin, deepTransformNode, enumerationPlugin, fallbackPlugin, filePlugin, findMatchingCondition, genderPlugin, getBasePlugins, getCondition, getContent, getDictionary, getEnumeration, getGender, getHTML, getInsertion, getInterpolableContent, getIntlayer, getNesting, getPlural, getSelect, getTranslation, insertionPlugin, isInterpolableWrapperNode, nestedPlugin, pluralPlugin, rebuildInterpolableContent, selectPlugin, splitInsertionTemplate, transformInterpolableNode, translationPlugin };
|
|
21
|
+
export { conditionPlugin, deepTransformNode, enumerationPlugin, fallbackPlugin, filePlugin, findMatchingCondition, genderPlugin, getBasePlugins, getCondition, getContent, getDictionary, getDictionaryAsync, getDictionaryTransformCacheKey, getEnumeration, getGender, getHTML, getInsertion, getInterpolableContent, getIntlayer, getIntlayerAsync, getNesting, getPluginsCacheKey, getPlural, getSelect, getTranslation, insertionPlugin, isInterpolableWrapperNode, nestedPlugin, pluralPlugin, readTransformCache, rebuildInterpolableContent, selectPlugin, splitInsertionTemplate, transformInterpolableNode, translationPlugin, writeTransformCache };
|
|
@@ -1,17 +1,45 @@
|
|
|
1
|
+
import { checkIsURLAbsolute } from "../utils/checkIsURLAbsolute.mjs";
|
|
1
2
|
import { resolveRoutingConfig } from "./getPrefix.mjs";
|
|
2
3
|
import { getMultilingualUrls } from "./getMultilingualUrls.mjs";
|
|
3
4
|
|
|
4
5
|
//#region src/localization/generateSitemap.ts
|
|
5
6
|
/**
|
|
6
|
-
* Returns whether xhtml:link alternate tags should be generated for the given
|
|
7
|
+
* Returns whether xhtml:link alternate tags should be generated for the given
|
|
8
|
+
* routing mode and domain map.
|
|
7
9
|
*
|
|
8
10
|
* Alternates are meaningful only when locale URLs are distinct:
|
|
9
|
-
* - 'no-prefix' produces identical URLs for all locales → no alternates
|
|
11
|
+
* - 'no-prefix' produces identical URLs for all locales → no alternates, unless
|
|
12
|
+
* `routing.domains` serves some locales from their own hostname
|
|
10
13
|
* - all other modes produce distinct URLs → alternates are generated
|
|
11
14
|
*/
|
|
12
|
-
const shouldIncludeAlternates = (mode, xhtmlLinks) => xhtmlLinks && mode !== "no-prefix";
|
|
15
|
+
const shouldIncludeAlternates = (mode, xhtmlLinks, domains) => xhtmlLinks && (mode !== "no-prefix" || hasConfiguredDomain(domains));
|
|
16
|
+
/** Whether at least one locale is mapped to a domain of its own. */
|
|
17
|
+
const hasConfiguredDomain = (domains) => Object.values(domains ?? {}).some((domain) => typeof domain === "string" && domain.length > 0);
|
|
13
18
|
/**
|
|
14
|
-
*
|
|
19
|
+
* Assembles a single `<url>` block from an absolute location and its metadata.
|
|
20
|
+
*
|
|
21
|
+
* @param location - The absolute URL to declare as `<loc>`.
|
|
22
|
+
* @param metadata - The optional `lastmod` / `changefreq` / `priority` values.
|
|
23
|
+
* @param alternateLines - Pre-rendered `<xhtml:link>` lines, empty when none.
|
|
24
|
+
* @returns A `<url>` XML string.
|
|
25
|
+
*/
|
|
26
|
+
const buildUrlEntry = (location, metadata, alternateLines) => {
|
|
27
|
+
const { lastmod, changefreq, priority } = metadata;
|
|
28
|
+
return [
|
|
29
|
+
" <url>",
|
|
30
|
+
` <loc>${location}</loc>`,
|
|
31
|
+
lastmod ? ` <lastmod>${lastmod}</lastmod>` : null,
|
|
32
|
+
changefreq ? ` <changefreq>${changefreq}</changefreq>` : null,
|
|
33
|
+
priority !== void 0 ? ` <priority>${priority}</priority>` : null,
|
|
34
|
+
...alternateLines,
|
|
35
|
+
" </url>"
|
|
36
|
+
].filter(Boolean).join("\n");
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Generates the `<url>` sitemap entries for the given path.
|
|
40
|
+
*
|
|
41
|
+
* Emits a single entry for the default locale, or — with `entryPerLocale` — one
|
|
42
|
+
* entry per localized URL, each repeating the same alternate set.
|
|
15
43
|
*
|
|
16
44
|
* Example:
|
|
17
45
|
*
|
|
@@ -30,35 +58,36 @@ const shouldIncludeAlternates = (mode, xhtmlLinks) => xhtmlLinks && mode !== "no
|
|
|
30
58
|
* // <loc>https://example.com/dashboard</loc>
|
|
31
59
|
* // <changefreq>weekly</changefreq>
|
|
32
60
|
* // <priority>0.8</priority>
|
|
33
|
-
* // <xhtml:link rel="alternate"
|
|
34
|
-
* // <xhtml:link rel="alternate"
|
|
35
|
-
* // <xhtml:link rel="alternate"
|
|
61
|
+
* // <xhtml:link rel="alternate" hreflang="en" href="https://example.com/dashboard"/>
|
|
62
|
+
* // <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/dashboard"/>
|
|
63
|
+
* // <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/dashboard"/>
|
|
36
64
|
* // </url>
|
|
37
65
|
* ```
|
|
38
66
|
*
|
|
39
|
-
* @param path - The canonical path to generate the
|
|
67
|
+
* @param path - The canonical path to generate the entries for.
|
|
40
68
|
* @param options - Configuration options.
|
|
41
|
-
* @returns
|
|
69
|
+
* @returns One or more `<url>` XML strings, joined by a newline.
|
|
42
70
|
*/
|
|
43
71
|
const generateSitemapUrl = (path, options) => {
|
|
44
|
-
const { siteUrl, changefreq, priority, lastmod, xhtmlLinks = true, ...routingOptions } = options;
|
|
72
|
+
const { siteUrl, changefreq, priority, lastmod, xhtmlLinks = true, entryPerLocale = false, ...routingOptions } = options;
|
|
45
73
|
const resolved = resolveRoutingConfig(routingOptions);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
lastmod
|
|
51
|
-
|
|
52
|
-
priority !== void 0 ? ` <priority>${priority}</priority>` : null
|
|
53
|
-
];
|
|
74
|
+
const defaultLocaleUrl = `${siteUrl}${path}`;
|
|
75
|
+
const metadata = {
|
|
76
|
+
changefreq,
|
|
77
|
+
priority,
|
|
78
|
+
lastmod
|
|
79
|
+
};
|
|
54
80
|
const hasFileExtension = /\.[a-z0-9]+$/i.test(path);
|
|
55
|
-
if (shouldIncludeAlternates(resolved.mode, xhtmlLinks) && !hasFileExtension)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
if (!(shouldIncludeAlternates(resolved.mode, xhtmlLinks, resolved.domains) && !hasFileExtension)) return buildUrlEntry(defaultLocaleUrl, metadata, []);
|
|
82
|
+
const alternates = getMultilingualUrls(path, routingOptions);
|
|
83
|
+
/**
|
|
84
|
+
* A locale served from its own domain already yields an absolute URL —
|
|
85
|
+
* prefixing it with `siteUrl` again would corrupt it.
|
|
86
|
+
*/
|
|
87
|
+
const toAbsoluteUrl = (localeUrl) => checkIsURLAbsolute(localeUrl) ? localeUrl : `${siteUrl}${localeUrl}`;
|
|
88
|
+
const alternateLines = [...Object.entries(alternates).map(([locale, localeUrl]) => ` <xhtml:link rel="alternate" hreflang="${locale}" href="${toAbsoluteUrl(localeUrl)}"/>`), ` <xhtml:link rel="alternate" hreflang="x-default" href="${defaultLocaleUrl}"/>`];
|
|
89
|
+
if (!entryPerLocale) return buildUrlEntry(defaultLocaleUrl, metadata, alternateLines);
|
|
90
|
+
return Object.values(alternates).map(toAbsoluteUrl).filter((localeUrl) => localeUrl.startsWith(siteUrl)).filter((localeUrl, index, urls) => urls.indexOf(localeUrl) === index).map((localeUrl) => buildUrlEntry(localeUrl, metadata, alternateLines)).join("\n");
|
|
62
91
|
};
|
|
63
92
|
/**
|
|
64
93
|
* Generates a full XML sitemap string from an array of URL entries.
|
|
@@ -89,13 +118,14 @@ const generateSitemapUrl = (path, options) => {
|
|
|
89
118
|
* @returns A full XML sitemap string.
|
|
90
119
|
*/
|
|
91
120
|
const generateSitemap = (entries, options) => {
|
|
92
|
-
const { siteUrl, xhtmlLinks = true, ...routingOptions } = options;
|
|
121
|
+
const { siteUrl, xhtmlLinks = true, entryPerLocale = false, ...routingOptions } = options;
|
|
93
122
|
const resolved = resolveRoutingConfig(routingOptions);
|
|
94
|
-
const includeAlternates = shouldIncludeAlternates(resolved.mode, xhtmlLinks);
|
|
123
|
+
const includeAlternates = shouldIncludeAlternates(resolved.mode, xhtmlLinks, resolved.domains);
|
|
95
124
|
const xmlEntries = entries.map((entry) => generateSitemapUrl(entry.path, {
|
|
96
125
|
...entry,
|
|
97
126
|
siteUrl,
|
|
98
127
|
xhtmlLinks,
|
|
128
|
+
entryPerLocale,
|
|
99
129
|
...routingOptions
|
|
100
130
|
}));
|
|
101
131
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateSitemap.mjs","names":[],"sources":["../../../src/localization/generateSitemap.ts"],"sourcesContent":["import type { RoutingConfig } from '@intlayer/types/config';\nimport type { LocalesValues } from '@intlayer/types/module_augmentation';\nimport { getMultilingualUrls } from './getMultilingualUrls';\nimport { type RoutingOptions, resolveRoutingConfig } from './getPrefix';\n\nexport type SitemapUrlEntry = {\n /** The canonical path, e.g. '/dashboard' */\n path: string;\n changefreq?: string;\n priority?: number;\n /** ISO date string, e.g. '2024-01-15' */\n lastmod?: string;\n};\n\nexport type GenerateSitemapOptions = {\n /** Base site URL without trailing slash, e.g. 'https://example.com' */\n siteUrl: string;\n /**\n * Whether to include xhtml:link alternate tags for multilingual support.\n *\n * When enabled, alternate links are only generated for modes where URLs\n * differ per locale:\n * - 'prefix-no-default': included\n * - 'prefix-all': included\n * - 'search-params': included\n * - 'no-prefix': excluded (all locales share the same URL)\n *\n * @default true\n */\n xhtmlLinks?: boolean;\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n rewrite?: RoutingConfig['rewrite'];\n};\n\n/**\n * Returns whether xhtml:link alternate tags should be generated for the given routing mode.\n *\n * Alternates are meaningful only when locale URLs are distinct:\n * - 'no-prefix' produces identical URLs for all locales → no alternates\n * - all other modes produce distinct URLs → alternates are generated\n */\nconst shouldIncludeAlternates = (\n mode: RoutingConfig['mode'],\n xhtmlLinks: boolean\n): boolean => xhtmlLinks && mode !== 'no-prefix';\n\n/**\n * Generates a single `<url>` sitemap entry for the given path.\n *\n * Example:\n *\n * ```ts\n * generateSitemapUrl('/dashboard', {\n * siteUrl: 'https://example.com',\n * changefreq: 'weekly',\n * priority: 0.8,\n * xhtmlLinks: true,\n * locales: ['en', 'fr'],\n * defaultLocale: 'en',\n * mode: 'prefix-no-default',\n * });\n * // Returns:\n * // <url>\n * // <loc>https://example.com/dashboard</loc>\n * // <changefreq>weekly</changefreq>\n * // <priority>0.8</priority>\n * // <xhtml:link rel=\"alternate\" hrefLang=\"en\" href=\"https://example.com/dashboard\"/>\n * // <xhtml:link rel=\"alternate\" hrefLang=\"fr\" href=\"https://example.com/fr/dashboard\"/>\n * // <xhtml:link rel=\"alternate\" hrefLang=\"x-default\" href=\"https://example.com/dashboard\"/>\n * // </url>\n * ```\n *\n * @param path - The canonical path to generate the entry for.\n * @param options - Configuration options.\n * @returns A `<url>` XML string.\n */\nexport const generateSitemapUrl = (\n path: string,\n options: SitemapUrlEntry & GenerateSitemapOptions\n): string => {\n const {\n siteUrl,\n changefreq,\n priority,\n lastmod,\n xhtmlLinks = true,\n ...routingOptions\n } = options;\n\n const resolved = resolveRoutingConfig(routingOptions as RoutingOptions);\n const fullUrl = `${siteUrl}${path}`;\n\n const lines: (string | null)[] = [\n ' <url>',\n ` <loc>${fullUrl}</loc>`,\n lastmod ? ` <lastmod>${lastmod}</lastmod>` : null,\n changefreq ? ` <changefreq>${changefreq}</changefreq>` : null,\n priority !== undefined ? ` <priority>${priority}</priority>` : null,\n ];\n\n const hasFileExtension = /\\.[a-z0-9]+$/i.test(path);\n if (shouldIncludeAlternates(resolved.mode, xhtmlLinks) && !hasFileExtension) {\n const alternates = getMultilingualUrls(\n path,\n routingOptions as RoutingOptions\n );\n\n for (const [lang, localePath] of Object.entries(alternates)) {\n lines.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"${lang}\" href=\"${siteUrl}${localePath}\"/>`\n );\n }\n\n lines.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"${fullUrl}\"/>`\n );\n }\n\n lines.push(' </url>');\n\n return lines.filter(Boolean).join('\\n');\n};\n\n/**\n * Generates a full XML sitemap string from an array of URL entries.\n *\n * Automatically adds `xmlns:xhtml` to the `<urlset>` declaration when\n * xhtml:link alternate tags are included.\n *\n * Example:\n *\n * ```ts\n * generateSitemap(\n * [\n * { path: '/', changefreq: 'daily', priority: 1.0 },\n * { path: '/about', changefreq: 'monthly', priority: 0.5 },\n * ],\n * {\n * siteUrl: 'https://example.com',\n * xhtmlLinks: true,\n * locales: ['en', 'fr'],\n * defaultLocale: 'en',\n * mode: 'prefix-no-default',\n * }\n * );\n * ```\n *\n * @param entries - Array of URL entries to include in the sitemap.\n * @param options - Sitemap generation options.\n * @returns A full XML sitemap string.\n */\nexport const generateSitemap = (\n entries: SitemapUrlEntry[],\n options: GenerateSitemapOptions\n): string => {\n const { siteUrl, xhtmlLinks = true, ...routingOptions } = options;\n\n const resolved = resolveRoutingConfig(routingOptions as RoutingOptions);\n const includeAlternates = shouldIncludeAlternates(resolved.mode, xhtmlLinks);\n\n const xmlEntries = entries.map((entry) =>\n generateSitemapUrl(entry.path, {\n ...entry,\n siteUrl,\n xhtmlLinks,\n ...routingOptions,\n })\n );\n\n const xmlns = includeAlternates\n ? '\\n xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"'\n : '';\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset\n xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"${xmlns}\n>\n${xmlEntries.join('\\n')}\n</urlset>`;\n};\n"],"mappings":";;;;;;;;;;;AA2CA,MAAM,2BACJ,MACA,eACY,cAAc,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCrC,MAAa,sBACX,MACA,YACW;CACX,MAAM,EACJ,SACA,YACA,UACA,SACA,aAAa,MACb,GAAG,mBACD;CAEJ,MAAM,WAAW,qBAAqB,cAAgC;CACtE,MAAM,UAAU,GAAG,UAAU;CAE7B,MAAM,QAA2B;EAC/B;EACA,YAAY,QAAQ;EACpB,UAAU,gBAAgB,QAAQ,cAAc;EAChD,aAAa,mBAAmB,WAAW,iBAAiB;EAC5D,aAAa,SAAY,iBAAiB,SAAS,eAAe;CACpE;CAEA,MAAM,mBAAmB,gBAAgB,KAAK,IAAI;CAClD,IAAI,wBAAwB,SAAS,MAAM,UAAU,KAAK,CAAC,kBAAkB;EAC3E,MAAM,aAAa,oBACjB,MACA,cACF;EAEA,KAAK,MAAM,CAAC,MAAM,eAAe,OAAO,QAAQ,UAAU,GACxD,MAAM,KACJ,6CAA6C,KAAK,UAAU,UAAU,WAAW,IACnF;EAGF,MAAM,KACJ,8DAA8D,QAAQ,IACxE;CACF;CAEA,MAAM,KAAK,UAAU;CAErB,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC,KAAK,IAAI;AACxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,mBACX,SACA,YACW;CACX,MAAM,EAAE,SAAS,aAAa,MAAM,GAAG,mBAAmB;CAE1D,MAAM,WAAW,qBAAqB,cAAgC;CACtE,MAAM,oBAAoB,wBAAwB,SAAS,MAAM,UAAU;CAE3E,MAAM,aAAa,QAAQ,KAAK,UAC9B,mBAAmB,MAAM,MAAM;EAC7B,GAAG;EACH;EACA;EACA,GAAG;CACL,CAAC,CACH;CAMA,OAAO;;uDAJO,oBACV,qDACA,GAIuD;;EAE3D,WAAW,KAAK,IAAI,EAAE;;AAExB"}
|
|
1
|
+
{"version":3,"file":"generateSitemap.mjs","names":[],"sources":["../../../src/localization/generateSitemap.ts"],"sourcesContent":["import type { RoutingConfig } from '@intlayer/types/config';\nimport type { LocalesValues } from '@intlayer/types/module_augmentation';\nimport { checkIsURLAbsolute } from '../utils/checkIsURLAbsolute';\nimport { getMultilingualUrls } from './getMultilingualUrls';\nimport { type RoutingOptions, resolveRoutingConfig } from './getPrefix';\n\nexport type SitemapUrlEntry = {\n /** The canonical path, e.g. '/dashboard' */\n path: string;\n changefreq?: string;\n priority?: number;\n /** ISO date string, e.g. '2024-01-15' */\n lastmod?: string;\n};\n\nexport type GenerateSitemapOptions = {\n /** Base site URL without trailing slash, e.g. 'https://example.com' */\n siteUrl: string;\n /**\n * Whether to include xhtml:link alternate tags for multilingual support.\n *\n * When enabled, alternate links are only generated for routing setups where\n * URLs differ per locale:\n * - 'prefix-no-default': included\n * - 'prefix-all': included\n * - 'search-params': included\n * - 'no-prefix': excluded, unless `domains` gives locales distinct hostnames\n *\n * @default true\n */\n xhtmlLinks?: boolean;\n /**\n * Whether to emit one `<url>` entry per locale instead of a single entry for\n * the default locale carrying the alternates.\n *\n * Both forms are valid for search engines, but only a URL listed as a `<loc>`\n * is reported as submitted: an alternate-only locale is discoverable, yet\n * Search Console attributes it no referring sitemap. Enabling this gives\n * every localized URL its own entry, each repeating the complete alternate\n * set, which is the form Google documents for large multilingual sites.\n *\n * Multiplies the entry count by the number of locales, so keep an eye on the\n * 50 000 URL / 50 MB per-file limits — split into a sitemap index beyond it.\n *\n * Only applies where alternates apply: locale URLs must be distinct (see\n * `xhtmlLinks`) and the path must not point at a file. Locales served from\n * their own domain are left out, as a sitemap may only submit URLs of the\n * site it is hosted on.\n *\n * @default false\n */\n entryPerLocale?: boolean;\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n rewrite?: RoutingConfig['rewrite'];\n domains?: RoutingConfig['domains'];\n};\n\n/**\n * Returns whether xhtml:link alternate tags should be generated for the given\n * routing mode and domain map.\n *\n * Alternates are meaningful only when locale URLs are distinct:\n * - 'no-prefix' produces identical URLs for all locales → no alternates, unless\n * `routing.domains` serves some locales from their own hostname\n * - all other modes produce distinct URLs → alternates are generated\n */\nconst shouldIncludeAlternates = (\n mode: RoutingConfig['mode'],\n xhtmlLinks: boolean,\n domains: RoutingConfig['domains']\n): boolean =>\n xhtmlLinks && (mode !== 'no-prefix' || hasConfiguredDomain(domains));\n\n/** Whether at least one locale is mapped to a domain of its own. */\nconst hasConfiguredDomain = (domains: RoutingConfig['domains']): boolean =>\n Object.values(domains ?? {}).some(\n (domain) => typeof domain === 'string' && domain.length > 0\n );\n\n/**\n * Assembles a single `<url>` block from an absolute location and its metadata.\n *\n * @param location - The absolute URL to declare as `<loc>`.\n * @param metadata - The optional `lastmod` / `changefreq` / `priority` values.\n * @param alternateLines - Pre-rendered `<xhtml:link>` lines, empty when none.\n * @returns A `<url>` XML string.\n */\nconst buildUrlEntry = (\n location: string,\n metadata: Omit<SitemapUrlEntry, 'path'>,\n alternateLines: string[]\n): string => {\n const { lastmod, changefreq, priority } = metadata;\n\n return [\n ' <url>',\n ` <loc>${location}</loc>`,\n lastmod ? ` <lastmod>${lastmod}</lastmod>` : null,\n changefreq ? ` <changefreq>${changefreq}</changefreq>` : null,\n priority !== undefined ? ` <priority>${priority}</priority>` : null,\n ...alternateLines,\n ' </url>',\n ]\n .filter(Boolean)\n .join('\\n');\n};\n\n/**\n * Generates the `<url>` sitemap entries for the given path.\n *\n * Emits a single entry for the default locale, or — with `entryPerLocale` — one\n * entry per localized URL, each repeating the same alternate set.\n *\n * Example:\n *\n * ```ts\n * generateSitemapUrl('/dashboard', {\n * siteUrl: 'https://example.com',\n * changefreq: 'weekly',\n * priority: 0.8,\n * xhtmlLinks: true,\n * locales: ['en', 'fr'],\n * defaultLocale: 'en',\n * mode: 'prefix-no-default',\n * });\n * // Returns:\n * // <url>\n * // <loc>https://example.com/dashboard</loc>\n * // <changefreq>weekly</changefreq>\n * // <priority>0.8</priority>\n * // <xhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https://example.com/dashboard\"/>\n * // <xhtml:link rel=\"alternate\" hreflang=\"fr\" href=\"https://example.com/fr/dashboard\"/>\n * // <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"https://example.com/dashboard\"/>\n * // </url>\n * ```\n *\n * @param path - The canonical path to generate the entries for.\n * @param options - Configuration options.\n * @returns One or more `<url>` XML strings, joined by a newline.\n */\nexport const generateSitemapUrl = (\n path: string,\n options: SitemapUrlEntry & GenerateSitemapOptions\n): string => {\n const {\n siteUrl,\n changefreq,\n priority,\n lastmod,\n xhtmlLinks = true,\n entryPerLocale = false,\n ...routingOptions\n } = options;\n\n const resolved = resolveRoutingConfig(routingOptions as RoutingOptions);\n const defaultLocaleUrl = `${siteUrl}${path}`;\n const metadata = { changefreq, priority, lastmod };\n\n // A path pointing at a file (`/llms.txt`) has no localized counterpart.\n const hasFileExtension = /\\.[a-z0-9]+$/i.test(path);\n const includeAlternates =\n shouldIncludeAlternates(resolved.mode, xhtmlLinks, resolved.domains) &&\n !hasFileExtension;\n\n if (!includeAlternates) return buildUrlEntry(defaultLocaleUrl, metadata, []);\n\n const alternates = getMultilingualUrls(\n path,\n routingOptions as RoutingOptions\n );\n\n /**\n * A locale served from its own domain already yields an absolute URL —\n * prefixing it with `siteUrl` again would corrupt it.\n */\n const toAbsoluteUrl = (localeUrl: string): string =>\n checkIsURLAbsolute(localeUrl) ? localeUrl : `${siteUrl}${localeUrl}`;\n\n const alternateLines = [\n ...Object.entries(alternates).map(\n ([locale, localeUrl]) =>\n ` <xhtml:link rel=\"alternate\" hreflang=\"${locale}\" href=\"${toAbsoluteUrl(localeUrl)}\"/>`\n ),\n ` <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"${defaultLocaleUrl}\"/>`,\n ];\n\n if (!entryPerLocale)\n return buildUrlEntry(defaultLocaleUrl, metadata, alternateLines);\n\n const localeUrls = Object.values(alternates)\n .map(toAbsoluteUrl)\n // A sitemap may only submit URLs of the site serving it, so locales living\n // on their own domain stay alternates and belong to their own sitemap.\n .filter((localeUrl) => localeUrl.startsWith(siteUrl))\n .filter((localeUrl, index, urls) => urls.indexOf(localeUrl) === index);\n\n return localeUrls\n .map((localeUrl) => buildUrlEntry(localeUrl, metadata, alternateLines))\n .join('\\n');\n};\n\n/**\n * Generates a full XML sitemap string from an array of URL entries.\n *\n * Automatically adds `xmlns:xhtml` to the `<urlset>` declaration when\n * xhtml:link alternate tags are included.\n *\n * Example:\n *\n * ```ts\n * generateSitemap(\n * [\n * { path: '/', changefreq: 'daily', priority: 1.0 },\n * { path: '/about', changefreq: 'monthly', priority: 0.5 },\n * ],\n * {\n * siteUrl: 'https://example.com',\n * xhtmlLinks: true,\n * locales: ['en', 'fr'],\n * defaultLocale: 'en',\n * mode: 'prefix-no-default',\n * }\n * );\n * ```\n *\n * @param entries - Array of URL entries to include in the sitemap.\n * @param options - Sitemap generation options.\n * @returns A full XML sitemap string.\n */\nexport const generateSitemap = (\n entries: SitemapUrlEntry[],\n options: GenerateSitemapOptions\n): string => {\n const {\n siteUrl,\n xhtmlLinks = true,\n entryPerLocale = false,\n ...routingOptions\n } = options;\n\n const resolved = resolveRoutingConfig(routingOptions as RoutingOptions);\n const includeAlternates = shouldIncludeAlternates(\n resolved.mode,\n xhtmlLinks,\n resolved.domains\n );\n\n const xmlEntries = entries.map((entry) =>\n generateSitemapUrl(entry.path, {\n ...entry,\n siteUrl,\n xhtmlLinks,\n entryPerLocale,\n ...routingOptions,\n })\n );\n\n const xmlns = includeAlternates\n ? '\\n xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"'\n : '';\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset\n xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"${xmlns}\n>\n${xmlEntries.join('\\n')}\n</urlset>`;\n};\n"],"mappings":";;;;;;;;;;;;;;AAoEA,MAAM,2BACJ,MACA,YACA,YAEA,eAAe,SAAS,eAAe,oBAAoB,OAAO;;AAGpE,MAAM,uBAAuB,YAC3B,OAAO,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,MAC1B,WAAW,OAAO,WAAW,YAAY,OAAO,SAAS,CAC5D;;;;;;;;;AAUF,MAAM,iBACJ,UACA,UACA,mBACW;CACX,MAAM,EAAE,SAAS,YAAY,aAAa;CAE1C,OAAO;EACL;EACA,YAAY,SAAS;EACrB,UAAU,gBAAgB,QAAQ,cAAc;EAChD,aAAa,mBAAmB,WAAW,iBAAiB;EAC5D,aAAa,SAAY,iBAAiB,SAAS,eAAe;EAClE,GAAG;EACH;CACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,IAAI;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,sBACX,MACA,YACW;CACX,MAAM,EACJ,SACA,YACA,UACA,SACA,aAAa,MACb,iBAAiB,OACjB,GAAG,mBACD;CAEJ,MAAM,WAAW,qBAAqB,cAAgC;CACtE,MAAM,mBAAmB,GAAG,UAAU;CACtC,MAAM,WAAW;EAAE;EAAY;EAAU;CAAQ;CAGjD,MAAM,mBAAmB,gBAAgB,KAAK,IAAI;CAKlD,IAAI,EAHF,wBAAwB,SAAS,MAAM,YAAY,SAAS,OAAO,KACnE,CAAC,mBAEqB,OAAO,cAAc,kBAAkB,UAAU,CAAC,CAAC;CAE3E,MAAM,aAAa,oBACjB,MACA,cACF;;;;;CAMA,MAAM,iBAAiB,cACrB,mBAAmB,SAAS,IAAI,YAAY,GAAG,UAAU;CAE3D,MAAM,iBAAiB,CACrB,GAAG,OAAO,QAAQ,UAAU,CAAC,CAAC,KAC3B,CAAC,QAAQ,eACR,6CAA6C,OAAO,UAAU,cAAc,SAAS,EAAE,IAC3F,GACA,8DAA8D,iBAAiB,IACjF;CAEA,IAAI,CAAC,gBACH,OAAO,cAAc,kBAAkB,UAAU,cAAc;CASjE,OAPmB,OAAO,OAAO,UAAU,CAAC,CACzC,IAAI,aAAa,CAAC,CAGlB,QAAQ,cAAc,UAAU,WAAW,OAAO,CAAC,CAAC,CACpD,QAAQ,WAAW,OAAO,SAAS,KAAK,QAAQ,SAAS,MAAM,KAElD,CAAC,CACd,KAAK,cAAc,cAAc,WAAW,UAAU,cAAc,CAAC,CAAC,CACtE,KAAK,IAAI;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,mBACX,SACA,YACW;CACX,MAAM,EACJ,SACA,aAAa,MACb,iBAAiB,OACjB,GAAG,mBACD;CAEJ,MAAM,WAAW,qBAAqB,cAAgC;CACtE,MAAM,oBAAoB,wBACxB,SAAS,MACT,YACA,SAAS,OACX;CAEA,MAAM,aAAa,QAAQ,KAAK,UAC9B,mBAAmB,MAAM,MAAM;EAC7B,GAAG;EACH;EACA;EACA;EACA,GAAG;CACL,CAAC,CACH;CAMA,OAAO;;uDAJO,oBACV,qDACA,GAIuD;;EAE3D,WAAW,KAAK,IAAI,EAAE;;AAExB"}
|
|
@@ -28,6 +28,9 @@ import { internationalization } from "@intlayer/config/built";
|
|
|
28
28
|
* getLocalizedPath('/contact', 'fr'); // '/fr/contact' (no rewrite rule matches)
|
|
29
29
|
* getLocalizedPath('https://intlayer.org/about', 'fr'); // '/fr/a-propos' (origin dropped)
|
|
30
30
|
*
|
|
31
|
+
* // routing: { domains: { zh: 'intlayer.cn' } } — zh is alone on that hostname
|
|
32
|
+
* getLocalizedPath('/about', 'zh'); // '/about' (the domain identifies the locale)
|
|
33
|
+
*
|
|
31
34
|
* // Manual rules, bypassing the configuration
|
|
32
35
|
* getLocalizedPath('/contact', 'fr', {
|
|
33
36
|
* rewrite: { '/contact': { fr: '/contactez-nous' } },
|
|
@@ -1 +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":"
|
|
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 * // routing: { domains: { zh: 'intlayer.cn' } } — zh is alone on that hostname\n * getLocalizedPath('/about', 'zh'); // '/about' (the domain identifies the locale)\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,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"}
|
|
@@ -78,6 +78,22 @@ describe("LocalizedPathname", () => {
|
|
|
78
78
|
expectTypeOf().toEqualTypeOf();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
/**
|
|
82
|
+
* `getLocalizedPath` never emits an origin, so `routing.domains` reaches it only
|
|
83
|
+
* through the prefix it suppresses for a locale alone on its domain.
|
|
84
|
+
*/
|
|
85
|
+
describe("LocalizedPathname with routing.domains", () => {
|
|
86
|
+
it("should drop the prefix of a locale alone on its domain", () => {
|
|
87
|
+
expectTypeOf().toEqualTypeOf();
|
|
88
|
+
expectTypeOf().toEqualTypeOf();
|
|
89
|
+
});
|
|
90
|
+
it("should keep the prefix of a locale sharing its domain", () => {
|
|
91
|
+
expectTypeOf().toEqualTypeOf();
|
|
92
|
+
});
|
|
93
|
+
it("should never emit the locale domain as an origin", () => {
|
|
94
|
+
expectTypeOf().toEqualTypeOf();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
81
97
|
|
|
82
98
|
//#endregion
|
|
83
99
|
//# sourceMappingURL=getLocalizedPath.test-d.mjs.map
|
|
@@ -1 +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
|
+
{"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\n/**\n * `getLocalizedPath` never emits an origin, so `routing.domains` reaches it only\n * through the prefix it suppresses for a locale alone on its domain.\n */\ndescribe('LocalizedPathname with routing.domains', () => {\n type Locales = 'en' | 'fr' | 'zh';\n type Domains = {\n en: { origin: 'https://intlayer.org'; exclusive: false };\n fr: { origin: 'https://intlayer.org'; exclusive: false };\n zh: { origin: 'https://intlayer.cn'; exclusive: true };\n };\n type Path<P extends string, L extends Locales> = LocalizedPathname<\n P,\n L,\n 'prefix-no-default',\n 'en',\n Locales,\n Domains\n >;\n\n it('should drop the prefix of a locale alone on its domain', () => {\n expectTypeOf<Path<'/about', 'zh'>>().toEqualTypeOf<'/about'>();\n expectTypeOf<Path<'/', 'zh'>>().toEqualTypeOf<'/'>();\n });\n\n it('should keep the prefix of a locale sharing its domain', () => {\n expectTypeOf<Path<'/about', 'fr'>>().toEqualTypeOf<'/fr/about'>();\n });\n\n it('should never emit the locale domain as an origin', () => {\n expectTypeOf<\n Path<'https://intlayer.org/about', 'zh'>\n >().toEqualTypeOf<'/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;;;;;AAMD,SAAS,gDAAgD;CAgBvD,GAAG,gEAAgE;EACjE,aAAmC,CAAC,CAAC,cAAwB;EAC7D,aAA8B,CAAC,CAAC,cAAmB;CACrD,CAAC;CAED,GAAG,+DAA+D;EAChE,aAAmC,CAAC,CAAC,cAA2B;CAClE,CAAC;CAED,GAAG,0DAA0D;EAC3D,aAEE,CAAC,CAAC,cAAwB;CAC9B,CAAC;AACH,CAAC"}
|
|
@@ -35,6 +35,10 @@ import { internationalization } from "@intlayer/config/built";
|
|
|
35
35
|
* // no-prefix mode
|
|
36
36
|
* getLocalizedUrl('/about', 'fr', { locales: ['en', 'fr'], defaultLocale: 'en', mode: 'no-prefix' });
|
|
37
37
|
* // Returns '/about' for any locale
|
|
38
|
+
*
|
|
39
|
+
* // domain routing
|
|
40
|
+
* getLocalizedUrl('/about', 'zh', { domains: { zh: 'intlayer.cn' }, currentDomain: 'intlayer.org' });
|
|
41
|
+
* // Returns 'https://intlayer.cn/about' — the domain identifies the locale, so no prefix
|
|
38
42
|
* ```
|
|
39
43
|
*
|
|
40
44
|
* @param url - The original URL string to be processed.
|
|
@@ -52,6 +56,10 @@ import { internationalization } from "@intlayer/config/built";
|
|
|
52
56
|
* The return type is narrowed to a template-literal type when both `url` and
|
|
53
57
|
* `currentLocale` are string literals and the routing mode / defaultLocale are
|
|
54
58
|
* not overridden via `options`.
|
|
59
|
+
*
|
|
60
|
+
* For a locale mapped to a domain by `routing.domains` it narrows to the union
|
|
61
|
+
* of the two URLs this function can return — the absolute one on that domain,
|
|
62
|
+
* and the relative one it returns when the current page already lives there.
|
|
55
63
|
*/
|
|
56
64
|
const getLocalizedUrl = (url, currentLocale = internationalization?.defaultLocale, options = {}) => {
|
|
57
65
|
const { domains, currentDomain } = resolveRoutingConfig(options);
|
|
@@ -1 +1 @@
|
|
|
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":"
|
|
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 * // domain routing\n * getLocalizedUrl('/about', 'zh', { domains: { zh: 'intlayer.cn' }, currentDomain: 'intlayer.org' });\n * // Returns 'https://intlayer.cn/about' — the domain identifies the locale, so no prefix\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 *\n * For a locale mapped to a domain by `routing.domains` it narrows to the union\n * of the two URLs this function can return — the absolute one on that domain,\n * and the relative one it returns when the current page already lives there.\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,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"}
|
|
@@ -37,6 +37,42 @@ describe("LocalizedUrl", () => {
|
|
|
37
37
|
expectTypeOf().toEqualTypeOf();
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
|
+
/**
|
|
41
|
+
* Domain routing, exercised against an explicit domain map — the same shape the
|
|
42
|
+
* generated module augmentation feeds it as `__RoutingRegistry['domains']`.
|
|
43
|
+
*
|
|
44
|
+
* `en` and `fr` share `intlayer.org` (prefix routing still applies), while `zh`
|
|
45
|
+
* is alone on `intlayer.cn` (the hostname identifies the locale, so no prefix).
|
|
46
|
+
*/
|
|
47
|
+
describe("LocalizedUrl with routing.domains", () => {
|
|
48
|
+
it("should drop the locale prefix of a locale alone on its domain", () => {
|
|
49
|
+
expectTypeOf().toEqualTypeOf();
|
|
50
|
+
});
|
|
51
|
+
it("should keep the locale prefix of a locale sharing its domain", () => {
|
|
52
|
+
expectTypeOf().toEqualTypeOf();
|
|
53
|
+
expectTypeOf().toEqualTypeOf();
|
|
54
|
+
});
|
|
55
|
+
it("should strip an existing locale segment before switching domain", () => {
|
|
56
|
+
expectTypeOf().toEqualTypeOf();
|
|
57
|
+
expectTypeOf().toEqualTypeOf();
|
|
58
|
+
});
|
|
59
|
+
it("should keep the root slash of a domain-exclusive locale", () => {
|
|
60
|
+
expectTypeOf().toEqualTypeOf();
|
|
61
|
+
});
|
|
62
|
+
it("should replace the origin of an absolute URL with the locale domain", () => {
|
|
63
|
+
expectTypeOf().toEqualTypeOf();
|
|
64
|
+
});
|
|
65
|
+
it("should leave a locale mapped to no domain untouched", () => {
|
|
66
|
+
expectTypeOf().toEqualTypeOf();
|
|
67
|
+
});
|
|
68
|
+
it("should keep the domain origin in no-prefix mode", () => {
|
|
69
|
+
expectTypeOf().toEqualTypeOf();
|
|
70
|
+
});
|
|
71
|
+
it("should drop the prefix in prefix-all mode too", () => {
|
|
72
|
+
expectTypeOf().toEqualTypeOf();
|
|
73
|
+
expectTypeOf().toEqualTypeOf();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
40
76
|
|
|
41
77
|
//#endregion
|
|
42
78
|
//# sourceMappingURL=getLocalizedUrl.test-d.mjs.map
|
|
@@ -1 +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"}
|
|
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\n/**\n * Domain routing, exercised against an explicit domain map — the same shape the\n * generated module augmentation feeds it as `__RoutingRegistry['domains']`.\n *\n * `en` and `fr` share `intlayer.org` (prefix routing still applies), while `zh`\n * is alone on `intlayer.cn` (the hostname identifies the locale, so no prefix).\n */\ndescribe('LocalizedUrl with routing.domains', () => {\n type Locales = 'en' | 'fr' | 'zh';\n type Domains = {\n en: { origin: 'https://intlayer.org'; exclusive: false };\n fr: { origin: 'https://intlayer.org'; exclusive: false };\n zh: { origin: 'https://intlayer.cn'; exclusive: true };\n };\n type Url<Path extends string, L extends Locales> = LocalizedUrl<\n Path,\n L,\n 'prefix-no-default',\n 'en',\n Locales,\n Domains\n >;\n\n it('should drop the locale prefix of a locale alone on its domain', () => {\n expectTypeOf<Url<'/about', 'zh'>>().toEqualTypeOf<\n 'https://intlayer.cn/about' | '/about'\n >();\n });\n\n it('should keep the locale prefix of a locale sharing its domain', () => {\n expectTypeOf<Url<'/about', 'fr'>>().toEqualTypeOf<\n 'https://intlayer.org/fr/about' | '/fr/about'\n >();\n expectTypeOf<Url<'/about', 'en'>>().toEqualTypeOf<\n 'https://intlayer.org/about' | '/about'\n >();\n });\n\n it('should strip an existing locale segment before switching domain', () => {\n expectTypeOf<Url<'/zh/about', 'zh'>>().toEqualTypeOf<\n 'https://intlayer.cn/about' | '/about'\n >();\n expectTypeOf<Url<'/fr/about', 'zh'>>().toEqualTypeOf<\n 'https://intlayer.cn/about' | '/about'\n >();\n });\n\n it('should keep the root slash of a domain-exclusive locale', () => {\n expectTypeOf<Url<'/', 'zh'>>().toEqualTypeOf<\n 'https://intlayer.cn/' | '/'\n >();\n });\n\n it('should replace the origin of an absolute URL with the locale domain', () => {\n expectTypeOf<Url<'https://intlayer.org/about', 'zh'>>().toEqualTypeOf<\n 'https://intlayer.cn/about' | 'https://intlayer.org/about'\n >();\n });\n\n it('should leave a locale mapped to no domain untouched', () => {\n type PartialDomains = {\n zh: { origin: 'https://intlayer.cn'; exclusive: true };\n };\n\n expectTypeOf<\n LocalizedUrl<\n '/about',\n 'fr',\n 'prefix-no-default',\n 'en',\n Locales,\n PartialDomains\n >\n >().toEqualTypeOf<'/fr/about'>();\n });\n\n it('should keep the domain origin in no-prefix mode', () => {\n expectTypeOf<\n LocalizedUrl<'/about', 'zh', 'no-prefix', 'en', Locales, Domains>\n >().toEqualTypeOf<'https://intlayer.cn/about' | '/about'>();\n });\n\n it('should drop the prefix in prefix-all mode too', () => {\n expectTypeOf<\n LocalizedUrl<'/about', 'zh', 'prefix-all', 'en', Locales, Domains>\n >().toEqualTypeOf<'https://intlayer.cn/about' | '/about'>();\n expectTypeOf<\n LocalizedUrl<'/about', 'fr', 'prefix-all', 'en', Locales, Domains>\n >().toEqualTypeOf<'https://intlayer.org/fr/about' | '/fr/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;;;;;;;;AASD,SAAS,2CAA2C;CAgBlD,GAAG,uEAAuE;EACxE,aAAkC,CAAC,CAAC,cAElC;CACJ,CAAC;CAED,GAAG,sEAAsE;EACvE,aAAkC,CAAC,CAAC,cAElC;EACF,aAAkC,CAAC,CAAC,cAElC;CACJ,CAAC;CAED,GAAG,yEAAyE;EAC1E,aAAqC,CAAC,CAAC,cAErC;EACF,aAAqC,CAAC,CAAC,cAErC;CACJ,CAAC;CAED,GAAG,iEAAiE;EAClE,aAA6B,CAAC,CAAC,cAE7B;CACJ,CAAC;CAED,GAAG,6EAA6E;EAC9E,aAAsD,CAAC,CAAC,cAEtD;CACJ,CAAC;CAED,GAAG,6DAA6D;EAK9D,aASE,CAAC,CAAC,cAA2B;CACjC,CAAC;CAED,GAAG,yDAAyD;EAC1D,aAEE,CAAC,CAAC,cAAsD;CAC5D,CAAC;CAED,GAAG,uDAAuD;EACxD,aAEE,CAAC,CAAC,cAAsD;EAC1D,aAEE,CAAC,CAAC,cAA6D;CACnE,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getPrefix.mjs","names":[],"sources":["../../../src/localization/getPrefix.ts"],"sourcesContent":["import { internationalization, routing } from '@intlayer/config/built';\nimport {\n DEFAULT_LOCALE,\n LOCALES,\n ROUTING_MODE,\n} from '@intlayer/config/defaultValues';\nimport type { RewriteRules, RoutingConfig } from '@intlayer/types/config';\nimport type {\n DeclaredLocales,\n LocalesValues,\n ResolvedDefaultLocale,\n ResolvedRoutingMode,\n} from '@intlayer/types/module_augmentation';\nimport { isLocaleExclusiveOnDomain } from './domainUtils';\n\n/**\n * Shared routing options used across all URL localization functions.\n */\nexport type RoutingOptions = {\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n rewrite?: RoutingConfig['rewrite'] | RewriteRules;\n domains?: RoutingConfig['domains'];\n /**\n * The hostname of the page currently being rendered (e.g. `'intlayer.org'`).\n * When provided, `getLocalizedUrl` returns a relative URL for locales whose\n * configured domain matches `currentDomain`, and an absolute URL only when\n * the target locale lives on a different domain.\n *\n * When omitted the function tries to infer it from:\n * 1. The domain of an absolute input URL.\n * 2. `window.location.hostname` in browser environments.\n * Falls back to always generating absolute URLs when neither is available.\n */\n currentDomain?: string;\n};\n\n/**\n * Resolves routing configuration by merging provided options with configuration defaults.\n * Single source of truth for default routing config resolution across all localization functions.\n */\nexport const resolveRoutingConfig = (\n options: RoutingOptions = {}\n): Omit<RoutingOptions, 'defaultLocale' | 'mode' | 'locales'> & {\n defaultLocale: DeclaredLocales;\n mode: RoutingConfig['mode'];\n locales: DeclaredLocales[];\n} => ({\n defaultLocale: internationalization?.defaultLocale ?? DEFAULT_LOCALE,\n mode: routing?.mode ?? ROUTING_MODE,\n locales: internationalization?.locales ?? LOCALES,\n rewrite: routing?.rewrite,\n domains: routing?.domains,\n ...options,\n});\n\n/**\n * Narrows an arbitrary string to a declared locale by membership test.\n *\n * `locales` is typed as `DeclaredLocales[]`, so a plain `includes` rejects the\n * `string` values that actually need testing (a URL segment, a query param, a\n * stored value). This keeps the check honest and hands back a narrowed type\n * instead of forcing a cast at every call site.\n *\n * @param value - The candidate locale, from a URL segment / storage / header.\n * @param locales - The locales declared by the configuration.\n * @returns Whether `value` is one of the declared locales.\n */\nexport const isDeclaredLocale = (\n value: LocalesValues | undefined | null,\n locales?: readonly LocalesValues[]\n): value is DeclaredLocales =>\n !!value && (locales ?? internationalization.locales).includes(value);\n\nexport type GetPrefixOptions = {\n defaultLocale?: ResolvedDefaultLocale;\n mode?: RoutingConfig['mode'];\n};\n\nexport type GetPrefixResult = {\n /**\n * The locale path segment appended to `/`, with a trailing slash (e.g. `'fr/'`).\n * Empty string when no prefix is needed.\n */\n prefix: string;\n /**\n * The bare locale identifier (e.g. `'fr'`), or `undefined` when no prefix is applied.\n */\n localePrefix: DeclaredLocales | undefined;\n};\n\n/**\n * Narrowed return type for {@link getPrefix} that carries the locale literal through.\n *\n * Distributes over union locales — calling `getPrefix('fr')` in `prefix-no-default`\n * mode with `defaultLocale = 'en'` resolves to `{ prefix: 'fr/'; localePrefix: 'fr' }`.\n *\n * Note:
|
|
1
|
+
{"version":3,"file":"getPrefix.mjs","names":[],"sources":["../../../src/localization/getPrefix.ts"],"sourcesContent":["import { internationalization, routing } from '@intlayer/config/built';\nimport {\n DEFAULT_LOCALE,\n LOCALES,\n ROUTING_MODE,\n} from '@intlayer/config/defaultValues';\nimport type { RewriteRules, RoutingConfig } from '@intlayer/types/config';\nimport type {\n DeclaredLocales,\n DomainRuleMap,\n IsDomainExclusive,\n LocalesValues,\n ResolvedDefaultLocale,\n ResolvedDomains,\n ResolvedRoutingMode,\n} from '@intlayer/types/module_augmentation';\nimport { isLocaleExclusiveOnDomain } from './domainUtils';\n\n/**\n * Shared routing options used across all URL localization functions.\n */\nexport type RoutingOptions = {\n locales?: LocalesValues[];\n defaultLocale?: LocalesValues;\n mode?: RoutingConfig['mode'];\n rewrite?: RoutingConfig['rewrite'] | RewriteRules;\n domains?: RoutingConfig['domains'];\n /**\n * The hostname of the page currently being rendered (e.g. `'intlayer.org'`).\n * When provided, `getLocalizedUrl` returns a relative URL for locales whose\n * configured domain matches `currentDomain`, and an absolute URL only when\n * the target locale lives on a different domain.\n *\n * When omitted the function tries to infer it from:\n * 1. The domain of an absolute input URL.\n * 2. `window.location.hostname` in browser environments.\n * Falls back to always generating absolute URLs when neither is available.\n */\n currentDomain?: string;\n};\n\n/**\n * Resolves routing configuration by merging provided options with configuration defaults.\n * Single source of truth for default routing config resolution across all localization functions.\n */\nexport const resolveRoutingConfig = (\n options: RoutingOptions = {}\n): Omit<RoutingOptions, 'defaultLocale' | 'mode' | 'locales'> & {\n defaultLocale: DeclaredLocales;\n mode: RoutingConfig['mode'];\n locales: DeclaredLocales[];\n} => ({\n defaultLocale: internationalization?.defaultLocale ?? DEFAULT_LOCALE,\n mode: routing?.mode ?? ROUTING_MODE,\n locales: internationalization?.locales ?? LOCALES,\n rewrite: routing?.rewrite,\n domains: routing?.domains,\n ...options,\n});\n\n/**\n * Narrows an arbitrary string to a declared locale by membership test.\n *\n * `locales` is typed as `DeclaredLocales[]`, so a plain `includes` rejects the\n * `string` values that actually need testing (a URL segment, a query param, a\n * stored value). This keeps the check honest and hands back a narrowed type\n * instead of forcing a cast at every call site.\n *\n * @param value - The candidate locale, from a URL segment / storage / header.\n * @param locales - The locales declared by the configuration.\n * @returns Whether `value` is one of the declared locales.\n */\nexport const isDeclaredLocale = (\n value: LocalesValues | undefined | null,\n locales?: readonly LocalesValues[]\n): value is DeclaredLocales =>\n !!value && (locales ?? internationalization.locales).includes(value);\n\nexport type GetPrefixOptions = {\n defaultLocale?: ResolvedDefaultLocale;\n mode?: RoutingConfig['mode'];\n};\n\nexport type GetPrefixResult = {\n /**\n * The locale path segment appended to `/`, with a trailing slash (e.g. `'fr/'`).\n * Empty string when no prefix is needed.\n */\n prefix: string;\n /**\n * The bare locale identifier (e.g. `'fr'`), or `undefined` when no prefix is applied.\n */\n localePrefix: DeclaredLocales | undefined;\n};\n\n/**\n * Narrowed return type for {@link getPrefix} that carries the locale literal through.\n *\n * Distributes over union locales — calling `getPrefix('fr')` in `prefix-no-default`\n * mode with `defaultLocale = 'en'` resolves to `{ prefix: 'fr/'; localePrefix: 'fr' }`.\n * A locale served from its own exclusive domain resolves to an empty prefix, as\n * the hostname already identifies it.\n *\n * Note: the \"locale not in locales\" edge case returns an empty result at runtime\n * regardless of what this type reports.\n */\nexport type GetPrefixResultNarrowed<\n L extends LocalesValues | undefined,\n Mode extends string = ResolvedRoutingMode,\n Default extends LocalesValues = ResolvedDefaultLocale,\n Domains extends DomainRuleMap = ResolvedDomains,\n> = L extends string\n ? [string] extends [L] // L is wide (string / LocalesValues) → distribute over declared locales\n ? GetPrefixResultNarrowed<DeclaredLocales, Mode, Default, Domains>\n : IsDomainExclusive<L, Domains> extends true\n ? { prefix: ''; localePrefix: undefined } // the domain identifies the locale\n : [string] extends [Mode]\n ? GetPrefixResult // mode is wide → fall back to generic result\n : Mode extends 'prefix-all'\n ? { prefix: `${L}/`; localePrefix: L }\n : Mode extends 'prefix-no-default'\n ? L extends Default\n ? { prefix: ''; localePrefix: undefined }\n : { prefix: `${L}/`; localePrefix: L }\n : { prefix: ''; localePrefix: undefined } // no-prefix / search-params\n : { prefix: ''; localePrefix: undefined }; // locale is undefined\n\n/**\n * Determines the URL prefix for a given locale based on the routing mode configuration.\n *\n * Example:\n *\n * ```ts\n * // prefix-no-default mode with default locale\n * getPrefix('en', { defaultLocale: 'en', mode: 'prefix-no-default' })\n * // Returns { prefix: '', localePrefix: undefined }\n *\n * // prefix-no-default mode with non-default locale\n * getPrefix('fr', { defaultLocale: 'en', mode: 'prefix-no-default' })\n * // Returns { prefix: '/fr', localePrefix: 'fr' }\n *\n * // prefix-all mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'prefix-all' })\n * // Returns { prefix: '/en', localePrefix: locale }\n *\n * // search-params mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'search-params' })\n * // Returns { prefix: '', localePrefix: undefined }\n *\n * // no-prefix mode\n * getPrefix('en', { defaultLocale: 'en', mode: 'no-prefix' })\n * // Returns { prefix: '', localePrefix: undefined }\n * ```\n *\n * @param locale - The locale to check for prefix. If not provided, uses configured default locale.\n * @param options - Configuration options\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 * @returns An object containing pathPrefix, prefix, and localePrefix for the given locale.\n */\nexport const getPrefix = <const L extends LocalesValues | undefined>(\n locale: L,\n options: RoutingOptions = {}\n): GetPrefixResultNarrowed<L> => {\n const { defaultLocale, mode, locales, domains } =\n resolveRoutingConfig(options);\n\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all' &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-no-default') ||\n !locale ||\n !isDeclaredLocale(locale, locales)\n ) {\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n }\n\n // If this locale is the only one assigned to its domain, no URL prefix is needed\n // (the domain itself identifies the locale). Shared domains use normal prefix logic.\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n isLocaleExclusiveOnDomain(locale as LocalesValues, domains)\n ) {\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n }\n\n // Handle prefix-based modes (prefix-all or prefix-no-default)\n const shouldPrefix =\n mode === 'prefix-all' ||\n (mode === 'prefix-no-default' && defaultLocale !== locale);\n\n if (shouldPrefix) {\n return {\n prefix: `${locale}/`,\n localePrefix: locale as DeclaredLocales,\n } as GetPrefixResultNarrowed<L>;\n }\n\n return {\n prefix: '',\n localePrefix: undefined,\n } as GetPrefixResultNarrowed<L>;\n};\n"],"mappings":";;;;;;;;;AA6CA,MAAa,wBACX,UAA0B,CAAC,OAKvB;CACJ,eAAe,sBAAsB,iBAAiB;CACtD,MAAM,SAAS,QAAQ;CACvB,SAAS,sBAAsB,WAAW;CAC1C,SAAS,SAAS;CAClB,SAAS,SAAS;CAClB,GAAG;AACL;;;;;;;;;;;;;AAcA,MAAa,oBACX,OACA,YAEA,CAAC,CAAC,UAAU,WAAW,qBAAqB,QAAO,CAAE,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFrE,MAAa,aACX,QACA,UAA0B,CAAC,MACI;CAC/B,MAAM,EAAE,eAAe,MAAM,SAAS,YACpC,qBAAqB,OAAO;CAE9B,IACG,QAAQ,IAAI,yBACX,QAAQ,IAAI,0BAA0B,gBACtC,QAAQ,IAAI,0BAA0B,uBACxC,CAAC,UACD,CAAC,iBAAiB,QAAQ,OAAO,GAEjC,OAAO;EACL,QAAQ;EACR,cAAc;CAChB;CAKF,IACE,QAAQ,IAAI,6BAA6B,WACzC,0BAA0B,QAAyB,OAAO,GAE1D,OAAO;EACL,QAAQ;EACR,cAAc;CAChB;CAQF,IAHE,SAAS,gBACR,SAAS,uBAAuB,kBAAkB,QAGnD,OAAO;EACL,QAAQ,GAAG,OAAO;EAClB,cAAc;CAChB;CAGF,OAAO;EACL,QAAQ;EACR,cAAc;CAChB;AACF"}
|
|
@@ -172,13 +172,34 @@ const HEADING_R = /^ *(#{1,6}) *([^\n]+?)(?: +#*)?(?:\n *)*(?:\n|$)/;
|
|
|
172
172
|
const HEADING_ATX_COMPLIANT_R = /^ *(#{1,6}) +([^\n]+?)(?: +#*)?(?:\n *)*(?:\n|$)/;
|
|
173
173
|
const HEADING_SETEXT_R = /^([^\n]+)\n *(=|-)\2{2,} *\n/;
|
|
174
174
|
/** HTML patterns */
|
|
175
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Run of characters carrying no meaning inside an opening tag: neither a quote
|
|
177
|
+
* opening a value nor an angle bracket delimiting a tag.
|
|
178
|
+
*/
|
|
179
|
+
const TAG_PLAIN_RUN = `[^>"'<]*`;
|
|
180
|
+
/**
|
|
181
|
+
* Attribute list of an opening tag, written as a plain run followed by
|
|
182
|
+
* quote-or-bracket-led segments so that each character has a single way to be
|
|
183
|
+
* read. A lone quote is allowed last so a tag holding an unbalanced one still
|
|
184
|
+
* parses, as it did before quoted values were recognised.
|
|
185
|
+
*/
|
|
186
|
+
const TAG_ATTRIBUTES = `${TAG_PLAIN_RUN}(?:(?:"[^"\\n]*"|'[^'\\n]*'|<.*?>|["'])${TAG_PLAIN_RUN})*`;
|
|
187
|
+
/** Rejects the opening tag of a self-closing element, `<div class="a"/>`. */
|
|
188
|
+
const NOT_SELF_CLOSING = `(?![^>]*/>)`;
|
|
189
|
+
/**
|
|
190
|
+
* Opening tag of a nested element bearing the same name as the outer one. This
|
|
191
|
+
* one stays a plain scan up to the first `>`: it is tried at every character of
|
|
192
|
+
* the element's content, and a richer pattern here costs an order of magnitude
|
|
193
|
+
* on documents whose tag never closes.
|
|
194
|
+
*/
|
|
195
|
+
const NESTED_SAME_TAG_OPENING = `<\\1[^>]*?>`;
|
|
196
|
+
const HTML_BLOCK_ELEMENT_R = new RegExp(`^ *(?!<[a-zA-Z][^ >/]* ?/>)<([a-zA-Z][^ >/]*) ?(${NOT_SELF_CLOSING}${TAG_ATTRIBUTES})>\\n?(\\s*(?:${NESTED_SAME_TAG_OPENING}[\\s\\S]*?</\\1>|(?!<\\1\\b)[\\s\\S])*?)</\\1>(?!</\\1>)\\n*`, "i");
|
|
176
197
|
const HTML_CHAR_CODE_R = /&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-fA-F]{1,6});/gi;
|
|
177
198
|
const HTML_COMMENT_R = /^<!--[\s\S]*?(?:-->)/;
|
|
178
199
|
const HTML_CUSTOM_ATTR_R = /^(data|aria|x)-[a-z_][a-z\d_.-]*$/;
|
|
179
|
-
const HTML_SELF_CLOSING_ELEMENT_R =
|
|
200
|
+
const HTML_SELF_CLOSING_ELEMENT_R = new RegExp(`^ *<([a-zA-Z][a-zA-Z0-9:]*)(?:\\s+(${TAG_ATTRIBUTES}))?/?>(?!</\\1>)(\\s*\\n)?`, "i");
|
|
180
201
|
/** Custom component pattern */
|
|
181
|
-
const CUSTOM_COMPONENT_R =
|
|
202
|
+
const CUSTOM_COMPONENT_R = new RegExp(`^ *<([A-Z][a-zA-Z0-9]*)(?:\\s+(${TAG_ATTRIBUTES}))?>\\n?(\\s*(?:${NESTED_SAME_TAG_OPENING}[\\s\\S]*?</\\1>|(?!<\\1\\b)[\\s\\S])*?)</\\1>(?!</\\1>)\\n*`);
|
|
182
203
|
/** Interpolation */
|
|
183
204
|
const INTERPOLATION_R = /^\{.*\}$/;
|
|
184
205
|
/** Link patterns */
|