Nuxt Internationalization (i18n) — Complete Guide to @nuxtjs/i18n
In this tutorial, you will learn about Nuxt Internationalization (i18n). We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Nuxt i18n with @nuxtjs/i18n — configure multiple locales, manage translation files, implement locale switching, and optimize SEO for multilingual sites.
In this lesson, you'll understand how to add multi-language support to your Nuxt application so users can switch between languages with localized URLs, SEO tags, and translated content.
What You'll Learn
How to install and configure @nuxtjs/i18n, create translation files for multiple locales, implement automated locale detection and switching, generate localized routes, and optimize multilingual SEO with hreflang tags.
Why It Matters
Internationalization opens your application to global audiences. Multilingual sites rank better in local search results, increase user engagement by 40-60%, and are essential for reaching non-English-speaking markets.
Real-World Use
A global e-commerce platform supports 12 languages with Nuxt i18n, using locale-specific URLs, automated browser language detection, and SEO-optimized hreflang tags that increased organic traffic from non-English markets by 180%.
flowchart LR
A[User Browser] --> B{Detect Locale}
B -->|en| C[English Site]
B -->|es| D[Spanish Site]
B -->|fr| E[French Site]
C --> F[en/about]
D --> G[es/acerca-de]
E --> H[fr/a-propos]
F --> I[Localized SEO Tags]
G --> I
H --> I
I --> J[hreflang Tags]
style A fill:#00dc82,color:#fff
Installation
npm install @nuxtjs/i18n@next
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
{ code: 'es', iso: 'es-ES', name: 'Español', file: 'es.json' },
{ code: 'fr', iso: 'fr-FR', name: 'Français', file: 'fr.json' }
],
defaultLocale: 'en',
lazy: true,
langDir: 'locales/',
strategy: 'prefix_except_default'
}
});
Translation Files
Create JSON files for each locale in the locales/ directory:
// locales/en.json
{
"home": {
"title": "Welcome to our website",
"description": "Learn Nuxt with practical examples",
"cta": "Get Started"
},
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact",
"blog": "Blog"
},
"footer": {
"copyright": "All rights reserved.",
"language": "Language"
}
}
// locales/es.json
{
"home": {
"title": "Bienvenido a nuestro sitio web",
"description": "Aprende Nuxt con ejemplos prácticos",
"cta": "Comenzar"
},
"nav": {
"home": "Inicio",
"about": "Acerca de",
"contact": "Contacto",
"blog": "Blog"
},
"footer": {
"copyright": "Todos los derechos reservados.",
"language": "Idioma"
}
}
Using Translations in Templates
Access translations with the $t function or the useI18n composable:
<template>
<div>
<h1>{{ $t('home.title') }}</h1>
<p>{{ $t('home.description') }}</p>
<button>{{ $t('home.cta') }}</button>
</div>
</template>
Using the composable in script:
<script setup>
const { t, locale, setLocale } = useI18n();
const pageTitle = computed(() => t('home.title'));
const pageDescription = computed(() => t('home.description'));
</script>
<template>
<Head>
<Title>{{ pageTitle }}</Title>
<Meta name="description" :content="pageDescription" />
</Head>
<h1>{{ pageTitle }}</h1>
</template>
Expected output: The page renders content in the user's selected language, with localized SEO meta tags.
Locale Switching
Create a language switcher component:
<template>
<select
:value="locale"
@change="switchLocale($event.target.value)"
class="language-selector"
>
<option
v-for="loc in locales"
:key="loc.code"
:value="loc.code"
>
{{ loc.name }}
</option>
</select>
</template>
<script setup>
const { locale, locales, setLocale } = useI18n();
const router = useRouter();
async function switchLocale(newLocale) {
await setLocale(newLocale);
// Router updates the path automatically with prefix strategy
}
</script>
Expected output: A dropdown that switches the entire page to the selected language, updating the URL prefix and all translated content.
Localized Routes with Parameters
Dynamic routes with localized slugs:
<script setup>
const { t, locale } = useI18n();
const route = useRoute();
// Fetch localized page data based on slug
const { data: page } = await useAsyncData('page', () => {
return queryContent(`/blog/${route.params.slug}`).findOne();
});
// Generate localized paths for SEO
const canonicalPath = computed(() => {
return locale.value === 'en'
? route.path
: `/${locale.value}${route.path.replace(/^\/[a-z]{2}/, '')}`;
});
</script>
<template>
<article>
<h1>{{ page.title }}</h1>
<div>{{ page.description }}</div>
</article>
</template>
Expected output: Blog posts with locale-specific slugs like /blog/my-post (English) and /es/blog/mi-articulo (Spanish).
SEO with hreflang Tags
Automatic hreflang tags for all locales:
<script setup>
const { locale, locales, t } = useI18n();
const route = useRoute();
// Generate hreflang links
const hreflangLinks = computed(() => {
return locales.value.map(loc => ({
hid: `alternate-hreflang-${loc.code}`,
rel: 'alternate',
href: loc.code === 'en'
? `https://example.com${route.path}`
: `https://example.com/${loc.code}${route.path}`,
hreflang: loc.iso
}));
});
</script>
<template>
<Head>
<link
v-for="link in hreflangLinks"
:key="link.hid"
:rel="link.rel"
:href="link.href"
:hreflang="link.hreflang"
/>
</Head>
</template>
Expected output: HTML <link> tags telling search engines which URLs serve which languages, preventing duplicate content issues.
Common Mistakes
Using prefix Strategy without considering SEO: The
prefixstrategy (/en/about,/es/acerca) is best for SEO. Theprefix_except_defaultstrategy avoids a prefix for the default locale but creates URL inconsistencies.Not providing translation keys for all locales: A missing key in any locale causes a fallback to the default locale or a raw key display. Always keep translation files synchronized across all locales.
Forgetting to localize SEO meta tags: Search engines read
<title>and<meta description>tags. These must be translated too, not just the visible page content.Hardcoding locale-specific content in components: Text outside
$t()calls is not translated. Always use translation keys for user-facing text, including button labels, error messages, and placeholders.Not testing locale persistence: Without proper configuration, the selected locale resets on page reload. Use
detectBrowserLanguagewithuseCookieto persist the user's choice.
Practice Questions
What does
strategy: 'prefix_except_default'mean? Answer: All locales except the default get a URL prefix (/es/about). The default locale uses clean URLs (/about). This balances SEO and URL cleanliness.How does Lazy Loading of translations improve performance? Answer: With
lazy: true, translation files load only when the user switches to that locale. The initial bundle is smaller because it doesn't include all languages.What is the purpose of hreflang tags? Answer: They tell search engines which language versions of a page exist, preventing duplicate content penalties and showing the correct language in search results.
How do you access the current locale in a component? Answer: Use
const { locale } = useI18n()or$i18n.localein templates. Both provide the current active locale code.
Challenge
Build a multilingual documentation site with: three locales (English, Spanish, French), localized content files organized by locale, a language switcher that persists in a cookie, localized slugs for each document, hreflang tags in the page head, and automatic locale detection based on browser preference.
Mini Project
Create a portfolio site in at least two languages with: full page translation using JSON files, localized navigation and footer, a language switcher dropdown, locale-specific URLs with prefix strategy, hreflang SEO tags, translated meta descriptions and Open Graph tags, and a cookie-based locale persistence.
FAQ
What's Next
Learn about Nuxt Authentication and Authorization to add user login, session management, and protected routes to your Nuxt application.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro