astro-intlayer 9.5.5 → 9.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -201,7 +201,6 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
201
201
  <li><a href="https://intlayer.org/doc/environment/astro/vue" rel=''>Vue</a></li>
202
202
  <li><a href="https://intlayer.org/doc/environment/astro/svelte" rel=''>Svelte</a></li>
203
203
  <li><a href="https://intlayer.org/doc/environment/astro/solid" rel=''>Solid</a></li>
204
- <li><a href="https://intlayer.org/doc/environment/astro/vanilla" rel=''>Vanilla JS</a></li>
205
204
  <li><a href="https://intlayer.org/doc/environment/astro/lit" rel=''>Lit</a></li>
206
205
  </ul>
207
206
  </li>
@@ -0,0 +1,28 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ exports.__toESM = __toESM;
@@ -0,0 +1,64 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _intlayer_core_localization = require("@intlayer/core/localization");
3
+ let _intlayer_config_built = require("@intlayer/config/built");
4
+ let _intlayer_core_utils = require("@intlayer/core/utils");
5
+
6
+ //#region src/client/localeRedirect.ts
7
+ /**
8
+ * Locale explicitly carried by a URL — path prefix, or `?locale=` in
9
+ * `search-params` mode. `undefined` when the URL is unlocalized, which in
10
+ * `prefix-no-default` mode means it belongs to the default locale.
11
+ */
12
+ const getExplicitUrlLocale = (url) => {
13
+ const candidate = _intlayer_config_built.routing.mode === "search-params" ? url.searchParams.get("locale") : url.pathname.split("/")[1];
14
+ return candidate && _intlayer_config_built.internationalization.locales.includes(candidate) ? candidate : void 0;
15
+ };
16
+ /**
17
+ * Computes where a prerendered page should send the visitor so it matches the
18
+ * locale they stored (cookie, localStorage or sessionStorage, per
19
+ * `routing.storage`).
20
+ *
21
+ * Mirrors the redirect the Intlayer proxy performs on server-rendered requests,
22
+ * for pages served as static files where no server can read the cookie:
23
+ * - the URL wins over storage, so a localized URL is never redirected;
24
+ * - `routing.enableProxy: false` and `no-prefix` routing disable it;
25
+ * - in auto mode a dev server keeps routing URL-driven, like the proxy.
26
+ *
27
+ * @param url - The URL of the current page.
28
+ * @param isDevServer - Whether `astro dev` is serving the page.
29
+ * @returns The localized URL to navigate to, or `undefined` to stay put.
30
+ */
31
+ const getStoredLocaleRedirect = (url, isDevServer) => {
32
+ const proxyMode = (0, _intlayer_core_localization.resolveProxyMode)(_intlayer_config_built.routing.enableProxy);
33
+ if (proxyMode === "disabled") return;
34
+ if (!(0, _intlayer_core_localization.isProxyStorageLocaleEnabled)(proxyMode, isDevServer)) return;
35
+ if (_intlayer_config_built.routing.mode === "no-prefix") return;
36
+ if (getExplicitUrlLocale(url)) return;
37
+ const storedLocale = (0, _intlayer_core_utils.getLocaleFromStorageClient)();
38
+ if (!storedLocale || storedLocale === _intlayer_config_built.internationalization.defaultLocale) return;
39
+ const currentPath = `${url.pathname}${url.search}${url.hash}`;
40
+ const localizedPath = (0, _intlayer_core_localization.getLocalizedUrl)(currentPath, storedLocale);
41
+ return localizedPath === currentPath ? void 0 : localizedPath;
42
+ };
43
+ /**
44
+ * Whether `astro dev` is serving the page. Vite inlines `import.meta.env.DEV`
45
+ * into the client bundle; the optional chain keeps the module loadable where
46
+ * no bundler defines it.
47
+ */
48
+ const isDevServer = {}.env?.DEV === true;
49
+ /**
50
+ * Sends the visitor of a prerendered page to the URL of their stored locale.
51
+ *
52
+ * Injected on every page by the `intlayer()` integration; a no-op once the URL
53
+ * already names a locale, so a redirected page does not redirect again.
54
+ */
55
+ const redirectToStoredLocale = () => {
56
+ if (typeof window === "undefined") return;
57
+ const target = getStoredLocaleRedirect(new URL(window.location.href), isDevServer);
58
+ if (target) window.location.replace(target);
59
+ };
60
+
61
+ //#endregion
62
+ exports.getStoredLocaleRedirect = getStoredLocaleRedirect;
63
+ exports.redirectToStoredLocale = redirectToStoredLocale;
64
+ //# sourceMappingURL=localeRedirect.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localeRedirect.cjs","names":["routing","internationalization","resolveProxyMode","isProxyStorageLocaleEnabled","getLocaleFromStorageClient","getLocalizedUrl"],"sources":["../../../src/client/localeRedirect.ts"],"sourcesContent":["import { internationalization, routing } from '@intlayer/config/built';\nimport {\n getLocalizedUrl,\n isProxyStorageLocaleEnabled,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport { getLocaleFromStorageClient } from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n\n/**\n * Locale explicitly carried by a URL — path prefix, or `?locale=` in\n * `search-params` mode. `undefined` when the URL is unlocalized, which in\n * `prefix-no-default` mode means it belongs to the default locale.\n */\nconst getExplicitUrlLocale = (url: URL): Locale | undefined => {\n const candidate =\n routing.mode === 'search-params'\n ? url.searchParams.get('locale')\n : url.pathname.split('/')[1];\n\n return candidate && internationalization.locales.includes(candidate as Locale)\n ? (candidate as Locale)\n : undefined;\n};\n\n/**\n * Computes where a prerendered page should send the visitor so it matches the\n * locale they stored (cookie, localStorage or sessionStorage, per\n * `routing.storage`).\n *\n * Mirrors the redirect the Intlayer proxy performs on server-rendered requests,\n * for pages served as static files where no server can read the cookie:\n * - the URL wins over storage, so a localized URL is never redirected;\n * - `routing.enableProxy: false` and `no-prefix` routing disable it;\n * - in auto mode a dev server keeps routing URL-driven, like the proxy.\n *\n * @param url - The URL of the current page.\n * @param isDevServer - Whether `astro dev` is serving the page.\n * @returns The localized URL to navigate to, or `undefined` to stay put.\n */\nexport const getStoredLocaleRedirect = (\n url: URL,\n isDevServer: boolean\n): string | undefined => {\n const proxyMode = resolveProxyMode(routing.enableProxy);\n\n if (proxyMode === 'disabled') return;\n if (!isProxyStorageLocaleEnabled(proxyMode, isDevServer)) return;\n if (routing.mode === 'no-prefix') return;\n if (getExplicitUrlLocale(url)) return;\n\n const storedLocale = getLocaleFromStorageClient();\n\n if (!storedLocale || storedLocale === internationalization.defaultLocale) {\n return;\n }\n\n const currentPath = `${url.pathname}${url.search}${url.hash}`;\n const localizedPath = getLocalizedUrl(currentPath, storedLocale);\n\n return localizedPath === currentPath ? undefined : localizedPath;\n};\n\n/**\n * Whether `astro dev` is serving the page. Vite inlines `import.meta.env.DEV`\n * into the client bundle; the optional chain keeps the module loadable where\n * no bundler defines it.\n */\nconst isDevServer =\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true;\n\n/**\n * Sends the visitor of a prerendered page to the URL of their stored locale.\n *\n * Injected on every page by the `intlayer()` integration; a no-op once the URL\n * already names a locale, so a redirected page does not redirect again.\n */\nexport const redirectToStoredLocale = (): void => {\n if (typeof window === 'undefined') return;\n\n const target = getStoredLocaleRedirect(\n new URL(window.location.href),\n isDevServer\n );\n\n if (target) window.location.replace(target);\n};\n"],"mappings":";;;;;;;;;;;AAcA,MAAM,wBAAwB,QAAiC;CAC7D,MAAM,YACJA,+BAAQ,SAAS,kBACb,IAAI,aAAa,IAAI,QAAQ,IAC7B,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC;CAE9B,OAAO,aAAaC,4CAAqB,QAAQ,SAAS,SAAmB,IACxE,YACD;AACN;;;;;;;;;;;;;;;;AAiBA,MAAa,2BACX,KACA,gBACuB;CACvB,MAAM,gBAAYC,8CAAiBF,+BAAQ,WAAW;CAEtD,IAAI,cAAc,YAAY;CAC9B,IAAI,KAACG,yDAA4B,WAAW,WAAW,GAAG;CAC1D,IAAIH,+BAAQ,SAAS,aAAa;CAClC,IAAI,qBAAqB,GAAG,GAAG;CAE/B,MAAM,mBAAeI,iDAA2B;CAEhD,IAAI,CAAC,gBAAgB,iBAAiBH,4CAAqB,eACzD;CAGF,MAAM,cAAc,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI;CACvD,MAAM,oBAAgBI,6CAAgB,aAAa,YAAY;CAE/D,OAAO,kBAAkB,cAAc,SAAY;AACrD;;;;;;AAOA,MAAM,iBACyC,KAAK,QAAQ;;;;;;;AAQ5D,MAAa,+BAAqC;CAChD,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,SAAS,wBACb,IAAI,IAAI,OAAO,SAAS,IAAI,GAC5B,WACF;CAEA,IAAI,QAAQ,OAAO,SAAS,QAAQ,MAAM;AAC5C"}
@@ -1,6 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_emitRewrittenPages = require('./emitRewrittenPages.cjs');
3
3
  let node_path = require("node:path");
4
+ let _intlayer_core_localization = require("@intlayer/core/localization");
5
+ let _intlayer_config_logger = require("@intlayer/config/logger");
4
6
  let _intlayer_config_node = require("@intlayer/config/node");
5
7
  let _intlayer_config_utils = require("@intlayer/config/utils");
6
8
  let _intlayer_engine_build = require("@intlayer/engine/build");
