Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started
On this page

Internationalization

This module's documentation site is published at https://i18n.meekojs.com and lives in apps/i18n. It includes guides and API documentation, with a playground for interpolation, plurals, selectors, formatting and RTL using real Spanish, English and Arabic catalogs. Start it with pnpm i18n:dev and build it with pnpm i18n:build. It shares the private packages/docs documentation base with the UI site and does not need a database.

@meekojs/i18n is a standalone Meeko module. Catalogs are validated and compiled when preparing the application; their keys and parameters are generated for TypeScript. The configuration does not require UI or install a second copy of the framework.

Configuring catalogs#

ts
// meeko.config.ts
import { defineMeekoConfig } from '@meekojs/meeko/framework'

export default defineMeekoConfig({
  modules: ['@meekojs/i18n'],
  i18n: {
    defaultLocale: 'es',
    catalogs: {
      es: { common: 'i18n/es/common.json' },
      en: { common: 'i18n/en/common.json' },
    },
  },
})

Register the module by name and place its options under the i18n key (its configKey), typed without any imports: Meeko loads its types into .meeko/types/config.d.ts (see Modules). '@meekojs/i18n' resolves the @meekojs/i18n/module subpath.

Each locale needs a common namespace. Languages must declare the same namespaces, keys and parameter contracts. Catalog paths must stay within the project. Catalogs load by language and namespace; routes lets you add namespaces to specific route IDs. global lists namespaces that always load along with common, on every route, its /en/… versions and the error page, such as the texts of a shared Shell (global: ['shell']); each name must exist in every language and cannot be common. directions lets you mark languages as rtl.

A key whose namespace the route did not load renders the key itself instead of breaking the document, and the module warns once per language and key on the server (logs and prerendering) and, with meeko dev, also in the browser: [meeko:i18n] Missing message …. createI18n throws ReferenceError unless it receives missing(key, locale).

Typed messages and parameters#

json
{
  "welcome": "Hola {name}",
  "ready": "Listo",
  "items": {
    "plural": "total",
    "forms": {
      "one": "{owner} tiene {count} elemento",
      "other": "{owner} tiene {count} elementos"
    }
  }
}
ts
const { t } = useI18n()
t('welcome', { name: 'Diego' })
t('ready')
t('items', { total: 3, owner: 'El equipo' })

Required parameters cannot be omitted or be null or undefined. Plural selectors require number; {count} is the numeric alias the runtime provides to their forms. Additional parameters used by any form are also required. select requires its selector and its forms' parameters. Every plural or select structure must declare other.

meeko check detects unknown keys, missing parameters and incorrect types. When preparing the module, two languages that use {name} and {user} for the same key produce a contract error. Plural categories can differ between languages. Older registries with keys declared as true keep their open contract; newly generated registries include the parameters.

The types I18n, I18nMessageKey, I18nMessageTags<'items'>, I18nMessageValues<'items'>, I18nRichPart, I18nTranslateOptions and I18nValue are available through import type from @meekojs/i18n. Without a generated registry, createI18n retains an open API and runtime validation.

A dynamic key with a union type needs valid parameters for all its alternatives. If one requires values, you cannot omit them even if another is a static message. You can narrow the key with an if to pass only the parameters for that case.

Messages with markup#

To place an element or a component inside a sentence, write the tag in the message and render it with <I18nT>, which asks for one snippet per tag. The whole sentence stays in the catalog, so each language orders its own words:

json
{ "built": "Made by {name} with <code>.meeko</code>. Read <a>the guide</a>." }
meeko
<I18nT key="built" bind:values="{ name: author.value }">
  <snippet name="code" params="text"><code>{text}</code></snippet>
  <snippet name="a" params="text"><UILink to="/guide">{text}</UILink></snippet>
</I18nT>

Each <name>…</name> or <name/> is a snippet that receives its text, with the parameters already replaced, and it belongs to the component that writes it: it reads that component's signals and events and keeps its <style scoped>. <I18nT> adds no elements around the sentence; a tag without a snippet shows its text. key and values are typed as in t(), and locale asks for the message in another language. It works in SSR, prerender and hydration; changing values updates the text without recreating the snippets' nodes, and a change of plural form changes the runs of the sentence.

Security. innerHTML is not used. Only the catalog's text forms tags, before the parameters are replaced: a value with <img onerror=…> renders as text. Tags take no attributes: the snippet decides the href and the classes, not the translator.

