Multi-language Support
Part 1 — For business and content users
You define languages in Cockpit → Translations using BCP 47 codes (e.g. de-CH, fr-FR, en-US), then assign them to markets via domains. Each domain serves one language, so www.shop.ch can show German while www.shop.ch/fr shows French.
Page paths and content can be localized per language (e.g. /produkte for German, /produits for French). Languages support fallback chains: de-CH (Swiss German) can fall back to de (German), so you only need to translate what actually differs.
Configuration is centralized in Cockpit (Translations and Markets). No code changes needed to add languages or domains.
Part 2 — For developers
Key types
RcLanguage:id,code(BCP 47),name,fallbacks(ordered list of language codes for content resolution).RenderLanguage: derived fromRcLanguagebybuildI18nConfig(); addslanguageCode,regionCode,direction(ltr/rtl),endonym,measurementSystem(metric/imperial),localeChain([code, ...fallbacks]), andmarketDomains.LocalizedValue<T>: object keyed by language code (e.g.{ "de": "/produkte", "fr": "/produits" }).
Content resolution
unlocalize(value, language.localeChain) resolves a LocalizedValue<T> to a single value for a given RenderLanguage. It walks the chain and returns the first match, or undefined if no locale in the chain has a value. For example, with de-CH configured with fallbacks ['de-DE', 'de'], it tries de-CH → de-DE → de → undefined.
import { unlocalize } from '#imports'
const path = unlocalize(page.paths, language.value.localeChain)
// de-CH → de-DE → de → undefined
Integration with nuxt-i18n
nuxt-i18n is configured with strategy: 'no_prefix'. It does not own routing. Frontend Core generates routes with aliases from market config and resolves the active locale from domain + path. nuxt-i18n is told which locale is active via setLocale().
nuxt-i18n handles:
$t()/useI18n()for UI string translations$n()/$d()for number and date formatting- Locale message lazy loading
Frontend Core handles:
- Route generation (aliases from market config)
- Domain → market → language resolution
- Link resolution (
linkResolver) - SEO head — canonical, hreflang alternates,
og:locale, and<html lang>(emitted automatically) - Market/language/domain composables
useLocalePath() and prefix-based routing do not apply. Use linkResolver.switchLocalePath() and linkResolver.switchMarketUrl() instead.Locale-aware formatting
The UI Kit registers global formatters that respect the active locale:
| Formatter | Input | Output example |
|---|---|---|
$money(money) | { amount, currency } | CHF 149.00 / 149,00 € |
$timespan(timespan) | { min?, max? } (Date) | 1. – 15. März 2025 |
$measurement(measurement) | { value, unit } | 100 cm / 10 m² |
$unitPrice(unitPrice) | { price, quantity, reference } | 13,99 € / 100 ml |
$duration(duration) | { duration } (ISO 8601) | 1h 30m |
$count(count) | number | 1.2K / 1234 |
Available in any template: {{ $money(price) }}. See Currencies for the developer-facing summary, and the Money reference for the full $money signature and options.
$count
$count formats a plain count — likes, views, comments — compactly for the active locale, and takes an optional Intl.NumberFormatOptions to override the defaults ($count(12345, { notation: 'standard' })).
Where it abbreviates is CLDR's decision, not ours, so it differs by language. English shortens from a thousand up; German only from a million:
| Input | English | German |
|---|---|---|
1234 | 1.2K | 1234 |
1234567 | 1.2M | 1,2 Mio. |
A design that budgets a fixed width for a count badge has to allow for the long form.
Language switcher
Frontend Core provides linkResolver (auto-imported) for building language and market switchers. You can override the resolution logic with Link Resolver hooks.
linkResolver.switchLocalePath() switches language within the same market:
<template>
<nav>
<NuxtLink
v-for="domain in market.domains"
:key="domain.id"
:to="linkResolver.switchLocalePath(domain.language.id)"
>
{{ domain.language.endonym }}
</NuxtLink>
</nav>
</template>
<script setup lang="ts">
const market = useMarket()
</script>
Returns '#' if the target language is not available in the current market.
linkResolver.switchMarketUrl() switches to a different market (full URL, requires full page load because the host may differ):
// Navigate to Germany market, default language
navigateTo(linkResolver.switchMarketUrl('mkt_germany'), { external: true })
// Navigate to Germany market, French
navigateTo(linkResolver.switchMarketUrl('mkt_germany', 'lng_fr'), { external: true })
Both resolve the correct localized path for the current page, including dynamic params. On a page type whose slug is translated per locale, see translated slugs for how the switch target picks up the target locale's own slug.
Link resolution
linkResolver.resolve(link) converts a Link object into a path string for the active language and market. useResolvedLink(link) is the reactive wrapper for use outside templates and computed(). Both are auto-imported.
See the Link reference page for the full API, reactivity caveats, and how to override resolution with hooks.
SEO: hreflang and canonical
Frontend Core emits SEO head tags automatically on every page — you don't need to call anything. On each render it adds to <head>:
<link rel="canonical">— the current page's URL on the current domain (https://{host}{prefix}{path})<link rel="alternate" hreflang="...">— one for every domain across all markets where the current page has a localized path. Thehreflangvalue is the domain's language code.<link rel="alternate" hreflang="x-default">— points to the first market's default domain as the fallback for search enginesog:localeandog:locale:alternate— the current and alternate locales<html lang>— the current domain's language code
All URLs are built as https://{domain.host}{domain.path}{localizedPagePath}, with dynamic route params (e.g. :slug) filled from the current route. Domains where the page has no localized path are silently skipped — no broken alternate links are generated.
Translated slugs
@laioutr-core/frontend-coreA German product at /produkte/handtuch-blau is the same product as the French /produits/serviette-bleue. Filling every alternate from the current route's params would advertise /produits/handtuch-blau, a URL that does not exist.
Where the page type's page index implements locate and reports a complete per-locale map, each alternate is filled with its own locale's params instead, and locales the page does not exist in are dropped from the alternates rather than guessed at. linkResolver.switchLocalePath() uses the same map, so switching language lands on the translated slug.
Three things follow from that:
- The lookup only runs where it can change anything: a project with more than one domain language, on a page type that resolves from a connector entity, on a route that has params. Single-language projects, static pages, and the 404 catch-all perform no lookup.
- It is bounded by a 2 second SSR budget. If the budget is breached, that render falls back to filling every alternate from the current locale's params while the request finishes and warms the server cache, so the next render is correct.
- Omission follows only from a complete map. A connector that resolves just the locale it was called in omits the map, and its pages keep filling every alternate from the current params. A partial map cannot tell "no page in this locale" apart from "did not look", so nothing is dropped on its word.
Page types whose connector provides no locate are unchanged. The Shopify and Shopware product detail pages are in that group today: they resolve the product but not its slug in every language.
Market-scoped pages
Alternates are automatically restricted to the markets a page belongs to. A page with a marketIds constraint only advertises alternates (and og:locale:alternate) for those markets — it never points to a market where the page doesn't exist and would 404.
Customizing the head
A single filter hook, frontend-core:page-head:resolve, lets you add, override, or remove head tags while seeing Frontend Core's computed values. Its result.value has two slots:
seo— the SEO meta (title,description,robots, and anyog:/twitter:field)locale— the locale head (canonical, hreflang alternates,og:locale,<html lang>)
// e.g. in a Nuxt plugin
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('frontend-core:page-head:resolve', ({ result }) => {
result.value.seo.title = `${result.value.seo.title} — Acme`
result.value.seo.ogImage = 'https://example.com/og.png'
// e.g. drop the x-default alternate
result.value.locale.link = result.value.locale.link.filter((l) => l.hreflang !== 'x-default')
})
})
Media and Media Library
Laioutr’s media library abstraction lets business users choose assets from connected backends visually in Cockpit. Implement a media library connector for your asset system so editors can browse, filter, and (optionally) upload media in Studio.
Multi-market
Laioutr's multi-market support lets you serve different regions (markets) from one project, each with its own domains, languages, and currency, configured in Cockpit.