Most products that hire senior frontend engineers in Europe ship in more than one language, so i18n is a practical interview topic: how routes work per locale, how you avoid shipping every language to every user, how search engines learn about the alternatives, and how formatting is done without hand-rolled helpers. In Nuxt the answer is @nuxtjs/i18n on top of Vue I18n, plus the Intl APIs the browser already has.
@nuxtjs/i18n wraps Vue I18n for Nuxt: it generates localised routes, detects and persists the locale, lazy-loads message files, and exposes useI18n() (t, locale, locales, n, d), useLocalePath(), useSwitchLocalePath(), useLocaleRoute(), <NuxtLinkLocale> and useLocaleHead(). Nuxt 4 projects use the v10 line.prefix_except_default (default: /about, /de/about), prefix (every locale prefixed, root redirects), prefix_and_default (both forms for the default locale), no_prefix (locale from detection only, no URL signal). The choice is an SEO decision: prefixed URLs give each language a stable, indexable address.code, language (a BCP 47 tag such as en-GB, used for hreflang and <html lang>), file or files, dir for RTL, and optional domain for differentDomains.i18n/locales/*.json (or .ts for dynamic messages) and only the active locale's file ships, loaded on demand when the user switches. The alternative, bundling every language, is a bundle-size bug that grows with each locale.detectBrowserLanguage reads Accept-Language on the server and a cookie (i18n_redirected) afterwards; redirectOn: 'root' limits redirects to /. The cookie is what keeps the server render and the client agreeing on the locale.useLocaleHead({ dir: true, lang: true, seo: true }) produces <html lang dir>, hreflang alternates including x-default, canonical, and og:locale/og:locale:alternate. Wire it in app.vue or the layout once.Intl's job. n(value, 'currency') and d(date, 'short') use Intl.NumberFormat and Intl.DateTimeFormat with per-locale formats you define once; Intl.RelativeTimeFormat, Intl.ListFormat, Intl.PluralRules and Intl.DisplayNames cover the rest. Pluralisation in messages uses Vue I18n's | syntax or ICU-style named counts.dir on the locale, render it on <html>, and write CSS with logical properties (margin-inline-start, padding-block, inset-inline) so layouts flip without duplicated rules.checkout.summary.total), the default locale is the fallback (fallbackLocale), missing keys warn in development (missingWarn), and extraction and translation management happen outside the app (a TMS, or JSON files reviewed in PRs). Content pages translate through per-locale content directories rather than message files.Intl formatters or your own message lookup.Configuration with lazy files, detection and formats:
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
strategy: 'prefix_except_default',
defaultLocale: 'en',
locales: [
{ code: 'en', language: 'en-GB', name: 'English', file: 'en.json' },
{ code: 'cs', language: 'cs-CZ', name: 'Čeština', file: 'cs.json' },
{ code: 'ar', language: 'ar-EG', name: 'العربية', file: 'ar.json', dir: 'rtl' },
],
detectBrowserLanguage: { useCookie: true, cookieKey: 'i18n_redirected', redirectOn: 'root' },
baseUrl: 'https://shop.example.com', // absolute hreflang and canonical URLs
},
})
export default defineI18nConfig(() => ({
legacy: false,
fallbackLocale: 'en',
numberFormats: {
'en': { currency: { style: 'currency', currency: 'EUR' } },
'cs': { currency: { style: 'currency', currency: 'CZK' } },
'ar': { currency: { style: 'currency', currency: 'EGP' } },
},
datetimeFormats: {
'en': { short: { day: 'numeric', month: 'short', year: 'numeric' } },
'cs': { short: { day: 'numeric', month: 'numeric', year: 'numeric' } },
'ar': { short: { day: 'numeric', month: 'long', year: 'numeric' } },
},
}))
Head tags once, links and formatting in a component:
<script setup lang="ts">
const head = useLocaleHead({ dir: true, lang: true, seo: true })
useHead(head) // <html lang dir>, hreflang alternates, canonical, og:locale
</script>
<script setup lang="ts">
const { t, n, d, locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
defineProps<{ total: number, updatedAt: Date, items: number }>()
</script>
<template>
<p>{{ t('cart.items', items) }}</p> <!-- "1 item | {count} items" -->
<p>{{ t('cart.total', { amount: n(total, 'currency') }) }}</p>
<p>{{ d(updatedAt, 'short') }}</p>
<NuxtLinkLocale to="/checkout">{{ t('cart.checkout') }}</NuxtLinkLocale>
<nav>
<NuxtLink v-for="l in locales" :key="l.code" :to="switchLocalePath(l.code)" :hreflang="l.language">
{{ l.name }}
</NuxtLink>
</nav>
</template>
{
"cart": {
"items": "no items | one item | {count} items",
"total": "Total: {amount}",
"checkout": "Go to checkout"
}
}
A formatter that does not depend on Vue, usable in a Nitro route:
export function formatMoney(amount: number, locale: string, currency: string) {
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
}
If the server picks the locale from Accept-Language but the client reads a different cookie, or a localStorage preference, the server renders German and the client hydrates English: a hydration mismatch plus a flash of wrong language. The locale has to come from one source available on both sides, which is the URL prefix or the cookie the module manages.
total.toFixed(2) + ' €' is wrong in half of Europe (decimal comma, symbol position, thousands separators). Intl.NumberFormat knows the rules for every locale and is built into every browser and Node; use it directly or through n().
The current major of @nuxtjs/i18n for Nuxt 4, the default locale-file directory (i18n/locales), the detectBrowserLanguage defaults and the status of the experimental server-side translation helpers.
useLocaleHead and view the server HTML: find lang, dir, the hreflang alternates and x-default."With @nuxtjs/i18n on top of Vue I18n. I choose a prefixed routing strategy so each language has its own indexable URLs, define locales with BCP 47 language tags and per-locale message files that load lazily, and let the module detect the language from Accept-Language on the first visit and persist it in a cookie so the server and the client always agree. useLocaleHead in the app root emits lang, dir, hreflang alternates and canonical tags. Formatting goes through Intl, either via the n and d helpers with formats defined once, or directly in shared utilities so server routes can use them. For right-to-left languages I write CSS with logical properties from the start."
Nothing yet. Notes filed here show up on this page and the section overview.
Rendering strategies
Universal rendering, SPA, prerendering, SWR and ISR per route, client-only and server components, islands, lazy hydration and streaming, and how to choose per route rather than per app.
Vue docs, chapter by chapter
How to use this section to learn the official Vue guide thoroughly enough to be quizzed on it, and how it relates to the senior-depth Vue section.