@@ -26,20 +28,28 @@ const intlayerNoExternalEnvironments = () => ({
26
28
  * optimizations (prune).
27
29
  * 3. Configures the Vite aliases for dictionary access.
28
30
  * 4. Registers the `astro-intlayer/middleware`, which resolves the request
29
- * locale into `Astro.locals.intlayer` for the hooks.
31
+ * locale into `Astro.locals.intlayer` for the hooks and redirects
32
+ * server-rendered requests to the visitor's locale.
33
+ * 5. Injects the client-side locale redirect, which does the same for
34
+ * prerendered pages: served as static files, nothing else can read the
35
+ * stored locale there.
30
36
  *
31
37
  * The dev-time content watcher is not started here: the bundled
32
38
  * `vite-intlayer` plugin already starts one from `configureServer`, and
33
39
  * `watch()` subscribes anew on every call, so doing both would rebuild each
34
40
  * content edit twice.
35
41
  */
36
- const configSetup = async ({ updateConfig, addMiddleware }) => {
42
+ const configSetup = async ({ command, updateConfig, addMiddleware, injectScript }) => {
37
43
  const configuration = (0, _intlayer_config_node.getConfiguration)();
38
44
  await (0, _intlayer_engine_build.prepareIntlayer)(configuration);
39
45
  addMiddleware({
40
46
  entrypoint: "astro-intlayer/middleware",
41
47
  order: "pre"
42
48
  });
49
+ if ((0, _intlayer_core_localization.resolveProxyMode)(configuration.routing.enableProxy) !== "disabled") {
50
+ injectScript("page", `import { redirectToStoredLocale } from 'astro-intlayer/client/locale-redirect';\nredirectToStoredLocale();`);
51
+ if (command === "preview") (0, _intlayer_config_logger.getAppLogger)(configuration)((0, _intlayer_core_localization.formatProxyEnabledMessage)(false), { level: "info" });
52
+ }
43
53
  updateConfig({ vite: {
44
54
  plugins: [(0, vite_intlayer.intlayer)(), intlayerNoExternalEnvironments()],
45
55
  resolve: { alias: { ...(0, _intlayer_config_utils.getAlias)({
@@ -1 +1 @@
1
- {"version":3,"file":"integration.cjs","names":["INTLAYER_NO_EXTERNAL_PATTERN","getConfiguration","prepareIntlayer","viteIntlayerPlugin","getAlias","resolve","emitRewrittenPages"],"sources":["../../src/integration.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n INTLAYER_NO_EXTERNAL_PATTERN,\n intlayer as viteIntlayerPlugin,\n} from 'vite-intlayer';\nimport { emitRewrittenPages } from './emitRewrittenPages';\n\n/**\n * Keeps the intlayer packages out of Node's native module loader in *every*\n * Vite environment Astro renders from.\n *\n * A static dev server renders from `ssr`, but other setups render from Astro's\n * `astro` or `prerender` environments, and a top-level `ssr.noExternal` only\n * seeds the `ssr` one. Declaring it per environment covers the rest.\n */\nconst intlayerNoExternalEnvironments = (): PluginOption => ({\n name: 'astro-intlayer-no-external',\n configEnvironment: () => ({\n resolve: { noExternal: [INTLAYER_NO_EXTERNAL_PATTERN] },\n }),\n});\n\n/**\n * Node-only half of the integration, loaded lazily by the `astro-intlayer`\n * entry when a hook runs.\n *\n * Pages import their hooks from that same entry, and Astro bundles the whole\n * package into the server build (`ssr.noExternal`), so the config loader,\n * `@intlayer/engine` and `vite-intlayer` must stay behind a dynamic import:\n * a static one would put them in the module graph of every page.\n */\n\ntype ConfigSetupOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:config:setup']>\n>[0];\n\ntype BuildDoneOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:build:done']>\n>[0];\n\n/**\n * `astro:config:setup`:\n * 1. Prepares the Intlayer resources (dictionaries).\n * 2. Injects the Vite plugins for aliases, locale-based routing and build\n * optimizations (prune).\n * 3. Configures the Vite aliases for dictionary access.\n * 4. Registers the `astro-intlayer/middleware`, which resolves the request\n * locale into `Astro.locals.intlayer` for the hooks.\n *\n * The dev-time content watcher is not started here: the bundled\n * `vite-intlayer` plugin already starts one from `configureServer`, and\n * `watch()` subscribes anew on every call, so doing both would rebuild each\n * content edit twice.\n */\nexport const configSetup = async ({\n updateConfig,\n addMiddleware,\n}: ConfigSetupOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n // Runs before the user middleware so `Astro.locals.intlayer` is set\n // everywhere, including in a hand-written `src/middleware.ts`.\n addMiddleware({\n entrypoint: 'astro-intlayer/middleware',\n order: 'pre',\n });\n\n updateConfig({\n vite: {\n plugins: [\n viteIntlayerPlugin(),\n intlayerNoExternalEnvironments(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n // `astro-intlayer` is tagged with the `astro` keyword, so Astro\n // treats it as an Astro package and crawls its dependency tree\n // (`vitefu`), force-externalizing every dependency it finds —\n // `@intlayer/core`, `@intlayer/config`, … — into\n // `resolve.external`. Vite checks `external` before `noExternal`,\n // so the `ssr.noExternal` that `vite-intlayer` returns from its\n // Vite `config` hook loses that race and the packages are loaded\n // natively by Node, stranding dictionary edits behind Node's\n // require cache. Declaring it here instead runs before the crawl,\n // which drops explicitly no-externalized packages from its result.\n ssr: {\n noExternal: [INTLAYER_NO_EXTERNAL_PATTERN],\n },\n },\n });\n};\n\n/**\n * `astro:build:done`: Astro renders each page from its canonical file-system\n * route, so a static build has no file for the localized paths declared in\n * `routing.rewrite`. Mirror them here, otherwise the URLs produced by\n * `getLocalizedUrl` (links, hreflang, sitemap) 404 once deployed.\n */\nexport const buildDone = async ({\n dir,\n logger,\n}: BuildDoneOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n const emittedPages = await emitRewrittenPages(configuration, dir);\n\n if (emittedPages.length > 0) {\n logger.info(\n `Emitted ${emittedPages.length} rewritten page(s): ${emittedPages\n .map(([from, to]) => `${from} \\u2192 ${to}`)\n .join(', ')}`\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,MAAM,wCAAsD;CAC1D,MAAM;CACN,0BAA0B,EACxB,SAAS,EAAE,YAAY,CAACA,0CAA4B,EAAE,EACxD;AACF;;;;;;;;;;;;;;;AAkCA,MAAa,cAAc,OAAO,EAChC,cACA,oBACuC;CACvC,MAAM,oBAAgBC,wCAAiB;CAGvC,UAAMC,wCAAgB,aAAa;CAInC,cAAc;EACZ,YAAY;EACZ,OAAO;CACT,CAAC;CAED,aAAa,EACX,MAAM;EACJ,SAAS,KACPC,wBAAmB,GACnB,+BAA+B,CACjC;EACA,SAAS,EACP,OAAO,EACL,OAAGC,iCAAS;GACV;GACA,YAAY,cAAUC,mBAAQ,KAAK;EACrC,CAAC,EACH,EACF;EAWA,KAAK,EACH,YAAY,CAACL,0CAA4B,EAC3C;CACF,EACF,CAAC;AACH;;;;;;;AAQA,MAAa,YAAY,OAAO,EAC9B,KACA,aACqC;CACrC,MAAM,oBAAgBC,wCAAiB;CAEvC,MAAM,eAAe,MAAMK,8CAAmB,eAAe,GAAG;CAEhE,IAAI,aAAa,SAAS,GACxB,OAAO,KACL,WAAW,aAAa,OAAO,sBAAsB,aAClD,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC,CAC3C,KAAK,IAAI,GACd;AAEJ"}
1
+ {"version":3,"file":"integration.cjs","names":["INTLAYER_NO_EXTERNAL_PATTERN","getConfiguration","prepareIntlayer","resolveProxyMode","formatProxyEnabledMessage","viteIntlayerPlugin","getAlias","resolve","emitRewrittenPages"],"sources":["../../src/integration.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport {\n formatProxyEnabledMessage,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n INTLAYER_NO_EXTERNAL_PATTERN,\n intlayer as viteIntlayerPlugin,\n} from 'vite-intlayer';\nimport { emitRewrittenPages } from './emitRewrittenPages';\n\n/**\n * Keeps the intlayer packages out of Node's native module loader in *every*\n * Vite environment Astro renders from.\n *\n * A static dev server renders from `ssr`, but other setups render from Astro's\n * `astro` or `prerender` environments, and a top-level `ssr.noExternal` only\n * seeds the `ssr` one. Declaring it per environment covers the rest.\n */\nconst intlayerNoExternalEnvironments = (): PluginOption => ({\n name: 'astro-intlayer-no-external',\n configEnvironment: () => ({\n resolve: { noExternal: [INTLAYER_NO_EXTERNAL_PATTERN] },\n }),\n});\n\n/**\n * Node-only half of the integration, loaded lazily by the `astro-intlayer`\n * entry when a hook runs.\n *\n * Pages import their hooks from that same entry, and Astro bundles the whole\n * package into the server build (`ssr.noExternal`), so the config loader,\n * `@intlayer/engine` and `vite-intlayer` must stay behind a dynamic import:\n * a static one would put them in the module graph of every page.\n */\n\ntype ConfigSetupOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:config:setup']>\n>[0];\n\ntype BuildDoneOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:build:done']>\n>[0];\n\n/**\n * `astro:config:setup`:\n * 1. Prepares the Intlayer resources (dictionaries).\n * 2. Injects the Vite plugins for aliases, locale-based routing and build\n * optimizations (prune).\n * 3. Configures the Vite aliases for dictionary access.\n * 4. Registers the `astro-intlayer/middleware`, which resolves the request\n * locale into `Astro.locals.intlayer` for the hooks and redirects\n * server-rendered requests to the visitor's locale.\n * 5. Injects the client-side locale redirect, which does the same for\n * prerendered pages: served as static files, nothing else can read the\n * stored locale there.\n *\n * The dev-time content watcher is not started here: the bundled\n * `vite-intlayer` plugin already starts one from `configureServer`, and\n * `watch()` subscribes anew on every call, so doing both would rebuild each\n * content edit twice.\n */\nexport const configSetup = async ({\n command,\n updateConfig,\n addMiddleware,\n injectScript,\n}: ConfigSetupOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n // Runs before the user middleware so `Astro.locals.intlayer` is set\n // everywhere, including in a hand-written `src/middleware.ts`.\n addMiddleware({\n entrypoint: 'astro-intlayer/middleware',\n order: 'pre',\n });\n\n const proxyMode = resolveProxyMode(configuration.routing.enableProxy);\n\n if (proxyMode !== 'disabled') {\n injectScript(\n 'page',\n `import { redirectToStoredLocale } from 'astro-intlayer/client/locale-redirect';\\nredirectToStoredLocale();`\n );\n\n // `dev` and `build` are announced by the bundled `vite-intlayer` plugin.\n // `astro preview` runs no Vite plugin, so the served output is announced\n // here: the stored locale drives redirects again once the output is\n // served — by the middleware on server-rendered pages, by the injected\n // script on static ones.\n if (command === 'preview') {\n getAppLogger(configuration)(formatProxyEnabledMessage(false), {\n level: 'info',\n });\n }\n }\n\n updateConfig({\n vite: {\n plugins: [\n viteIntlayerPlugin(),\n intlayerNoExternalEnvironments(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n // `astro-intlayer` is tagged with the `astro` keyword, so Astro\n // treats it as an Astro package and crawls its dependency tree\n // (`vitefu`), force-externalizing every dependency it finds —\n // `@intlayer/core`, `@intlayer/config`, … — into\n // `resolve.external`. Vite checks `external` before `noExternal`,\n // so the `ssr.noExternal` that `vite-intlayer` returns from its\n // Vite `config` hook loses that race and the packages are loaded\n // natively by Node, stranding dictionary edits behind Node's\n // require cache. Declaring it here instead runs before the crawl,\n // which drops explicitly no-externalized packages from its result.\n ssr: {\n noExternal: [INTLAYER_NO_EXTERNAL_PATTERN],\n },\n },\n });\n};\n\n/**\n * `astro:build:done`: Astro renders each page from its canonical file-system\n * route, so a static build has no file for the localized paths declared in\n * `routing.rewrite`. Mirror them here, otherwise the URLs produced by\n * `getLocalizedUrl` (links, hreflang, sitemap) 404 once deployed.\n */\nexport const buildDone = async ({\n dir,\n logger,\n}: BuildDoneOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n const emittedPages = await emitRewrittenPages(configuration, dir);\n\n if (emittedPages.length > 0) {\n logger.info(\n `Emitted ${emittedPages.length} rewritten page(s): ${emittedPages\n .map(([from, to]) => `${from} \\u2192 ${to}`)\n .join(', ')}`\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAyBA,MAAM,wCAAsD;CAC1D,MAAM;CACN,0BAA0B,EACxB,SAAS,EAAE,YAAY,CAACA,0CAA4B,EAAE,EACxD;AACF;;;;;;;;;;;;;;;;;;;AAsCA,MAAa,cAAc,OAAO,EAChC,SACA,cACA,eACA,mBACuC;CACvC,MAAM,oBAAgBC,wCAAiB;CAGvC,UAAMC,wCAAgB,aAAa;CAInC,cAAc;EACZ,YAAY;EACZ,OAAO;CACT,CAAC;CAID,QAFkBC,8CAAiB,cAAc,QAAQ,WAE7C,MAAM,YAAY;EAC5B,aACE,QACA,4GACF;EAOA,IAAI,YAAY,WACd,0CAAa,aAAa,CAAC,KAACC,uDAA0B,KAAK,GAAG,EAC5D,OAAO,OACT,CAAC;CAEL;CAEA,aAAa,EACX,MAAM;EACJ,SAAS,KACPC,wBAAmB,GACnB,+BAA+B,CACjC;EACA,SAAS,EACP,OAAO,EACL,OAAGC,iCAAS;GACV;GACA,YAAY,cAAUC,mBAAQ,KAAK;EACrC,CAAC,EACH,EACF;EAWA,KAAK,EACH,YAAY,CAACP,0CAA4B,EAC3C;CACF,EACF,CAAC;AACH;;;;;;;AAQA,MAAa,YAAY,OAAO,EAC9B,KACA,aACqC;CACrC,MAAM,oBAAgBC,wCAAiB;CAEvC,MAAM,eAAe,MAAMO,8CAAmB,eAAe,GAAG;CAEhE,IAAI,aAAa,SAAS,GACxB,OAAO,KACL,WAAW,aAAa,OAAO,sBAAsB,aAClD,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC,CAC3C,KAAK,IAAI,GACd;AAEJ"}
@@ -1,16 +1,66 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
3
  const require_requestStorage = require('./requestStorage.cjs');
3
4
  let _intlayer_core_localization = require("@intlayer/core/localization");
4
5
  let _intlayer_config_built = require("@intlayer/config/built");
6
+ _intlayer_config_built = require_runtime.__toESM(_intlayer_config_built);
7
+ let vite_intlayer_web_proxy_handler = require("vite-intlayer/web-proxy-handler");
5
8
 
6
9
  //#region src/middleware.ts
7
10
  /**
11
+ * Whether `astro dev` is serving the app. Vite inlines `import.meta.env.DEV`
12
+ * into the server bundle; the optional chain keeps the module loadable where
13
+ * no bundler defines it (`process.env.NODE_ENV` cannot be used instead: the
14
+ * package build inlines it).
15
+ */
16
+ const isDevServer = {}.env?.DEV === true;
17
+ const configuration = _intlayer_config_built;
18
+ const { internationalization, routing } = configuration;
19
+ /**
20
+ * Locale-routing proxy shared with the Vite dev server and the Nitro handler.
21
+ *
22
+ * `undefined` when `routing.enableProxy` is `false`. In auto mode (option
23
+ * unset) `astro dev` keeps routing URL-driven: the stored locale is not a
24
+ * redirect source, exactly like the Vite proxy mounted on the same dev server,
25
+ * so the two never disagree.
26
+ */
27
+ const proxy = (0, _intlayer_core_localization.resolveProxyMode)(routing.enableProxy) === "disabled" ? void 0 : (0, vite_intlayer_web_proxy_handler.createIntlayerWebProxyHandler)({
28
+ configuration,
29
+ isDevServer
30
+ });
31
+ /**
32
+ * Appends the headers the proxy produced (the persisted locale cookie) to the
33
+ * rendered response. Some responses ship immutable headers, in which case the
34
+ * body is re-wrapped in a fresh `Response`.
35
+ */
36
+ const withProxyHeaders = (response, headers) => {
37
+ const entries = [...headers];
38
+ if (entries.length === 0) return response;
39
+ const applyHeaders = (target) => {
40
+ for (const [name, value] of entries) target.headers.append(name, value);
41
+ return target;
42
+ };
43
+ try {
44
+ return applyHeaders(response);
45
+ } catch {
46
+ return applyHeaders(new Response(response.body, response));
47
+ }
48
+ };
49
+ /**
8
50
  * Astro middleware resolving the request locale into `Astro.locals.intlayer`.
9
51
  *
10
52
  * Priority: the URL (path prefix or `?locale=` search param, in every routing
11
53
  * mode but `no-prefix`), then the locale persisted by the client (cookie or
12
54
  * custom header), then `Accept-Language` negotiation.
13
55
  *
56
+ * On server-rendered requests it also runs the Intlayer locale-routing proxy,
57
+ * so a visitor whose stored locale is `fr` is redirected from `/` to `/fr`
58
+ * (per `routing.mode`) and the resolved locale is persisted on the response.
59
+ * Only redirects are honoured: the proxy's internal rewrites target
60
+ * prefix-based routers, while Astro pages already resolve the locale from the
61
+ * URL they are rendered for. Prerendered pages are served as static files, so
62
+ * nothing can redirect them at request time and the proxy is skipped.
63
+ *
14
64
  * The `intlayer()` integration registers it automatically, before the user
15
65
  * middleware, so `Astro.locals.intlayer` is available in every page, endpoint
16
66
  * and middleware. The rest of the request renders inside an
@@ -27,15 +77,25 @@ let _intlayer_config_built = require("@intlayer/config/built");
27
77
  * ```
28
78
  */
29
79
  const onRequest = async (context, next) => {
80
+ let proxyOutcome;
81
+ if (proxy && !context.isPrerendered) {
82
+ const outcome = proxy({
83
+ url: context.url,
84
+ headers: context.request.headers
85
+ });
86
+ if (outcome.kind === "response") return outcome.response;
87
+ proxyOutcome = outcome;
88
+ }
30
89
  context.locals.intlayer = {
31
90
  locale: await (0, _intlayer_core_localization.getLocaleFromRequest)({
32
91
  url: context.url,
33
92
  headers: context.isPrerendered ? new Headers() : context.request.headers
34
93
  }),
35
- defaultLocale: _intlayer_config_built.internationalization.defaultLocale,
36
- availableLocales: _intlayer_config_built.internationalization.locales
94
+ defaultLocale: internationalization.defaultLocale,
95
+ availableLocales: internationalization.locales
37
96
  };
38
- return require_requestStorage.requestStorage.run(context.locals, next);
97
+ const response = await require_requestStorage.requestStorage.run(context.locals, next);
98
+ return proxyOutcome ? withProxyHeaders(response, proxyOutcome.headers) : response;
39
99
  };
40
100
 
41
101
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.cjs","names":["getLocaleFromRequest","internationalization","requestStorage"],"sources":["../../src/middleware.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport { getLocaleFromRequest } from '@intlayer/core/localization';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport type { MiddlewareHandler } from 'astro';\nimport { requestStorage } from './requestStorage';\n\n/**\n * Intlayer state stored in `Astro.locals.intlayer` by the {@link onRequest} middleware.\n */\nexport type IntlayerLocals = {\n /** Locale resolved for the current request. */\n locale: DeclaredLocales;\n /** Locale configured as fallback in `intlayer.config.ts`. */\n defaultLocale: DeclaredLocales;\n /** Every locale declared in `intlayer.config.ts`. */\n availableLocales: DeclaredLocales[];\n};\n\ndeclare global {\n namespace App {\n interface Locals {\n intlayer: IntlayerLocals;\n }\n }\n}\n\n/**\n * Astro middleware resolving the request locale into `Astro.locals.intlayer`.\n *\n * Priority: the URL (path prefix or `?locale=` search param, in every routing\n * mode but `no-prefix`), then the locale persisted by the client (cookie or\n * custom header), then `Accept-Language` negotiation.\n *\n * The `intlayer()` integration registers it automatically, before the user\n * middleware, so `Astro.locals.intlayer` is available in every page, endpoint\n * and middleware. The rest of the request renders inside an\n * `AsyncLocalStorage` scope bound to those locals, which is how the hooks of\n * `astro-intlayer/server` read the locale with no argument.\n *\n * @example\n * ```ts\n * // src/middleware.ts — only needed when composing it by hand\n * import { onRequest as intlayer } from 'astro-intlayer/middleware';\n * import { sequence } from 'astro:middleware';\n *\n * export const onRequest = sequence(intlayer, myMiddleware);\n * ```\n */\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n context.locals.intlayer = {\n locale: await getLocaleFromRequest({\n url: context.url,\n // A prerendered page is rendered once and served to every visitor, so\n // its locale can only come from the URL. Reading the request headers\n // there would also make Astro warn on every page of the build.\n headers: context.isPrerendered ? new Headers() : context.request.headers,\n }),\n defaultLocale: internationalization.defaultLocale as DeclaredLocales,\n availableLocales: internationalization.locales as DeclaredLocales[],\n };\n\n return requestStorage.run(context.locals, next);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,YAA+B,OAAO,SAAS,SAAS;CACnE,QAAQ,OAAO,WAAW;EACxB,QAAQ,UAAMA,kDAAqB;GACjC,KAAK,QAAQ;GAIb,SAAS,QAAQ,gBAAgB,IAAI,QAAQ,IAAI,QAAQ,QAAQ;EACnE,CAAC;EACD,eAAeC,4CAAqB;EACpC,kBAAkBA,4CAAqB;CACzC;CAEA,OAAOC,sCAAe,IAAI,QAAQ,QAAQ,IAAI;AAChD"}
1
+ {"version":3,"file":"middleware.cjs","names":["builtConfiguration","resolveProxyMode","createIntlayerWebProxyHandler","getLocaleFromRequest","requestStorage"],"sources":["../../src/middleware.ts"],"sourcesContent":["import * as builtConfiguration from '@intlayer/config/built';\nimport {\n getLocaleFromRequest,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport type { MiddlewareHandler } from 'astro';\nimport {\n createIntlayerWebProxyHandler,\n type WebProxyOutcome,\n} from 'vite-intlayer/web-proxy-handler';\nimport { requestStorage } from './requestStorage';\n\n/** Proxy outcome letting the request continue to the page. */\ntype WebProxyNextOutcome = Extract<WebProxyOutcome, { kind: 'next' }>;\n\n/**\n * Whether `astro dev` is serving the app. Vite inlines `import.meta.env.DEV`\n * into the server bundle; the optional chain keeps the module loadable where\n * no bundler defines it (`process.env.NODE_ENV` cannot be used instead: the\n * package build inlines it).\n */\nconst isDevServer =\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true;\n\nconst configuration = builtConfiguration as unknown as IntlayerConfig;\nconst { internationalization, routing } = configuration;\n\n/**\n * Locale-routing proxy shared with the Vite dev server and the Nitro handler.\n *\n * `undefined` when `routing.enableProxy` is `false`. In auto mode (option\n * unset) `astro dev` keeps routing URL-driven: the stored locale is not a\n * redirect source, exactly like the Vite proxy mounted on the same dev server,\n * so the two never disagree.\n */\nconst proxy =\n resolveProxyMode(routing.enableProxy) === 'disabled'\n ? undefined\n : createIntlayerWebProxyHandler({\n configuration,\n isDevServer,\n });\n\n/**\n * Appends the headers the proxy produced (the persisted locale cookie) to the\n * rendered response. Some responses ship immutable headers, in which case the\n * body is re-wrapped in a fresh `Response`.\n */\nconst withProxyHeaders = (response: Response, headers: Headers): Response => {\n const entries = [...headers];\n if (entries.length === 0) return response;\n\n const applyHeaders = (target: Response) => {\n for (const [name, value] of entries) {\n target.headers.append(name, value);\n }\n return target;\n };\n\n try {\n return applyHeaders(response);\n } catch {\n return applyHeaders(new Response(response.body, response));\n }\n};\n\n/**\n * Intlayer state stored in `Astro.locals.intlayer` by the {@link onRequest} middleware.\n */\nexport type IntlayerLocals = {\n /** Locale resolved for the current request. */\n locale: DeclaredLocales;\n /** Locale configured as fallback in `intlayer.config.ts`. */\n defaultLocale: DeclaredLocales;\n /** Every locale declared in `intlayer.config.ts`. */\n availableLocales: DeclaredLocales[];\n};\n\ndeclare global {\n namespace App {\n interface Locals {\n intlayer: IntlayerLocals;\n }\n }\n}\n\n/**\n * Astro middleware resolving the request locale into `Astro.locals.intlayer`.\n *\n * Priority: the URL (path prefix or `?locale=` search param, in every routing\n * mode but `no-prefix`), then the locale persisted by the client (cookie or\n * custom header), then `Accept-Language` negotiation.\n *\n * On server-rendered requests it also runs the Intlayer locale-routing proxy,\n * so a visitor whose stored locale is `fr` is redirected from `/` to `/fr`\n * (per `routing.mode`) and the resolved locale is persisted on the response.\n * Only redirects are honoured: the proxy's internal rewrites target\n * prefix-based routers, while Astro pages already resolve the locale from the\n * URL they are rendered for. Prerendered pages are served as static files, so\n * nothing can redirect them at request time and the proxy is skipped.\n *\n * The `intlayer()` integration registers it automatically, before the user\n * middleware, so `Astro.locals.intlayer` is available in every page, endpoint\n * and middleware. The rest of the request renders inside an\n * `AsyncLocalStorage` scope bound to those locals, which is how the hooks of\n * `astro-intlayer/server` read the locale with no argument.\n *\n * @example\n * ```ts\n * // src/middleware.ts — only needed when composing it by hand\n * import { onRequest as intlayer } from 'astro-intlayer/middleware';\n * import { sequence } from 'astro:middleware';\n *\n * export const onRequest = sequence(intlayer, myMiddleware);\n * ```\n */\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n let proxyOutcome: WebProxyNextOutcome | undefined;\n\n if (proxy && !context.isPrerendered) {\n const outcome = proxy({\n url: context.url,\n headers: context.request.headers,\n });\n\n if (outcome.kind === 'response') return outcome.response;\n proxyOutcome = outcome;\n }\n\n context.locals.intlayer = {\n locale: await getLocaleFromRequest({\n url: context.url,\n // A prerendered page is rendered once and served to every visitor, so\n // its locale can only come from the URL. Reading the request headers\n // there would also make Astro warn on every page of the build.\n headers: context.isPrerendered ? new Headers() : context.request.headers,\n }),\n defaultLocale: internationalization.defaultLocale as DeclaredLocales,\n availableLocales: internationalization.locales as DeclaredLocales[],\n };\n\n const response = await requestStorage.run(context.locals, next);\n\n return proxyOutcome\n ? withProxyHeaders(response, proxyOutcome.headers)\n : response;\n};\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,MAAM,iBACyC,KAAK,QAAQ;AAE5D,MAAM,gBAAgBA;AACtB,MAAM,EAAE,sBAAsB,YAAY;;;;;;;;;AAU1C,MAAM,YACJC,8CAAiB,QAAQ,WAAW,MAAM,aACtC,aACAC,+DAA8B;CAC5B;CACA;AACF,CAAC;;;;;;AAOP,MAAM,oBAAoB,UAAoB,YAA+B;CAC3E,MAAM,UAAU,CAAC,GAAG,OAAO;CAC3B,IAAI,QAAQ,WAAW,GAAG,OAAO;CAEjC,MAAM,gBAAgB,WAAqB;EACzC,KAAK,MAAM,CAAC,MAAM,UAAU,SAC1B,OAAO,QAAQ,OAAO,MAAM,KAAK;EAEnC,OAAO;CACT;CAEA,IAAI;EACF,OAAO,aAAa,QAAQ;CAC9B,QAAQ;EACN,OAAO,aAAa,IAAI,SAAS,SAAS,MAAM,QAAQ,CAAC;CAC3D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,YAA+B,OAAO,SAAS,SAAS;CACnE,IAAI;CAEJ,IAAI,SAAS,CAAC,QAAQ,eAAe;EACnC,MAAM,UAAU,MAAM;GACpB,KAAK,QAAQ;GACb,SAAS,QAAQ,QAAQ;EAC3B,CAAC;EAED,IAAI,QAAQ,SAAS,YAAY,OAAO,QAAQ;EAChD,eAAe;CACjB;CAEA,QAAQ,OAAO,WAAW;EACxB,QAAQ,UAAMC,kDAAqB;GACjC,KAAK,QAAQ;GAIb,SAAS,QAAQ,gBAAgB,IAAI,QAAQ,IAAI,QAAQ,QAAQ;EACnE,CAAC;EACD,eAAe,qBAAqB;EACpC,kBAAkB,qBAAqB;CACzC;CAEA,MAAM,WAAW,MAAMC,sCAAe,IAAI,QAAQ,QAAQ,IAAI;CAE9D,OAAO,eACH,iBAAiB,UAAU,aAAa,OAAO,IAC/C;AACN"}
@@ -0,0 +1,62 @@
1
+ import { getLocalizedUrl, isProxyStorageLocaleEnabled, resolveProxyMode } from "@intlayer/core/localization";
2
+ import { internationalization, routing } from "@intlayer/config/built";
3
+ import { getLocaleFromStorageClient } from "@intlayer/core/utils";
4
+
5
+ //#region src/client/localeRedirect.ts
6
+ /**
7
+ * Locale explicitly carried by a URL — path prefix, or `?locale=` in
8
+ * `search-params` mode. `undefined` when the URL is unlocalized, which in
9
+ * `prefix-no-default` mode means it belongs to the default locale.
10
+ */
11
+ const getExplicitUrlLocale = (url) => {
12
+ const candidate = routing.mode === "search-params" ? url.searchParams.get("locale") : url.pathname.split("/")[1];
13
+ return candidate && internationalization.locales.includes(candidate) ? candidate : void 0;
14
+ };
15
+ /**
16
+ * Computes where a prerendered page should send the visitor so it matches the
17
+ * locale they stored (cookie, localStorage or sessionStorage, per
18
+ * `routing.storage`).
19
+ *
20
+ * Mirrors the redirect the Intlayer proxy performs on server-rendered requests,
21
+ * for pages served as static files where no server can read the cookie:
22
+ * - the URL wins over storage, so a localized URL is never redirected;
23
+ * - `routing.enableProxy: false` and `no-prefix` routing disable it;
24
+ * - in auto mode a dev server keeps routing URL-driven, like the proxy.
25
+ *
26
+ * @param url - The URL of the current page.
27
+ * @param isDevServer - Whether `astro dev` is serving the page.
28
+ * @returns The localized URL to navigate to, or `undefined` to stay put.
29
+ */
30
+ const getStoredLocaleRedirect = (url, isDevServer) => {
31
+ const proxyMode = resolveProxyMode(routing.enableProxy);
32
+ if (proxyMode === "disabled") return;
33
+ if (!isProxyStorageLocaleEnabled(proxyMode, isDevServer)) return;
34
+ if (routing.mode === "no-prefix") return;
35
+ if (getExplicitUrlLocale(url)) return;
36
+ const storedLocale = getLocaleFromStorageClient();
37
+ if (!storedLocale || storedLocale === internationalization.defaultLocale) return;
38
+ const currentPath = `${url.pathname}${url.search}${url.hash}`;
39
+ const localizedPath = getLocalizedUrl(currentPath, storedLocale);
40
+ return localizedPath === currentPath ? void 0 : localizedPath;
41
+ };
42
+ /**
43
+ * Whether `astro dev` is serving the page. Vite inlines `import.meta.env.DEV`
44
+ * into the client bundle; the optional chain keeps the module loadable where
45
+ * no bundler defines it.
46
+ */
47
+ const isDevServer = import.meta.env?.DEV === true;
48
+ /**
49
+ * Sends the visitor of a prerendered page to the URL of their stored locale.
50
+ *
51
+ * Injected on every page by the `intlayer()` integration; a no-op once the URL
52
+ * already names a locale, so a redirected page does not redirect again.
53
+ */
54
+ const redirectToStoredLocale = () => {
55
+ if (typeof window === "undefined") return;
56
+ const target = getStoredLocaleRedirect(new URL(window.location.href), isDevServer);
57
+ if (target) window.location.replace(target);
58
+ };
59
+
60
+ //#endregion
61
+ export { getStoredLocaleRedirect, redirectToStoredLocale };
62
+ //# sourceMappingURL=localeRedirect.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localeRedirect.mjs","names":[],"sources":["../../../src/client/localeRedirect.ts"],"sourcesContent":["import { internationalization, routing } from '@intlayer/config/built';\nimport {\n getLocalizedUrl,\n isProxyStorageLocaleEnabled,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport { getLocaleFromStorageClient } from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n\n/**\n * Locale explicitly carried by a URL — path prefix, or `?locale=` in\n * `search-params` mode. `undefined` when the URL is unlocalized, which in\n * `prefix-no-default` mode means it belongs to the default locale.\n */\nconst getExplicitUrlLocale = (url: URL): Locale | undefined => {\n const candidate =\n routing.mode === 'search-params'\n ? url.searchParams.get('locale')\n : url.pathname.split('/')[1];\n\n return candidate && internationalization.locales.includes(candidate as Locale)\n ? (candidate as Locale)\n : undefined;\n};\n\n/**\n * Computes where a prerendered page should send the visitor so it matches the\n * locale they stored (cookie, localStorage or sessionStorage, per\n * `routing.storage`).\n *\n * Mirrors the redirect the Intlayer proxy performs on server-rendered requests,\n * for pages served as static files where no server can read the cookie:\n * - the URL wins over storage, so a localized URL is never redirected;\n * - `routing.enableProxy: false` and `no-prefix` routing disable it;\n * - in auto mode a dev server keeps routing URL-driven, like the proxy.\n *\n * @param url - The URL of the current page.\n * @param isDevServer - Whether `astro dev` is serving the page.\n * @returns The localized URL to navigate to, or `undefined` to stay put.\n */\nexport const getStoredLocaleRedirect = (\n url: URL,\n isDevServer: boolean\n): string | undefined => {\n const proxyMode = resolveProxyMode(routing.enableProxy);\n\n if (proxyMode === 'disabled') return;\n if (!isProxyStorageLocaleEnabled(proxyMode, isDevServer)) return;\n if (routing.mode === 'no-prefix') return;\n if (getExplicitUrlLocale(url)) return;\n\n const storedLocale = getLocaleFromStorageClient();\n\n if (!storedLocale || storedLocale === internationalization.defaultLocale) {\n return;\n }\n\n const currentPath = `${url.pathname}${url.search}${url.hash}`;\n const localizedPath = getLocalizedUrl(currentPath, storedLocale);\n\n return localizedPath === currentPath ? undefined : localizedPath;\n};\n\n/**\n * Whether `astro dev` is serving the page. Vite inlines `import.meta.env.DEV`\n * into the client bundle; the optional chain keeps the module loadable where\n * no bundler defines it.\n */\nconst isDevServer =\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true;\n\n/**\n * Sends the visitor of a prerendered page to the URL of their stored locale.\n *\n * Injected on every page by the `intlayer()` integration; a no-op once the URL\n * already names a locale, so a redirected page does not redirect again.\n */\nexport const redirectToStoredLocale = (): void => {\n if (typeof window === 'undefined') return;\n\n const target = getStoredLocaleRedirect(\n new URL(window.location.href),\n isDevServer\n );\n\n if (target) window.location.replace(target);\n};\n"],"mappings":";;;;;;;;;;AAcA,MAAM,wBAAwB,QAAiC;CAC7D,MAAM,YACJ,QAAQ,SAAS,kBACb,IAAI,aAAa,IAAI,QAAQ,IAC7B,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC;CAE9B,OAAO,aAAa,qBAAqB,QAAQ,SAAS,SAAmB,IACxE,YACD;AACN;;;;;;;;;;;;;;;;AAiBA,MAAa,2BACX,KACA,gBACuB;CACvB,MAAM,YAAY,iBAAiB,QAAQ,WAAW;CAEtD,IAAI,cAAc,YAAY;CAC9B,IAAI,CAAC,4BAA4B,WAAW,WAAW,GAAG;CAC1D,IAAI,QAAQ,SAAS,aAAa;CAClC,IAAI,qBAAqB,GAAG,GAAG;CAE/B,MAAM,eAAe,2BAA2B;CAEhD,IAAI,CAAC,gBAAgB,iBAAiB,qBAAqB,eACzD;CAGF,MAAM,cAAc,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI;CACvD,MAAM,gBAAgB,gBAAgB,aAAa,YAAY;CAE/D,OAAO,kBAAkB,cAAc,SAAY;AACrD;;;;;;AAOA,MAAM,cACH,YAA4C,KAAK,QAAQ;;;;;;;AAQ5D,MAAa,+BAAqC;CAChD,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,SAAS,wBACb,IAAI,IAAI,OAAO,SAAS,IAAI,GAC5B,WACF;CAEA,IAAI,QAAQ,OAAO,SAAS,QAAQ,MAAM;AAC5C"}
@@ -1,5 +1,7 @@
1
1
  import { emitRewrittenPages } from "./emitRewrittenPages.mjs";
2
2
  import { resolve } from "node:path";
3
+ import { formatProxyEnabledMessage, resolveProxyMode } from "@intlayer/core/localization";
4
+ import { getAppLogger } from "@intlayer/config/logger";
3
5
  import { getConfiguration } from "@intlayer/config/node";
4
6
  import { getAlias } from "@intlayer/config/utils";
5
7
  import { prepareIntlayer } from "@intlayer/engine/build";
@@ -25,20 +27,28 @@ const intlayerNoExternalEnvironments = () => ({
25
27
  * optimizations (prune).
26
28
  * 3. Configures the Vite aliases for dictionary access.
27
29
  * 4. Registers the `astro-intlayer/middleware`, which resolves the request
28
- * locale into `Astro.locals.intlayer` for the hooks.
30
+ * locale into `Astro.locals.intlayer` for the hooks and redirects
31
+ * server-rendered requests to the visitor's locale.
32
+ * 5. Injects the client-side locale redirect, which does the same for
33
+ * prerendered pages: served as static files, nothing else can read the
34
+ * stored locale there.
29
35
  *
30
36
  * The dev-time content watcher is not started here: the bundled
31
37
  * `vite-intlayer` plugin already starts one from `configureServer`, and
32
38
  * `watch()` subscribes anew on every call, so doing both would rebuild each
33
39
  * content edit twice.
34
40
  */
35
- const configSetup = async ({ updateConfig, addMiddleware }) => {
41
+ const configSetup = async ({ command, updateConfig, addMiddleware, injectScript }) => {
36
42
  const configuration = getConfiguration();
37
43
  await prepareIntlayer(configuration);
38
44
  addMiddleware({
39
45
  entrypoint: "astro-intlayer/middleware",
40
46
  order: "pre"
41
47
  });
48
+ if (resolveProxyMode(configuration.routing.enableProxy) !== "disabled") {
49
+ injectScript("page", `import { redirectToStoredLocale } from 'astro-intlayer/client/locale-redirect';\nredirectToStoredLocale();`);
50
+ if (command === "preview") getAppLogger(configuration)(formatProxyEnabledMessage(false), { level: "info" });
51
+ }
42
52
  updateConfig({ vite: {
43
53
  plugins: [intlayer(), intlayerNoExternalEnvironments()],
44
54
  resolve: { alias: { ...getAlias({
@@ -1 +1 @@
1
- {"version":3,"file":"integration.mjs","names":["viteIntlayerPlugin"],"sources":["../../src/integration.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n INTLAYER_NO_EXTERNAL_PATTERN,\n intlayer as viteIntlayerPlugin,\n} from 'vite-intlayer';\nimport { emitRewrittenPages } from './emitRewrittenPages';\n\n/**\n * Keeps the intlayer packages out of Node's native module loader in *every*\n * Vite environment Astro renders from.\n *\n * A static dev server renders from `ssr`, but other setups render from Astro's\n * `astro` or `prerender` environments, and a top-level `ssr.noExternal` only\n * seeds the `ssr` one. Declaring it per environment covers the rest.\n */\nconst intlayerNoExternalEnvironments = (): PluginOption => ({\n name: 'astro-intlayer-no-external',\n configEnvironment: () => ({\n resolve: { noExternal: [INTLAYER_NO_EXTERNAL_PATTERN] },\n }),\n});\n\n/**\n * Node-only half of the integration, loaded lazily by the `astro-intlayer`\n * entry when a hook runs.\n *\n * Pages import their hooks from that same entry, and Astro bundles the whole\n * package into the server build (`ssr.noExternal`), so the config loader,\n * `@intlayer/engine` and `vite-intlayer` must stay behind a dynamic import:\n * a static one would put them in the module graph of every page.\n */\n\ntype ConfigSetupOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:config:setup']>\n>[0];\n\ntype BuildDoneOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:build:done']>\n>[0];\n\n/**\n * `astro:config:setup`:\n * 1. Prepares the Intlayer resources (dictionaries).\n * 2. Injects the Vite plugins for aliases, locale-based routing and build\n * optimizations (prune).\n * 3. Configures the Vite aliases for dictionary access.\n * 4. Registers the `astro-intlayer/middleware`, which resolves the request\n * locale into `Astro.locals.intlayer` for the hooks.\n *\n * The dev-time content watcher is not started here: the bundled\n * `vite-intlayer` plugin already starts one from `configureServer`, and\n * `watch()` subscribes anew on every call, so doing both would rebuild each\n * content edit twice.\n */\nexport const configSetup = async ({\n updateConfig,\n addMiddleware,\n}: ConfigSetupOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n // Runs before the user middleware so `Astro.locals.intlayer` is set\n // everywhere, including in a hand-written `src/middleware.ts`.\n addMiddleware({\n entrypoint: 'astro-intlayer/middleware',\n order: 'pre',\n });\n\n updateConfig({\n vite: {\n plugins: [\n viteIntlayerPlugin(),\n intlayerNoExternalEnvironments(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n // `astro-intlayer` is tagged with the `astro` keyword, so Astro\n // treats it as an Astro package and crawls its dependency tree\n // (`vitefu`), force-externalizing every dependency it finds —\n // `@intlayer/core`, `@intlayer/config`, … — into\n // `resolve.external`. Vite checks `external` before `noExternal`,\n // so the `ssr.noExternal` that `vite-intlayer` returns from its\n // Vite `config` hook loses that race and the packages are loaded\n // natively by Node, stranding dictionary edits behind Node's\n // require cache. Declaring it here instead runs before the crawl,\n // which drops explicitly no-externalized packages from its result.\n ssr: {\n noExternal: [INTLAYER_NO_EXTERNAL_PATTERN],\n },\n },\n });\n};\n\n/**\n * `astro:build:done`: Astro renders each page from its canonical file-system\n * route, so a static build has no file for the localized paths declared in\n * `routing.rewrite`. Mirror them here, otherwise the URLs produced by\n * `getLocalizedUrl` (links, hreflang, sitemap) 404 once deployed.\n */\nexport const buildDone = async ({\n dir,\n logger,\n}: BuildDoneOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n const emittedPages = await emitRewrittenPages(configuration, dir);\n\n if (emittedPages.length > 0) {\n logger.info(\n `Emitted ${emittedPages.length} rewritten page(s): ${emittedPages\n .map(([from, to]) => `${from} \\u2192 ${to}`)\n .join(', ')}`\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,MAAM,wCAAsD;CAC1D,MAAM;CACN,0BAA0B,EACxB,SAAS,EAAE,YAAY,CAAC,4BAA4B,EAAE,EACxD;AACF;;;;;;;;;;;;;;;AAkCA,MAAa,cAAc,OAAO,EAChC,cACA,oBACuC;CACvC,MAAM,gBAAgB,iBAAiB;CAGvC,MAAM,gBAAgB,aAAa;CAInC,cAAc;EACZ,YAAY;EACZ,OAAO;CACT,CAAC;CAED,aAAa,EACX,MAAM;EACJ,SAAS,CACPA,SAAmB,GACnB,+BAA+B,CACjC;EACA,SAAS,EACP,OAAO,EACL,GAAG,SAAS;GACV;GACA,YAAY,UAAU,QAAQ,KAAK;EACrC,CAAC,EACH,EACF;EAWA,KAAK,EACH,YAAY,CAAC,4BAA4B,EAC3C;CACF,EACF,CAAC;AACH;;;;;;;AAQA,MAAa,YAAY,OAAO,EAC9B,KACA,aACqC;CACrC,MAAM,gBAAgB,iBAAiB;CAEvC,MAAM,eAAe,MAAM,mBAAmB,eAAe,GAAG;CAEhE,IAAI,aAAa,SAAS,GACxB,OAAO,KACL,WAAW,aAAa,OAAO,sBAAsB,aAClD,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC,CAC3C,KAAK,IAAI,GACd;AAEJ"}
1
+ {"version":3,"file":"integration.mjs","names":["viteIntlayerPlugin"],"sources":["../../src/integration.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getAlias } from '@intlayer/config/utils';\nimport {\n formatProxyEnabledMessage,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport { prepareIntlayer } from '@intlayer/engine/build';\nimport type { AstroIntegration } from 'astro';\nimport type { PluginOption } from 'vite';\nimport {\n INTLAYER_NO_EXTERNAL_PATTERN,\n intlayer as viteIntlayerPlugin,\n} from 'vite-intlayer';\nimport { emitRewrittenPages } from './emitRewrittenPages';\n\n/**\n * Keeps the intlayer packages out of Node's native module loader in *every*\n * Vite environment Astro renders from.\n *\n * A static dev server renders from `ssr`, but other setups render from Astro's\n * `astro` or `prerender` environments, and a top-level `ssr.noExternal` only\n * seeds the `ssr` one. Declaring it per environment covers the rest.\n */\nconst intlayerNoExternalEnvironments = (): PluginOption => ({\n name: 'astro-intlayer-no-external',\n configEnvironment: () => ({\n resolve: { noExternal: [INTLAYER_NO_EXTERNAL_PATTERN] },\n }),\n});\n\n/**\n * Node-only half of the integration, loaded lazily by the `astro-intlayer`\n * entry when a hook runs.\n *\n * Pages import their hooks from that same entry, and Astro bundles the whole\n * package into the server build (`ssr.noExternal`), so the config loader,\n * `@intlayer/engine` and `vite-intlayer` must stay behind a dynamic import:\n * a static one would put them in the module graph of every page.\n */\n\ntype ConfigSetupOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:config:setup']>\n>[0];\n\ntype BuildDoneOptions = Parameters<\n NonNullable<AstroIntegration['hooks']['astro:build:done']>\n>[0];\n\n/**\n * `astro:config:setup`:\n * 1. Prepares the Intlayer resources (dictionaries).\n * 2. Injects the Vite plugins for aliases, locale-based routing and build\n * optimizations (prune).\n * 3. Configures the Vite aliases for dictionary access.\n * 4. Registers the `astro-intlayer/middleware`, which resolves the request\n * locale into `Astro.locals.intlayer` for the hooks and redirects\n * server-rendered requests to the visitor's locale.\n * 5. Injects the client-side locale redirect, which does the same for\n * prerendered pages: served as static files, nothing else can read the\n * stored locale there.\n *\n * The dev-time content watcher is not started here: the bundled\n * `vite-intlayer` plugin already starts one from `configureServer`, and\n * `watch()` subscribes anew on every call, so doing both would rebuild each\n * content edit twice.\n */\nexport const configSetup = async ({\n command,\n updateConfig,\n addMiddleware,\n injectScript,\n}: ConfigSetupOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n // Prepare once per process start to ensure generated entries exist\n await prepareIntlayer(configuration);\n\n // Runs before the user middleware so `Astro.locals.intlayer` is set\n // everywhere, including in a hand-written `src/middleware.ts`.\n addMiddleware({\n entrypoint: 'astro-intlayer/middleware',\n order: 'pre',\n });\n\n const proxyMode = resolveProxyMode(configuration.routing.enableProxy);\n\n if (proxyMode !== 'disabled') {\n injectScript(\n 'page',\n `import { redirectToStoredLocale } from 'astro-intlayer/client/locale-redirect';\\nredirectToStoredLocale();`\n );\n\n // `dev` and `build` are announced by the bundled `vite-intlayer` plugin.\n // `astro preview` runs no Vite plugin, so the served output is announced\n // here: the stored locale drives redirects again once the output is\n // served — by the middleware on server-rendered pages, by the injected\n // script on static ones.\n if (command === 'preview') {\n getAppLogger(configuration)(formatProxyEnabledMessage(false), {\n level: 'info',\n });\n }\n }\n\n updateConfig({\n vite: {\n plugins: [\n viteIntlayerPlugin(),\n intlayerNoExternalEnvironments(),\n ] as PluginOption[],\n resolve: {\n alias: {\n ...getAlias({\n configuration,\n formatter: (value) => resolve(value),\n }),\n },\n },\n // `astro-intlayer` is tagged with the `astro` keyword, so Astro\n // treats it as an Astro package and crawls its dependency tree\n // (`vitefu`), force-externalizing every dependency it finds —\n // `@intlayer/core`, `@intlayer/config`, … — into\n // `resolve.external`. Vite checks `external` before `noExternal`,\n // so the `ssr.noExternal` that `vite-intlayer` returns from its\n // Vite `config` hook loses that race and the packages are loaded\n // natively by Node, stranding dictionary edits behind Node's\n // require cache. Declaring it here instead runs before the crawl,\n // which drops explicitly no-externalized packages from its result.\n ssr: {\n noExternal: [INTLAYER_NO_EXTERNAL_PATTERN],\n },\n },\n });\n};\n\n/**\n * `astro:build:done`: Astro renders each page from its canonical file-system\n * route, so a static build has no file for the localized paths declared in\n * `routing.rewrite`. Mirror them here, otherwise the URLs produced by\n * `getLocalizedUrl` (links, hreflang, sitemap) 404 once deployed.\n */\nexport const buildDone = async ({\n dir,\n logger,\n}: BuildDoneOptions): Promise<void> => {\n const configuration = getConfiguration();\n\n const emittedPages = await emitRewrittenPages(configuration, dir);\n\n if (emittedPages.length > 0) {\n logger.info(\n `Emitted ${emittedPages.length} rewritten page(s): ${emittedPages\n .map(([from, to]) => `${from} \\u2192 ${to}`)\n .join(', ')}`\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyBA,MAAM,wCAAsD;CAC1D,MAAM;CACN,0BAA0B,EACxB,SAAS,EAAE,YAAY,CAAC,4BAA4B,EAAE,EACxD;AACF;;;;;;;;;;;;;;;;;;;AAsCA,MAAa,cAAc,OAAO,EAChC,SACA,cACA,eACA,mBACuC;CACvC,MAAM,gBAAgB,iBAAiB;CAGvC,MAAM,gBAAgB,aAAa;CAInC,cAAc;EACZ,YAAY;EACZ,OAAO;CACT,CAAC;CAID,IAFkB,iBAAiB,cAAc,QAAQ,WAE7C,MAAM,YAAY;EAC5B,aACE,QACA,4GACF;EAOA,IAAI,YAAY,WACd,aAAa,aAAa,CAAC,CAAC,0BAA0B,KAAK,GAAG,EAC5D,OAAO,OACT,CAAC;CAEL;CAEA,aAAa,EACX,MAAM;EACJ,SAAS,CACPA,SAAmB,GACnB,+BAA+B,CACjC;EACA,SAAS,EACP,OAAO,EACL,GAAG,SAAS;GACV;GACA,YAAY,UAAU,QAAQ,KAAK;EACrC,CAAC,EACH,EACF;EAWA,KAAK,EACH,YAAY,CAAC,4BAA4B,EAC3C;CACF,EACF,CAAC;AACH;;;;;;;AAQA,MAAa,YAAY,OAAO,EAC9B,KACA,aACqC;CACrC,MAAM,gBAAgB,iBAAiB;CAEvC,MAAM,eAAe,MAAM,mBAAmB,eAAe,GAAG;CAEhE,IAAI,aAAa,SAAS,GACxB,OAAO,KACL,WAAW,aAAa,OAAO,sBAAsB,aAClD,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC,CAC3C,KAAK,IAAI,GACd;AAEJ"}
@@ -1,15 +1,63 @@
1
1
  import { requestStorage } from "./requestStorage.mjs";
2
- import { getLocaleFromRequest } from "@intlayer/core/localization";
3
- import { internationalization } from "@intlayer/config/built";
2
+ import { getLocaleFromRequest, resolveProxyMode } from "@intlayer/core/localization";
3
+ import * as builtConfiguration from "@intlayer/config/built";
4
+ import { createIntlayerWebProxyHandler } from "vite-intlayer/web-proxy-handler";
4
5
 
5
6
  //#region src/middleware.ts
6
7
  /**
8
+ * Whether `astro dev` is serving the app. Vite inlines `import.meta.env.DEV`
9
+ * into the server bundle; the optional chain keeps the module loadable where
10
+ * no bundler defines it (`process.env.NODE_ENV` cannot be used instead: the
11
+ * package build inlines it).
12
+ */
13
+ const isDevServer = import.meta.env?.DEV === true;
14
+ const configuration = builtConfiguration;
15
+ const { internationalization: internationalization$1, routing: routing$1 } = configuration;
16
+ /**
17
+ * Locale-routing proxy shared with the Vite dev server and the Nitro handler.
18
+ *
19
+ * `undefined` when `routing.enableProxy` is `false`. In auto mode (option
20
+ * unset) `astro dev` keeps routing URL-driven: the stored locale is not a
21
+ * redirect source, exactly like the Vite proxy mounted on the same dev server,
22
+ * so the two never disagree.
23
+ */
24
+ const proxy = resolveProxyMode(routing$1.enableProxy) === "disabled" ? void 0 : createIntlayerWebProxyHandler({
25
+ configuration,
26
+ isDevServer
27
+ });
28
+ /**
29
+ * Appends the headers the proxy produced (the persisted locale cookie) to the
30
+ * rendered response. Some responses ship immutable headers, in which case the
31
+ * body is re-wrapped in a fresh `Response`.
32
+ */
33
+ const withProxyHeaders = (response, headers) => {
34
+ const entries = [...headers];
35
+ if (entries.length === 0) return response;
36
+ const applyHeaders = (target) => {
37
+ for (const [name, value] of entries) target.headers.append(name, value);
38
+ return target;
39
+ };
40
+ try {
41
+ return applyHeaders(response);
42
+ } catch {
43
+ return applyHeaders(new Response(response.body, response));
44
+ }
45
+ };
46
+ /**
7
47
  * Astro middleware resolving the request locale into `Astro.locals.intlayer`.
8
48
  *
9
49
  * Priority: the URL (path prefix or `?locale=` search param, in every routing
10
50
  * mode but `no-prefix`), then the locale persisted by the client (cookie or
11
51
  * custom header), then `Accept-Language` negotiation.
12
52
  *
53
+ * On server-rendered requests it also runs the Intlayer locale-routing proxy,
54
+ * so a visitor whose stored locale is `fr` is redirected from `/` to `/fr`
55
+ * (per `routing.mode`) and the resolved locale is persisted on the response.
56
+ * Only redirects are honoured: the proxy's internal rewrites target
57
+ * prefix-based routers, while Astro pages already resolve the locale from the
58
+ * URL they are rendered for. Prerendered pages are served as static files, so
59
+ * nothing can redirect them at request time and the proxy is skipped.
60
+ *
13
61
  * The `intlayer()` integration registers it automatically, before the user
14
62
  * middleware, so `Astro.locals.intlayer` is available in every page, endpoint
15
63
  * and middleware. The rest of the request renders inside an
@@ -26,15 +74,25 @@ import { internationalization } from "@intlayer/config/built";
26
74
  * ```
27
75
  */
28
76
  const onRequest = async (context, next) => {
77
+ let proxyOutcome;
78
+ if (proxy && !context.isPrerendered) {
79
+ const outcome = proxy({
80
+ url: context.url,
81
+ headers: context.request.headers
82
+ });
83
+ if (outcome.kind === "response") return outcome.response;
84
+ proxyOutcome = outcome;
85
+ }
29
86
  context.locals.intlayer = {
30
87
  locale: await getLocaleFromRequest({
31
88
  url: context.url,
32
89
  headers: context.isPrerendered ? new Headers() : context.request.headers
33
90
  }),
34
- defaultLocale: internationalization.defaultLocale,
35
- availableLocales: internationalization.locales
91
+ defaultLocale: internationalization$1.defaultLocale,
92
+ availableLocales: internationalization$1.locales
36
93
  };
37
- return requestStorage.run(context.locals, next);
94
+ const response = await requestStorage.run(context.locals, next);
95
+ return proxyOutcome ? withProxyHeaders(response, proxyOutcome.headers) : response;
38
96
  };
39
97
 
40
98
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/middleware.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport { getLocaleFromRequest } from '@intlayer/core/localization';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport type { MiddlewareHandler } from 'astro';\nimport { requestStorage } from './requestStorage';\n\n/**\n * Intlayer state stored in `Astro.locals.intlayer` by the {@link onRequest} middleware.\n */\nexport type IntlayerLocals = {\n /** Locale resolved for the current request. */\n locale: DeclaredLocales;\n /** Locale configured as fallback in `intlayer.config.ts`. */\n defaultLocale: DeclaredLocales;\n /** Every locale declared in `intlayer.config.ts`. */\n availableLocales: DeclaredLocales[];\n};\n\ndeclare global {\n namespace App {\n interface Locals {\n intlayer: IntlayerLocals;\n }\n }\n}\n\n/**\n * Astro middleware resolving the request locale into `Astro.locals.intlayer`.\n *\n * Priority: the URL (path prefix or `?locale=` search param, in every routing\n * mode but `no-prefix`), then the locale persisted by the client (cookie or\n * custom header), then `Accept-Language` negotiation.\n *\n * The `intlayer()` integration registers it automatically, before the user\n * middleware, so `Astro.locals.intlayer` is available in every page, endpoint\n * and middleware. The rest of the request renders inside an\n * `AsyncLocalStorage` scope bound to those locals, which is how the hooks of\n * `astro-intlayer/server` read the locale with no argument.\n *\n * @example\n * ```ts\n * // src/middleware.ts — only needed when composing it by hand\n * import { onRequest as intlayer } from 'astro-intlayer/middleware';\n * import { sequence } from 'astro:middleware';\n *\n * export const onRequest = sequence(intlayer, myMiddleware);\n * ```\n */\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n context.locals.intlayer = {\n locale: await getLocaleFromRequest({\n url: context.url,\n // A prerendered page is rendered once and served to every visitor, so\n // its locale can only come from the URL. Reading the request headers\n // there would also make Astro warn on every page of the build.\n headers: context.isPrerendered ? new Headers() : context.request.headers,\n }),\n defaultLocale: internationalization.defaultLocale as DeclaredLocales,\n availableLocales: internationalization.locales as DeclaredLocales[],\n };\n\n return requestStorage.run(context.locals, next);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,YAA+B,OAAO,SAAS,SAAS;CACnE,QAAQ,OAAO,WAAW;EACxB,QAAQ,MAAM,qBAAqB;GACjC,KAAK,QAAQ;GAIb,SAAS,QAAQ,gBAAgB,IAAI,QAAQ,IAAI,QAAQ,QAAQ;EACnE,CAAC;EACD,eAAe,qBAAqB;EACpC,kBAAkB,qBAAqB;CACzC;CAEA,OAAO,eAAe,IAAI,QAAQ,QAAQ,IAAI;AAChD"}
1
+ {"version":3,"file":"middleware.mjs","names":["routing","internationalization"],"sources":["../../src/middleware.ts"],"sourcesContent":["import * as builtConfiguration from '@intlayer/config/built';\nimport {\n getLocaleFromRequest,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { DeclaredLocales } from '@intlayer/types/module_augmentation';\nimport type { MiddlewareHandler } from 'astro';\nimport {\n createIntlayerWebProxyHandler,\n type WebProxyOutcome,\n} from 'vite-intlayer/web-proxy-handler';\nimport { requestStorage } from './requestStorage';\n\n/** Proxy outcome letting the request continue to the page. */\ntype WebProxyNextOutcome = Extract<WebProxyOutcome, { kind: 'next' }>;\n\n/**\n * Whether `astro dev` is serving the app. Vite inlines `import.meta.env.DEV`\n * into the server bundle; the optional chain keeps the module loadable where\n * no bundler defines it (`process.env.NODE_ENV` cannot be used instead: the\n * package build inlines it).\n */\nconst isDevServer =\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true;\n\nconst configuration = builtConfiguration as unknown as IntlayerConfig;\nconst { internationalization, routing } = configuration;\n\n/**\n * Locale-routing proxy shared with the Vite dev server and the Nitro handler.\n *\n * `undefined` when `routing.enableProxy` is `false`. In auto mode (option\n * unset) `astro dev` keeps routing URL-driven: the stored locale is not a\n * redirect source, exactly like the Vite proxy mounted on the same dev server,\n * so the two never disagree.\n */\nconst proxy =\n resolveProxyMode(routing.enableProxy) === 'disabled'\n ? undefined\n : createIntlayerWebProxyHandler({\n configuration,\n isDevServer,\n });\n\n/**\n * Appends the headers the proxy produced (the persisted locale cookie) to the\n * rendered response. Some responses ship immutable headers, in which case the\n * body is re-wrapped in a fresh `Response`.\n */\nconst withProxyHeaders = (response: Response, headers: Headers): Response => {\n const entries = [...headers];\n if (entries.length === 0) return response;\n\n const applyHeaders = (target: Response) => {\n for (const [name, value] of entries) {\n target.headers.append(name, value);\n }\n return target;\n };\n\n try {\n return applyHeaders(response);\n } catch {\n return applyHeaders(new Response(response.body, response));\n }\n};\n\n/**\n * Intlayer state stored in `Astro.locals.intlayer` by the {@link onRequest} middleware.\n */\nexport type IntlayerLocals = {\n /** Locale resolved for the current request. */\n locale: DeclaredLocales;\n /** Locale configured as fallback in `intlayer.config.ts`. */\n defaultLocale: DeclaredLocales;\n /** Every locale declared in `intlayer.config.ts`. */\n availableLocales: DeclaredLocales[];\n};\n\ndeclare global {\n namespace App {\n interface Locals {\n intlayer: IntlayerLocals;\n }\n }\n}\n\n/**\n * Astro middleware resolving the request locale into `Astro.locals.intlayer`.\n *\n * Priority: the URL (path prefix or `?locale=` search param, in every routing\n * mode but `no-prefix`), then the locale persisted by the client (cookie or\n * custom header), then `Accept-Language` negotiation.\n *\n * On server-rendered requests it also runs the Intlayer locale-routing proxy,\n * so a visitor whose stored locale is `fr` is redirected from `/` to `/fr`\n * (per `routing.mode`) and the resolved locale is persisted on the response.\n * Only redirects are honoured: the proxy's internal rewrites target\n * prefix-based routers, while Astro pages already resolve the locale from the\n * URL they are rendered for. Prerendered pages are served as static files, so\n * nothing can redirect them at request time and the proxy is skipped.\n *\n * The `intlayer()` integration registers it automatically, before the user\n * middleware, so `Astro.locals.intlayer` is available in every page, endpoint\n * and middleware. The rest of the request renders inside an\n * `AsyncLocalStorage` scope bound to those locals, which is how the hooks of\n * `astro-intlayer/server` read the locale with no argument.\n *\n * @example\n * ```ts\n * // src/middleware.ts — only needed when composing it by hand\n * import { onRequest as intlayer } from 'astro-intlayer/middleware';\n * import { sequence } from 'astro:middleware';\n *\n * export const onRequest = sequence(intlayer, myMiddleware);\n * ```\n */\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n let proxyOutcome: WebProxyNextOutcome | undefined;\n\n if (proxy && !context.isPrerendered) {\n const outcome = proxy({\n url: context.url,\n headers: context.request.headers,\n });\n\n if (outcome.kind === 'response') return outcome.response;\n proxyOutcome = outcome;\n }\n\n context.locals.intlayer = {\n locale: await getLocaleFromRequest({\n url: context.url,\n // A prerendered page is rendered once and served to every visitor, so\n // its locale can only come from the URL. Reading the request headers\n // there would also make Astro warn on every page of the build.\n headers: context.isPrerendered ? new Headers() : context.request.headers,\n }),\n defaultLocale: internationalization.defaultLocale as DeclaredLocales,\n availableLocales: internationalization.locales as DeclaredLocales[],\n };\n\n const response = await requestStorage.run(context.locals, next);\n\n return proxyOutcome\n ? withProxyHeaders(response, proxyOutcome.headers)\n : response;\n};\n"],"mappings":";;;;;;;;;;;;AAuBA,MAAM,cACH,YAA4C,KAAK,QAAQ;AAE5D,MAAM,gBAAgB;AACtB,MAAM,EAAE,8CAAsB,uBAAY;;;;;;;;;AAU1C,MAAM,QACJ,iBAAiBA,UAAQ,WAAW,MAAM,aACtC,SACA,8BAA8B;CAC5B;CACA;AACF,CAAC;;;;;;AAOP,MAAM,oBAAoB,UAAoB,YAA+B;CAC3E,MAAM,UAAU,CAAC,GAAG,OAAO;CAC3B,IAAI,QAAQ,WAAW,GAAG,OAAO;CAEjC,MAAM,gBAAgB,WAAqB;EACzC,KAAK,MAAM,CAAC,MAAM,UAAU,SAC1B,OAAO,QAAQ,OAAO,MAAM,KAAK;EAEnC,OAAO;CACT;CAEA,IAAI;EACF,OAAO,aAAa,QAAQ;CAC9B,QAAQ;EACN,OAAO,aAAa,IAAI,SAAS,SAAS,MAAM,QAAQ,CAAC;CAC3D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,YAA+B,OAAO,SAAS,SAAS;CACnE,IAAI;CAEJ,IAAI,SAAS,CAAC,QAAQ,eAAe;EACnC,MAAM,UAAU,MAAM;GACpB,KAAK,QAAQ;GACb,SAAS,QAAQ,QAAQ;EAC3B,CAAC;EAED,IAAI,QAAQ,SAAS,YAAY,OAAO,QAAQ;EAChD,eAAe;CACjB;CAEA,QAAQ,OAAO,WAAW;EACxB,QAAQ,MAAM,qBAAqB;GACjC,KAAK,QAAQ;GAIb,SAAS,QAAQ,gBAAgB,IAAI,QAAQ,IAAI,QAAQ,QAAQ;EACnE,CAAC;EACD,eAAeC,uBAAqB;EACpC,kBAAkBA,uBAAqB;CACzC;CAEA,MAAM,WAAW,MAAM,eAAe,IAAI,QAAQ,QAAQ,IAAI;CAE9D,OAAO,eACH,iBAAiB,UAAU,aAAa,OAAO,IAC/C;AACN"}
@@ -0,0 +1,26 @@
1
+ //#region src/client/localeRedirect.d.ts
2
+ /**
3
+ * Computes where a prerendered page should send the visitor so it matches the
4
+ * locale they stored (cookie, localStorage or sessionStorage, per
5
+ * `routing.storage`).
6
+ *
7
+ * Mirrors the redirect the Intlayer proxy performs on server-rendered requests,
8
+ * for pages served as static files where no server can read the cookie:
9
+ * - the URL wins over storage, so a localized URL is never redirected;
10
+ * - `routing.enableProxy: false` and `no-prefix` routing disable it;
11
+ * - in auto mode a dev server keeps routing URL-driven, like the proxy.
12
+ *
13
+ * @param url - The URL of the current page.
14
+ * @param isDevServer - Whether `astro dev` is serving the page.
15
+ * @returns The localized URL to navigate to, or `undefined` to stay put.
16
+ */
17
+ export declare const getStoredLocaleRedirect: (url: URL, isDevServer: boolean) => string | undefined;
18
+ /**
19
+ * Sends the visitor of a prerendered page to the URL of their stored locale.
20
+ *
21
+ * Injected on every page by the `intlayer()` integration; a no-op once the URL
22
+ * already names a locale, so a redirected page does not redirect again.
23
+ */
24
+ export declare const redirectToStoredLocale: () => void;
25
+ //#endregion
26
+ //# sourceMappingURL=localeRedirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localeRedirect.d.ts","names":[],"sources":["../../../src/client/localeRedirect.ts"],"mappings":";;;;;;;;;;;;;;;;qBAwCa,0BAAuB,KAC7B,KAAG;;;;;;;qBAoCG"}
@@ -18,14 +18,18 @@ type BuildDoneOptions = Parameters<NonNullable<AstroIntegration['hooks']['astro:
18
18
  * optimizations (prune).
19
19
  * 3. Configures the Vite aliases for dictionary access.
20
20
  * 4. Registers the `astro-intlayer/middleware`, which resolves the request
21
- * locale into `Astro.locals.intlayer` for the hooks.
21
+ * locale into `Astro.locals.intlayer` for the hooks and redirects
22
+ * server-rendered requests to the visitor's locale.
23
+ * 5. Injects the client-side locale redirect, which does the same for
24
+ * prerendered pages: served as static files, nothing else can read the
25
+ * stored locale there.
22
26
  *
23
27
  * The dev-time content watcher is not started here: the bundled
24
28
  * `vite-intlayer` plugin already starts one from `configureServer`, and
25
29
  * `watch()` subscribes anew on every call, so doing both would rebuild each
26
30
  * content edit twice.
27
31
  */
28
- export declare const configSetup: ({ updateConfig, addMiddleware }: ConfigSetupOptions) => Promise<void>;
32
+ export declare const configSetup: ({ command, updateConfig, addMiddleware, injectScript }: ConfigSetupOptions) => Promise<void>;
29
33
  /**
30
34
  * `astro:build:done`: Astro renders each page from its canonical file-system
31
35
  * route, so a static build has no file for the localized paths declared in
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","names":[],"sources":["../../src/integration.ts"],"mappings":";;;;;;;;;;;KAqCK,qBAAqB,WACxB,YAAY;KAGT,mBAAmB,WACtB,YAAY;;;;;;;;;;;;;;;qBAiBD,gBAAW,cAAA,iBAGrB,uBAAqB;;;;;;;qBAkDX,cAAS,KAAA,UAGnB,qBAAmB"}
1
+ {"version":3,"file":"integration.d.ts","names":[],"sources":["../../src/integration.ts"],"mappings":";;;;;;;;;;;KA0CK,qBAAqB,WACxB,YAAY;KAGT,mBAAmB,WACtB,YAAY;;;;;;;;;;;;;;;;;;;qBAqBD,gBAAW,SAAA,cAAA,eAAA,gBAKrB,uBAAqB;;;;;;;qBAsEX,cAAS,KAAA,UAGnB,qBAAmB"}
@@ -26,6 +26,14 @@ declare global {
26
26
  * mode but `no-prefix`), then the locale persisted by the client (cookie or
27
27
  * custom header), then `Accept-Language` negotiation.
28
28
  *
29
+ * On server-rendered requests it also runs the Intlayer locale-routing proxy,
30
+ * so a visitor whose stored locale is `fr` is redirected from `/` to `/fr`
31
+ * (per `routing.mode`) and the resolved locale is persisted on the response.
32
+ * Only redirects are honoured: the proxy's internal rewrites target
33
+ * prefix-based routers, while Astro pages already resolve the locale from the
34
+ * URL they are rendered for. Prerendered pages are served as static files, so
35
+ * nothing can redirect them at request time and the proxy is skipped.
36
+ *
29
37
  * The `intlayer()` integration registers it automatically, before the user
30
38
  * middleware, so `Astro.locals.intlayer` is available in every page, endpoint
31
39
  * and middleware. The rest of the request renders inside an
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","names":[],"sources":["../../src/middleware.ts"],"mappings":";;;;;;YASY;;EAEV,QAAQ;;EAER,eAAe;;EAEf,kBAAkB;;QAGZ;YACI;cACE;MACR,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;qBA2BH,WAAW"}
1
+ {"version":3,"file":"middleware.d.ts","names":[],"sources":["../../src/middleware.ts"],"mappings":";;;;;;YAuEY;;EAEV,QAAQ;;EAER,eAAe;;EAEf,kBAAkB;;QAGZ;YACI;cACE;MACR,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmCH,WAAW"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-intlayer",
3
- "version": "9.5.5",
3
+ "version": "9.5.6",
4
4
  "private": false,
5
5
  "description": "Easily internationalize i18n your Astro applications with type-safe multilingual content management.",
6
6
  "keywords": [
@@ -77,6 +77,11 @@
77
77
  "require": "./dist/cjs/client/index.cjs",
78
78
  "import": "./dist/esm/client/index.mjs"
79
79
  },
80
+ "./client/locale-redirect": {
81
+ "types": "./dist/types/client/localeRedirect.d.ts",
82
+ "require": "./dist/cjs/client/localeRedirect.cjs",
83
+ "import": "./dist/esm/client/localeRedirect.mjs"
84
+ },
80
85
  "./integration": {
81
86
  "types": "./dist/types/integration.d.ts",
82
87
  "require": "./dist/cjs/integration.cjs",
@@ -116,6 +121,9 @@
116
121
  "client": [
117
122
  "./dist/types/client/index.d.ts"
118
123
  ],
124
+ "client/locale-redirect": [
125
+ "./dist/types/client/localeRedirect.d.ts"
126
+ ],
119
127
  "format": [
120
128
  "./dist/types/format/index.d.ts"
121
129
  ],
@@ -159,15 +167,15 @@
159
167
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
160
168
  },
161
169
  "dependencies": {
162
- "@intlayer/config": "9.5.5",
163
- "@intlayer/core": "9.5.5",
164
- "@intlayer/engine": "9.5.5",
165
- "@intlayer/types": "9.5.5",
166
- "vanilla-intlayer": "9.5.5",
167
- "vite-intlayer": "9.5.5"
170
+ "@intlayer/config": "9.5.6",
171
+ "@intlayer/core": "9.5.6",
172
+ "@intlayer/engine": "9.5.6",
173
+ "@intlayer/types": "9.5.6",
174
+ "vanilla-intlayer": "9.5.6",
175
+ "vite-intlayer": "9.5.6"
168
176
  },
169
177
  "devDependencies": {
170
- "@intlayer/dictionaries-entry": "9.5.5",
178
+ "@intlayer/dictionaries-entry": "9.5.6",
171
179
  "@types/node": "^22",
172
180
  "@utils/ts-config": "1.0.4",
173
181
  "@utils/ts-config-types": "1.0.4",