meeko check requires the snippets of that message's tags and rejects any others (I18nMessageTags<K>). When preparing the module, two languages with different tags for the same key, or a tag inside another, are an error. What does not form a pair (Array<T>, <a href=x>) is text. t() returns the text as written, tags included; i18n.rich(key, values?, options?) returns the runs, { text, tag? }, to render them yourself.

Text in another language#

Each document loads only the catalog of its URL's language. For text written in another language, such as a suggestion in the suggested language, declare a small namespace in multilingual, which loads in every language:

ts
i18n: {
  defaultLocale: 'es',
  catalogs: {
    es: { common: 'i18n/es/common.json', suggestion: 'i18n/es/suggestion.json' },
    en: { common: 'i18n/en/common.json', suggestion: 'i18n/en/suggestion.json' },
  },
  multilingual: ['suggestion'],
}

t('suggestion.go', {}, { locale: 'en' }) is synchronous and uses that language's plural rules, the same in SSR and hydration. A key from another namespace is treated as a message that is not loaded. Each name must exist in every language and cannot be common. Each namespace adds one chunk per language to every document.

The module includes no suggestion component: multilingual also solves other text in another language, and each site keeps its own design. The recipe is in Suggesting the language without redirecting.

In <script server>#

useI18n() does not exist in <script server>, which also runs in a navigation's data request, without an application or plugins. usePageI18n(), auto-imported in server code, gives the URL's language and the translator:

meeko
<script server lang="ts">
  const { params } = usePage()
  const { locale, t } = await usePageI18n()
  export const doc = await readDoc(params.slug, locale)
  export const head = { title: t('docs.title', { name: doc.title }) }
</script>

locale is typed with the catalogs' languages (I18nLocale), and all namespaces of all languages are loaded, since on the server they weigh on no bundle. Like usePage(), it only works in the top-level code of a page or a server route middleware: elsewhere it gives MEEK073. It is imported from @meekojs/i18n/server, along with the types PageI18n and I18nLocale.

Formatting and switching languages#

useI18n() exposes locale, defaultLocale, locales, dir, t, rich, has, number, date, localePath, switchLocalePath, pageLocales, localeName, directionOf, suggestedLocale, rememberLocale and switchLocale. Intl formatters are reused by options. Specify timeZone when formatting dates that must match between SSR and the browser. switchLocale saves the language in the cookie and navigates to the equivalent route, with its search and fragment, in a new document.

Localized routes#

With strategy: 'prefix_except_default' (the default), the default language has no prefix and every other language lives under its own: /guide and /en/guide. The module generates each language's routes from existing pages with meeko.extendRoutes: they share the file, layouts, middleware and route options (render, prerenderEntries, sitemapEntries), so SSR, hydration, prerendering, the sitemap and route types treat them like any other route. /es/guide does not exist.

ts
i18n: {
  defaultLocale: 'es',
  catalogs: { es: { common: 'app/locales/es/common.json' }, en: { common: 'app/locales/en/common.json' } },
  exclude: ['/admin/*'],              // no other language versions or hreflang
  partial: { ar: ['/playground'] },   // a partially translated language only has these routes
  cookie: { name: 'meeko-locale', domain: 'example.com' }, // or false
  hreflang: true,                     // <link rel="alternate" hreflang> and x-default
  ogLocale: true,                     // og:locale and og:locale:alternate; or { es: 'es_MX' }, or false
  sitemap: true,                      // xhtml:link alternatives in sitemap.xml
}

A project page at another language's URL, such as app/pages/en/legal.meeko, replaces the generated route; this is how you write a page's prose in each language. Routes whose first segment is already a language or [locale] are not localized again. The namespaces in routes are declared with the unprefixed ID and also load in its /en/… versions.

Link with <MeekoLink to="/guide"> (or <UILink to>): with Meeko 1.8, the module registers a link resolver that adds the document language's prefix and preserves typed to, aria-current and client navigation; hreflang="es" forces another language and bind:resolve="false" leaves the route unchanged. localePath('/guide') is for <a>, code and URLs with a fragment. A language selector uses real links to switchLocalePath(code) for each language in pageLocales(), with localeName(code) as text; following them saves the choice in the cookie through the module.

hreflang, sitemap and language suggestion#

