{intl.formatMessage(messages.greeting, { name })}
{intl.formatMessage(messages.items, { count })}
{m.cart_items({ count })}
``` 4. ### Scope each render with middleware src/middleware.ts ```ts import { defineMiddleware } from 'astro:middleware' import { runWithLocale } from 'loclizr/server' export const onRequest = defineMiddleware((context, next) => { const { lang } = context.params return lang === undefined ? next() : runWithLocale(lang, next) }) ``` Middleware runs for every prerendered page at build time, and its scope covers each child component. `npm run build` writes: dist/de/index.html ```html3 Artikel in deinem Warenkorb
``` dist/en/index.html ```html3 items in your cart
``` Caution Passing `{ locale: Astro.params.lang }` per call instead also typechecks, but a call that misses it renders the source language and the build still passes. Caution Never call `setLocale` in frontmatter: outside a scope it sets one process-wide locale that pages built in parallel share, and inside the middleware it throws. ## On-demand rendering This replaces the static setup: one page at `/` picks the language per request. Step 2 replaces `src/middleware.ts` and step 3 deletes `src/pages/[lang]/`. 1. ### Add the Node adapter ```bash npx astro add node --yes ``` `astro add` writes the adapter; add `output: 'server'` yourself. astro.config.mjs ```js // @ts-check import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; // https://astro.build/config export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }) }); ``` 2. ### Replace the middleware src/middleware.ts ```ts import { defineMiddleware } from 'astro:middleware' import { localeFromRequest, runWithLocale } from 'loclizr/server' import * as m from './loclizr/messages' const options = { locales: m.locales, sourceLocale: m.sourceLocale, cookie: m.cookie } export const onRequest = defineMiddleware(async (context, next) => { const locale = localeFromRequest(context.request, options) const response = await runWithLocale(locale, next) response.headers.set('Content-Language', locale) response.headers.append('Vary', 'Accept-Language, Cookie') return response }) ``` `localeFromRequest` reads the configured cookie (`m.cookie`), then `Accept-Language`, then the source locale. 3. ### Serve the page from `/` Delete `src/pages/[lang]/`, then replace `src/pages/index.astro` with the same page minus `getStaticPaths`: src/pages/index.astro ```astro --- import * as m from '../loclizr/messages' import CartBadge from '../components/CartBadge.astro' ---3 Artikel in deinem Warenkorb
``` curl -si -H 'Accept-Language: en-US' -H 'Cookie: locale=de' http\://localhost:8080/ ```txt HTTP/1.1 200 OK content-language: de content-type: text/html vary: Accept-Language, Cookie Date: Sat, 10 Oct 2026 18:51:05 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked3 Artikel in deinem Warenkorb
``` The cookie beats `Accept-Language`. Keep localized responses out of shared caches unless the cache keys on the locale cookie, as [Server rendering](https://yasinmiran.github.io/loclizr/guides/server-rendering/) explains. ## Islands Wire the island like any client app: [React](https://yasinmiran.github.io/loclizr/guides/react/), [Svelte](https://yasinmiran.github.io/loclizr/guides/svelte/), [Vue](https://yasinmiran.github.io/loclizr/guides/vue/) or [Solid](https://yasinmiran.github.io/loclizr/guides/solid/). A server-rendered Vue island must not subscribe during the server render, or each render leaves a listener on the process-wide store; the [Vue composable](https://yasinmiran.github.io/loclizr/guides/vue/#write-the-composable) subscribes in `onMounted`, which runs only in the browser. With on-demand rendering, render `` from `m.getLocale()`, as above. The client store reads the cookie, then ``, and the server negotiated from the same cookie or `Accept-Language`, so an island hydrates in the server’s language. On the static `[lang]` route the URL picks the language, but the client store still reads the cookie before ``. Once a visitor has the cookie: * An island on `/en/` hydrates in the cookie’s language, and React reports a hydration mismatch. * The store rewrites `` to the cookie’s language. * A `setLocale` switcher updates only the islands; Astro-rendered text stays in the URL’s language. The locale comes from the cookie by design ([Limits](https://yasinmiran.github.io/loclizr/project/limits/#the-locale-never-comes-from-the-url)). On this route, switch language by linking to `/{tag}/`, and sync the cookie to the URL’s locale before any island hydrates: src/pages/\[lang]/index.astro ```astro{intl.formatMessage(messages.items, { count })}
{m.cart_items({ count })}
{m.cart_total({ amount: total })}
{m.cart_items({ count: cart.count })}
{m.cart_items({ count: props.count }, { locale: locale() })}
} ``` src/Switcher.tsx ```tsx import { For } from 'solid-js' import { createLocale } from './locale' import * as m from './loclizr/messages' export function Switcher() { const locale = createLocale() return ({items}
``` A bare `{m.nav_cart()}` never reruns: the message reads the store, and the store is not a rune. Pick one of the two forms: | Form | On a switch | | ------------------------------------- | ---------------------------------------------------- | | `{#key locale.current}` | remounts the block; component state inside it resets | | `{ locale: locale.current }` per call | reruns that expression in place; state survives | Caution The context record misses a call inside a quoted attribute value: `title="{m.nav_home()}"` records no usage for `nav_home`. Write `title={m.nav_home()}`; markup text and `{{ m.cart_items({ count }) }}
``` `Cart` reads no ref and its prop does not change, so after a switch it still says `3 items in your cart`. 5. ### Key the root on the locale src/Root.vue ```vue