Skip to content

Languages and routes

Default language without a prefix, generated /en/… routes, localized links, switcher, hreflang and sitemap.

Default language without a prefix

With strategy: 'prefix_except_default', the default option, the URLs of the default language do not change and every other language lives under its prefix. The module generates the /en/… routes from the existing pages, with the same file, layouts and options: there is no need to duplicate pages.

meeko.config.ts ts
i18n: {
  defaultLocale: 'es',
  catalogs: { es: { common: 'app/locales/es/common.json' }, en: { common: 'app/locales/en/common.json' } },
  // By default: es without a prefix and en under /en/…
  strategy: 'prefix_except_default',
  // No /en/… versions and no hreflang:
  exclude: ['/admin/*'],
  // A language with a partial translation only has these routes:
  partial: { ar: ['/playground'] },
}

// app/pages/guide.meeko          -> /guide and /en/guide
// app/pages/docs/[...slug].meeko -> /docs/… and /en/docs/…
// app/pages/en/legal.meeko       -> replaces the generated /en/legal

A project page at the URL of another language, such as app/pages/en/legal.meeko, replaces the generated route: that is how the prose of a page is written in each language. exclude leaves a route without versions and partial limits the ones of a language with a partial translation. A URL with the prefix of the default language, such as /es/guide, does not exist.

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

<template>
  <!-- /guide in Spanish, /en/guide in English -->
  <a bind:href="i18n.localePath('/guide')">{i18n.t('nav.guide')}</a>
  <UILink bind:href="i18n.localePath('/docs/intro#setup')">Intro</UILink>
</template>

localePath adds the prefix of the document's language, or of the language it receives, and keeps the search and the fragment. Use it in the internal links of the interface.

Language switcher

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

<template>
  <nav aria-label="Language">
    <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>

<!-- /playground?tab=demo#messages -> /en/playground?tab=demo#messages -->

switchLocalePath returns the equivalent route of the current page in another language and pageLocales the languages that have a version of it. Use real links: they work without JavaScript and search engines follow them. Changing the language is a full navigation that stores the choice in a cookie; switchLocale does the same from a script.

hreflang and sitemap

html
<!-- /en/guide, with site: 'https://example.com' -->
<html lang="en" dir="ltr">
<link rel="alternate" href="https://example.com/guide" hreflang="es">
<link rel="alternate" href="https://example.com/en/guide" hreflang="en">
<link rel="alternate" href="https://example.com/guide" hreflang="x-default">

<!-- sitemap.xml (sitemap: true) -->
<url>
  <loc>https://example.com/en/guide</loc>
  <xhtml:link rel="alternate" hreflang="es" href="https://example.com/guide"/>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/guide"/>
  <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/guide"/>
</url>

Each localized page declares lang, dir and one link rel="alternate" per language plus x-default, with absolute URLs based on site. With sitemap: true, Meeko's sitemap.xml lists each version with its alternates.

Suggest the language, without redirecting

meeko
<script lang="ts">
  const i18n = useI18n()
  const suggestion = signal<string | undefined>(undefined)
  // Only in the browser, after hydrating: it never redirects.
  onMounted(() => { suggestion.value = i18n.suggestedLocale() })
</script>

<template>
  <p if:when="suggestion.value" role="status">
    <a bind:href="i18n.switchLocalePath(suggestion.value!)">
      {i18n.localeName(suggestion.value!)}
    </a>
    <button type="button" on:click="() => { i18n.rememberLocale(i18n.locale); suggestion.value = undefined }">
      {i18n.localeName(i18n.locale)}
    </button>
  </p>
</template>

URLs without a prefix always show the default language: the module never redirects based on Accept-Language or the cookie. That way each URL has a single content that search engines can index and caches can share. suggestedLocale proposes, only in the browser, the language of the cookie or of navigator.languages so that you can show a notice.

URL utilities

Explicit imports ts
import { localeOf, localizedPath, negotiateLocale, stripLocale } from '@meekojs/i18n'

const locales = ['es', 'en', 'ar']
localeOf('/en/playground', locales, 'es') // 'en'
localeOf('/playground', locales, 'es')    // 'es'
localizedPath('/playground?q=1#form', 'en', locales, 'es')
// '/en/playground?q=1#form'
localizedPath('/en/playground', 'es', locales, 'es') // '/playground'
stripLocale('/en/playground', locales)               // '/playground'
negotiateLocale('en-US,en;q=0.9,es;q=0.8', locales)  // 'en'

localeOf returns the language of the first segment or the fallback it receives. localizedPath replaces a known prefix and omits the one of the default language; negotiateLocale chooses a configured language from an Accept-Language header or from a list of preferences.

Try a switch with a query and a fragment

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