Each localized page has <html lang dir>, a <link rel="alternate" hreflang> for each language with a version of the page, and x-default pointing to the default language, with absolute URLs based on site (or MEEKO_PUBLIC_ORIGIN at build time; without either, the request origin). With sitemap: true, Meeko's sitemap.xml lists each version with its xhtml:link, through the sitemap:resolved hook. An application that serves the sitemap from a database writes it with sitemapXml.

It also emits og:locale and one og:locale:alternate for each other version of the page, with the likeliest region of each language (es_ES, en_US). ogLocale: { es: 'es_MX' } chooses it and ogLocale: false turns them off; an og:locale from the page or the layout wins.

A cookie with a domain is shared between subdomains. On a host outside that domain (localhost, a preview), where the browser would reject it, it is written without Domain, so it needs no per-environment configuration.

The module never redirects based on Accept-Language or the cookie: each URL has a single content representation that search engines index and caches share. suggestedLocale() returns, only in the browser, the language from the cookie or navigator.languages when it differs from the document's; call it in onMounted to show a notice, and rememberLocale(locale) when dismissing it. negotiateLocale(header, locales) and readLocaleCookie(cookies, name, locales) are available for the server.

Catalogs from a linked package (for example, a shared Shell in node_modules/meeko-docs/locales/…) are accepted even when their real path is outside the project; a path explicitly written outside the project is rejected.

Validation#

sh
pnpm -F @meekojs/i18n build
pnpm -F @meekojs/i18n check
pnpm -F @meekojs/i18n test

Tests include text, select and plural contracts, TypeScript consumers and .meeko components, an SSR build, type removal when disabling the module, routes, cookies and language negotiation, <I18nT> in SSR, hydration and updates with hostile values, and static builds with /en/… routes, hreflang, og:locale, usePageI18n, lazy, global (also on the error page) and multilingual namespaces, unloaded keys and a localized sitemap.

Integration guide#

It needs Meeko 1.8 (meeko.extendRoutes and the sitemap:resolved hook). For a dynamic sitemap (sitemap: false and sitemapXml), localeAlternates(pathname, routing, available?) gives the alternatives of each URL.

This guide is the recipe for localizing a Meeko site with Spanish without a prefix (the usual URLs) and English under /en/….

1. Declare the languages and the strategy#

ts
// meeko.config.ts
import { defineMeekoConfig } from '@meekojs/meeko/framework'

export default defineMeekoConfig({
	// Public origin: absolute hreflang and sitemap URLs (or MEEKO_PUBLIC_ORIGIN at build time).
	site: 'https://meekojs.com',
	sitemap: true,
	modules: ['@meekojs/i18n'],
	i18n: {
		defaultLocale: 'es',
		// By default: es without a prefix and en with /en/…
		strategy: 'prefix_except_default',
		catalogs: {
			es: { common: 'app/locales/es/common.json', docs: 'app/locales/es/docs.json' },
			en: { common: 'app/locales/en/common.json', docs: 'app/locales/en/docs.json' },
		},
		// Namespaces loaded lazily on a route (ID without a language prefix).
		routes: { '/docs/[...slug]': ['docs'] },
		// Routes without other language versions or hreflang.
		exclude: ['/admin/*'],
		// Selected language cookie, shared across subdomains (for suggestions only).
		cookie: { name: 'meeko-locale', domain: 'meekojs.com' },
	},
})

The module is registered by its name and its options go under the i18n key, typed without importing anything. With this configuration:

  • Each existing page has its English version without duplicating files: app/pages/guide.meeko serves /guide and /en/guide; app/pages/docs/[...slug].meeko, /docs/… and /en/docs/…. The generated routes share file, layouts, middleware, render, prerenderEntries and sitemapEntries; pages from layers and modules are localized too.
  • /es/guide does not exist (404): the default language never has a prefix.
  • <html lang dir> follows the language of the URL and each localized page declares <link rel="alternate" hreflang> per language plus x-default (the default language).
  • sitemap.xml lists each version with its xhtml:link alternatives.
  • partial: { ar: ['/playground'] } limits a partially translated language to those routes.

2. Extract the texts to catalogs#

  • One JSON file per language and namespace: app/locales/<idioma>/<namespace>.json. All languages declare the same namespaces, the same keys and the same parameters; otherwise the module fails during preparation.
  • common is loaded on every route: navigation, header, footer, buttons, accessibility, empty states and errors. The namespaces in global (global: ['shell']) are always loaded along with common: on every route, their /en/… versions and the error page, in SSR, prerendering, hydration and client navigation. The other namespaces are loaded only on the routes in routes.
  • An unloaded key (from a namespace the route does not load) shows the key itself instead of breaking the page, and warns once per language and key on the server (log, prerender) and, with meeko dev, also in the browser console. Throwing an error would bring down the whole document, including the error page. If [meeko:i18n] Missing message … appears, add its namespace to global or to the route in routes.
  • Keys are global (they are not repeated across namespaces), so they start with a prefix for their area: nav.*, actions.*, a11y.*, footer.*, search.* in common; docs.* in docs; dashboard.* in dashboard. Nested JSON is flattened with dots: { "nav": { "guide": "Guía" } } is nav.guide.
  • Parameters with {name}; plurals and selectors with { "plural": "total", "forms": { "=0": …, "one": …, "other": … } } and { "select": "role", "forms": { …, "other": … } }.
  • meeko check checks the keys and parameters of each t().
  • A shared Shell or layer keeps its catalogs in its package, and the application declares them as one more namespace, with the path inside node_modules, and adds it to global so its texts are on every page, including the error page. A path written outside the project is rejected; a package linked by pnpm is accepted.
ts
i18n: {
	defaultLocale: 'es',
	catalogs: {
		es: { common: 'app/locales/es/common.json', shell: 'node_modules/meeko-docs/locales/es/shell.json' },
		en: { common: 'app/locales/en/common.json', shell: 'node_modules/meeko-docs/locales/en/shell.json' },
	},
	// Each name exists in every language in catalogs and does not repeat common.
	global: ['shell'],
}
vue
<script lang="ts">
	const { t } = useI18n()
</script>

<template>
	<button type="button">{t('actions.copy')}</button>
	<p>{t('search.results', { total: 3 })}</p>
</template>

useI18n is auto-imported in components (import { useI18n } from '@meekojs/i18n' in .ts files).

Messages with markup#

To interleave an element or a component, write the tag in the message and render the message with <I18nT>. The sentence stays whole in the catalog, so each language orders its words:

json
{ "footer": { "built": "Hecho con <code>.meeko</code> por {name}. Lee <a>la guía</a>." } }
json
{ "footer": { "built": "Made by {name} with <code>.meeko</code>. Read <a>the guide</a>." } }
vue
<template>
	<p>
		<I18nT
			key="footer.built"
			bind:values="{ name: author.value }"
		>
			<snippet
				name="code"
				params="text"
			>
				<code>{text}</code>
			</snippet>
			<snippet
				name="a"
				params="text"
			>
				<UILink to="/guide">{text}</UILink>
			</snippet>
		</I18nT>
	</p>
</template>
  • Each <name>…</name> tag (or <name/>, without text) is a snippet that receives its text, with the parameters already substituted. It belongs to your component: it reads its signals and events and keeps its <style scoped>. <I18nT> adds no elements around it, and a tag without a snippet shows its text.
  • It is safe. innerHTML is not used. Only the catalog's text forms tags: a value with <img onerror=…> is rendered as text. Tags accept no attributes: the snippet decides the href and the classes, not the translator.
  • meeko check requires the snippets for that message's tags and rejects those it does not have. All languages must use the same tags and a tag cannot contain another; otherwise, the module fails during preparation.
  • t() returns the text as it is, with the tags. What does not form a pair (Array<T>, <a href=x>) remains text in <I18nT> too.
  • i18n.rich(key, values?) returns the pieces ({ text, tag? }) if you render it yourself.

It works in SSR, prerendering and hydration, and changing values updates the text without creating the snippets' nodes again.

3. Localize the prose of .meeko pages#

Choose by type of content:

  1. Interface and short texts (section titles, labels, DocsPage descriptions): catalog keys with t().
  2. A handwritten prose page: one file per language. app/pages/en/legal.meeko replaces the /en/legal route the module would generate from app/pages/legal.meeko; the other pages keep reusing the Spanish file.
  3. Documentation generated from Markdown or a database: the page's <script server> reads the language with usePageI18n() and asks for the content in that language (falling back to the default language if the translation is missing):
vue
<script server lang="ts">
	const { params } = usePage()
	const { locale, t } = await usePageI18n()
	export const doc = await readDoc(params.slug, locale)
	export const head = { title: t('docs.title', { name: doc.title }) }
</script>

useI18n() does not exist there: that block also runs in the data request of a navigation, with no application or plugins. usePageI18n() is auto-imported in server code (@meekojs/i18n/server), takes the language from the URL, types it with the languages of the catalogs and has all the namespaces loaded. Like usePage(), it only works in the top-level code of a page.

Without a translation, an /en/… route shows the same content as the Spanish one; use exclude or partial to avoid publishing versions that do not exist yet.

Write internal links with <MeekoLink to> (or <UILink to> / <UIButton to>) and the route without a prefix: the module registers a link resolver (app.resolveLink, Meeko 1.8) that adds the prefix of the document's language in SSR, prerendering, hydration and client navigation.

vue
<script lang="ts">
	const i18n = useI18n()
</script>

<template>
	<!-- /guide in Spanish, /en/guide in English: typed `to`, aria-current and client navigation -->
	<MeekoLink to="/guide">{i18n.t('nav.guide')}</MeekoLink>
	<MeekoLink
		to="/docs/[...slug]"
		bind:params="{ slug: 'intro' }"
		bind:search="{ tab: 'setup' }"
	>
		Intro
	</MeekoLink>
	<UILink to="/installation">{i18n.t('actions.start')}</UILink>
	<!-- Another language: hreflang selects the version (and remains on the <a>) -->
	<MeekoLink
		to="/guide"
		hreflang="es"
	>
		Spanish version
	</MeekoLink>
	<!-- Without a prefix, unchanged -->
	<MeekoLink
		to="/guide"
		bind:resolve="false"
	>
		Always /guide
	</MeekoLink>
	<!-- With a fragment or a path outside the application: localePath -->
	<a bind:href="i18n.localePath('/docs/intro#setup')">Intro</a>
</template>
  • Resolver rules. to="/guide" gives /guide in the default language and /en/guide in English. hreflang chooses a configured language (exact or by base language: en-US is en); an unknown one is ignored. The href stays as href() builds it when to already names a language (/en/legal, /[locale]/…), when the route is in exclude and when the target language has no version of it (partial): it links to the default version. bind:resolve="false" disables it on a link.
  • Active state. aria-current compares the resolved href with the route, so <MeekoLink to="/guide"> is active on /en/guide. Outside <MeekoLink>, compare routes without a prefix: stripLocale(route.path.value, i18n.locales) (under /en, the active route ID is /en/guide).
  • A link in the same language is a client navigation; one to another language (for example with hreflang), a full navigation that saves the language cookie.
  • localePath(path, locale?) remains available for <a>, URLs with a fragment and code: it keeps the search and the fragment.

5. Language switcher and hreflang#

vue
<script lang="ts">
	const i18n = useI18n()
</script>

<template>
	<nav aria-label="Idioma">
		<a
			each:of="i18n.pageLocales()"
			each:as="code"
			each:key="code"
			bind:href="i18n.switchLocalePath(code)"
			bind:hreflang="code"
			bind:lang="code"
			bind:aria-current="code === i18n.locale ? 'page' : undefined"
		>
			{i18n.localeName(code)}
		</a>
	</nav>
</template>
  • switchLocalePath(code) is the equivalent route of the current page (with search and fragment) and pageLocales() the languages that have a version of it. Use real links: they work without JavaScript and search engines follow them. When they are followed, the module saves the choice in the cookie.
  • hreflang, x-default, lang and dir need no code: the module adds them (hreflang: false disables it). Declare site so the URLs are absolute and stable.
  • og:locale and one og:locale:alternate for each other version of the page, too. They use the most likely region of each language (es_ES, en_US); ogLocale: { es: 'es_MX' } chooses it and ogLocale: false disables them. An og:locale of the page or layout wins.
  • The cookie across subdomains. With cookie: { name: 'meeko-locale', domain: 'meekojs.com' }, subdomains share the choice. On a host that does not belong to that domain (localhost, a preview) the cookie is written without Domain, so there is no need to configure it per environment.

6. Suggesting the language without redirecting#

URLs without a prefix always serve the default language: the module never redirects based on Accept-Language or the cookie. A redirect would mean search engines (which usually crawl without Accept-Language or in English) never see one of the versions, would break the hreflang ↔ URL correspondence and would force the cache to vary by header. Instead, show a suggestion, written in the language it offers. Only the catalog of the document's language is loaded, so declare its texts as a small multilingual namespace, which is loaded in all languages:

ts
i18n: {
	defaultLocale: 'es',
	catalogs: {
		es: { common: 'app/locales/es/common.json', suggestion: 'app/locales/es/suggestion.json' },
		en: { common: 'app/locales/en/common.json', suggestion: 'app/locales/en/suggestion.json' },
	},
	multilingual: ['suggestion'],
}
vue
<script lang="ts">
	const i18n = useI18n()
	const suggestion = signal<string | undefined>(undefined)
	onMounted(() => {
		suggestion.value = i18n.suggestedLocale()
	})
</script>

<template>
	<aside
		if:when="suggestion.value"
		bind:lang="suggestion.value"
	>
		<p role="status">{i18n.t('suggestion.message', {}, { locale: suggestion.value! })}</p>
		<a bind:href="i18n.switchLocalePath(suggestion.value!)">{i18n.t('suggestion.go', {}, { locale: suggestion.value! })}</a>
		<button
			type="button"
			on:click="() => { i18n.rememberLocale(i18n.locale); suggestion.value = undefined }"
		>
			{i18n.t('suggestion.stay')}
		</button>
	</aside>
</template>

t(key, values, { locale }) is synchronous and uses that language's plural rules, the same in SSR and hydration. It only knows the multilingual namespaces: another key is treated as an unloaded message. Each multilingual namespace adds one chunk per language to each document, so it must be small. Make the notice float (position: fixed) so its arrival after hydration does not shift the page.

suggestedLocale() reads the cookie and, without it, navigator.languages; on the server it returns undefined, which is why it is called in onMounted (the SSR HTML and hydration match). On the server, negotiateLocale(request.headers.get('accept-language'), locales) and readLocaleCookie(...) serve for other decisions, never for changing the content of a URL.

API#

useI18n() returns:

Member Description
locale, defaultLocale, locales, dir Language of the document, language without a prefix, configured languages and direction.
t(key, values?, { locale }?) Message with a key and parameters typed from the catalogs; in another language, with multilingual.
rich(key, values?, { locale }?) The message split by its tags ({ text, tag? }), the basis of <I18nT>.
has(key, { locale }?) Whether the key has a message in the loaded catalogs, for a key that comes from data.
number(value, options?), date(value, options?) Intl.NumberFormat and Intl.DateTimeFormat of the language, cached by options.
localePath(path, locale?) Internal route in a language (by default, the current one).
switchLocalePath(locale) Equivalent route of the current page in another language. Reactive.
pageLocales() Languages with a version of the current page. Reactive.
localeName(locale, displayLocale?) Name of the language (English, Español), by default in that language.
directionOf(locale) ltr or rtl.
suggestedLocale() Language preferred by the reader if it is not the current one; only in the browser.
rememberLocale(locale) Saves the language in the cookie.
switchLocale(locale) Saves the language and navigates to the equivalent route (new document).

<I18nT key values? locale?> renders a message with markup, with one snippet per tag. usePageI18n(), from @meekojs/i18n/server, gives the language and the translator in a <script server>. Both are auto-imported.

Exports of the root: useI18n, createI18n, i18nContext, localeOf, localizedPath, stripLocale, negotiateLocale, readLocaleCookie and the types I18n, I18nCookie, I18nDirection, I18nMessageKey, I18nMessageTags, I18nMessageValues, I18nRichPart, I18nTranslateOptions, I18nValue, CreateI18nOptions. From @meekojs/i18n/server: usePageI18n and the types PageI18n and I18nLocale. '@meekojs/i18n' in modules resolves the subpath @meekojs/i18n/module.

Module options: defaultLocale, catalogs, global, multilingual, routes, directions, strategy, exclude, partial, cookie, hreflang, ogLocale and sitemap (see the previous sections).

SSR, prerendering and Cloudflare#

The language comes from the URL, so SSR, hydration, prerendering and the static adapter produce the same HTML for each URL without reading headers. The catalogs are compiled at build time and loaded by language and namespace with import(); the runtime only uses Intl and works the same in Node, Workers, Bun and Deno.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/i18n.md