Nuxt 3's powerful meta-framework architecture combined with the @nuxtjs/i18n module creates one of the most developer-friendly internationalization experiences in the Vue ecosystem. With first-class support for lazy loading translations, automatic SEO optimization through hreflang tags, flexible routing strategies (prefix, no_prefix, prefix_except_default), and seamless SSR/SSG rendering, Nuxt provides everything needed for production-grade multilingual applications. This comprehensive guide covers module configuration, lazy-loaded translations, SEO best practices, route strategies, dynamic locale switching, Pinia state management integration, and automated translation workflows with IntlPull for streamlined team collaboration.
Understanding Nuxt i18n Architecture
Nuxt i18n leverages three key layers:
- @nuxtjs/i18n Module: Official Nuxt module wrapping vue-i18n with enhanced features
- Route Middleware: Automatic locale detection and routing
- SSR/SSG Support: Server-rendered translations with zero hydration mismatch
This architecture provides automatic route generation for all locales, SEO-optimized alternate links, and lazy loading to minimize initial bundle size—all configured through Nuxt's intuitive module system.
Basic Setup with @nuxtjs/i18n
Step 1: Install Dependencies
Terminalnpm install @nuxtjs/i18n@next
Step 2: Configure Module
Update nuxt.config.ts:
TypeScript1export default defineNuxtConfig({ 2 modules: ['@nuxtjs/i18n'], 3 4 i18n: { 5 locales: [ 6 { 7 code: 'en', 8 iso: 'en-US', 9 name: 'English', 10 file: 'en.json', 11 }, 12 { 13 code: 'es', 14 iso: 'es-ES', 15 name: 'Español', 16 file: 'es.json', 17 }, 18 { 19 code: 'fr', 20 iso: 'fr-FR', 21 name: 'Français', 22 file: 'fr.json', 23 }, 24 ], 25 defaultLocale: 'en', 26 strategy: 'prefix_except_default', 27 lazy: true, 28 langDir: 'locales/', 29 detectBrowserLanguage: { 30 useCookie: true, 31 cookieKey: 'i18n_redirected', 32 redirectOn: 'root', 33 }, 34 }, 35});
Strategy Options:
prefix_except_default:/about(en),/es/about(es)prefix:/en/about,/es/aboutno_prefix:/about(all locales, uses cookie/header detection)
Step 3: Create Translation Files
Create locales/en.json:
JSON1{ 2 "nav": { 3 "home": "Home", 4 "about": "About", 5 "contact": "Contact" 6 }, 7 "home": { 8 "title": "Welcome to {appName}", 9 "subtitle": "Built with Nuxt 3", 10 "cta": "Get Started" 11 }, 12 "common": { 13 "loading": "Loading...", 14 "error": "An error occurred" 15 } 16}
Create locales/es.json:
JSON1{ 2 "nav": { 3 "home": "Inicio", 4 "about": "Acerca de", 5 "contact": "Contacto" 6 }, 7 "home": { 8 "title": "Bienvenido a {appName}", 9 "subtitle": "Construido con Nuxt 3", 10 "cta": "Comenzar" 11 }, 12 "common": { 13 "loading": "Cargando...", 14 "error": "Ocurrió un error" 15 } 16}
Step 4: Use Translations in Components
Create pages/index.vue:
VUE1<template> 2 <div> 3 <nav> 4 <NuxtLink to="/">{{ t('nav.home') }}</NuxtLink> 5 <NuxtLink to="/about">{{ t('nav.about') }}</NuxtLink> 6 <NuxtLink to="/contact">{{ t('nav.contact') }}</NuxtLink> 7 </nav> 8 9 <main> 10 <h1>{{ t('home.title', { appName: 'MyApp' }) }}</h1> 11 <p>{{ t('home.subtitle') }}</p> 12 <button>{{ t('home.cta') }}</button> 13 </main> 14 </div> 15</template> 16 17<script setup lang="ts"> 18const { t } = useI18n(); 19</script>
Lazy Loading Strategies
Namespace-Based Lazy Loading
Split translations by feature:
locales/
├── en/
│ ├── common.json
│ ├── home.json
│ ├── about.json
│ └── checkout.json
└── es/
├── common.json
├── home.json
└── ...
Update nuxt.config.ts:
TypeScript1export default defineNuxtConfig({ 2 i18n: { 3 locales: [ 4 { 5 code: 'en', 6 iso: 'en-US', 7 files: [ 8 'en/common.json', 9 'en/home.json', 10 'en/about.json', 11 'en/checkout.json', 12 ], 13 }, 14 ], 15 lazy: true, 16 langDir: 'locales/', 17 }, 18});
Load on Demand:
VUE1<script setup> 2const { t } = useI18n({ 3 useScope: 'local', 4 messages: { 5 en: { 6 checkout: await import('~/locales/en/checkout.json'), 7 }, 8 }, 9}); 10</script>
Dynamic Import for Large Files
TypeScript1// composables/useAsyncI18n.ts 2export const useAsyncI18n = async (namespace: string) => { 3 const { locale } = useI18n(); 4 const messages = await import(`~/locales/${locale.value}/${namespace}.json`); 5 6 return useI18n({ 7 messages: { 8 [locale.value]: messages.default, 9 }, 10 }); 11};
SEO Optimization
Automatic Hreflang Tags
@nuxtjs/i18n automatically generates hreflang tags:
VUE1<template> 2 <div> 3 <h1>{{ t('about.title') }}</h1> 4 </div> 5</template> 6 7<script setup> 8const { t } = useI18n(); 9 10// Generates: 11// <link rel="alternate" hreflang="en" href="https://example.com/about" /> 12// <link rel="alternate" hreflang="es" href="https://example.com/es/about" /> 13// <link rel="alternate" hreflang="x-default" href="https://example.com/about" /> 14</script>
Custom Meta Tags
VUE1<script setup> 2const { t } = useI18n(); 3 4useHead({ 5 title: t('about.metaTitle'), 6 meta: [ 7 { name: 'description', content: t('about.metaDescription') }, 8 { property: 'og:title', content: t('about.ogTitle') }, 9 { property: 'og:description', content: t('about.ogDescription') }, 10 ], 11}); 12</script>
Sitemap Generation
Install sitemap module:
Terminalnpm install @nuxtjs/sitemap
TypeScript1export default defineNuxtConfig({ 2 modules: ['@nuxtjs/i18n', '@nuxtjs/sitemap'], 3 4 sitemap: { 5 hostname: 'https://example.com', 6 i18n: true, // Auto-generates localized URLs 7 }, 8});
Route Strategies and Dynamic Switching
Custom Route Names
TypeScript1export default defineNuxtConfig({ 2 i18n: { 3 customRoutes: 'config', 4 pages: { 5 about: { 6 en: '/about-us', 7 es: '/acerca-de', 8 fr: '/a-propos', 9 }, 10 'services/index': { 11 en: '/services', 12 es: '/servicios', 13 fr: '/services', 14 }, 15 }, 16 }, 17});
Language Switcher Component
Create components/LanguageSwitcher.vue:
VUE1<template> 2 <div class="language-switcher"> 3 <button 4 v-for="locale in availableLocales" 5 :key="locale.code" 6 :class="{ active: locale.code === currentLocale }" 7 @click="setLocale(locale.code)" 8 > 9 {{ locale.name }} 10 </button> 11 </div> 12</template> 13 14<script setup lang="ts"> 15const { locale, locales, setLocale } = useI18n(); 16 17const availableLocales = computed(() => 18 locales.value.filter((l) => l.code !== locale.value) 19); 20 21const currentLocale = computed(() => locale.value); 22</script> 23 24<style scoped> 25.language-switcher { 26 display: flex; 27 gap: 1rem; 28} 29 30.active { 31 font-weight: bold; 32} 33</style>
Programmatic Locale Switching
VUE1<script setup> 2const { setLocale, locale } = useI18n(); 3const switchTo = useLocalePath(); 4 5async function changeLanguage(newLocale: string) { 6 await setLocale(newLocale); 7 await navigateTo(switchTo({ name: 'index' }, newLocale)); 8} 9</script>
Pinia State Management Integration
Setup Pinia Store with i18n
Create stores/user.ts:
TypeScript1import { defineStore } from 'pinia'; 2 3export const useUserStore = defineStore('user', { 4 state: () => ({ 5 preferredLocale: 'en', 6 profile: null, 7 }), 8 9 actions: { 10 async setPreferredLocale(locale: string) { 11 this.preferredLocale = locale; 12 13 // Sync with i18n 14 const { setLocale } = useI18n(); 15 await setLocale(locale); 16 17 // Persist to backend 18 if (this.profile) { 19 await $fetch('/api/user/preferences', { 20 method: 'PATCH', 21 body: { locale }, 22 }); 23 } 24 }, 25 }, 26});
Usage in Component:
VUE1<script setup> 2const userStore = useUserStore(); 3const { locale } = useI18n(); 4 5onMounted(() => { 6 // Restore user's preferred locale 7 if (userStore.preferredLocale !== locale.value) { 8 userStore.setPreferredLocale(locale.value); 9 } 10}); 11</script>
SSR and SSG Considerations
SSR: Server-Side Rendering
Nuxt automatically renders translations server-side:
VUE1<script setup> 2const { t } = useI18n(); 3 4// Rendered on server, hydrated on client (no FOUC) 5const title = t('home.title'); 6</script>
SSG: Static Site Generation
Pre-render all locale routes:
Terminalnpm run generate
Output Structure:
.output/public/
├── index.html (en)
├── about.html (en)
├── es/
│ ├── index.html
│ └── about.html
└── fr/
└── ...
Hybrid Rendering
Mix SSR and SSG:
VUE1<script setup> 2definePageMeta({ 3 prerender: true, // Prerender this route 4}); 5</script>
IntlPull CLI Integration
Installation
Terminalnpm install -g @intlpullhq/cli cd my-nuxt-app intlpull init --framework nuxt
Configuration (intlpull.config.json):
JSON1{ 2 "projectId": "your-project-id", 3 "apiKey": "ip_live_...", 4 "framework": "nuxt", 5 "sourcePath": "locales", 6 "format": "json", 7 "languages": ["en", "es", "fr"], 8 "defaultLanguage": "en", 9 "namespaces": ["common", "home", "about", "checkout"] 10}
Automated Workflows
Terminal1# Extract hardcoded strings from .vue files 2intlpull scan pages --auto-wrap 3 4# Before: 5<h1>Welcome to our app</h1> 6 7# After: 8<h1>{{ t('common.welcome') }}</h1> 9 10# Push translations 11intlpull push 12 13# Pull latest translations 14intlpull pull 15 16# Watch for remote changes 17intlpull watch
CI/CD Integration
YAML1# .github/workflows/i18n-sync.yml 2name: Sync Translations 3on: 4 push: 5 paths: 6 - 'locales/**' 7jobs: 8 sync: 9 runs-on: ubuntu-latest 10 steps: 11 - uses: actions/checkout@v3 12 - run: npm install -g @intlpullhq/cli 13 - run: intlpull push 14 env: 15 INTLPULL_API_KEY: ${{ secrets.INTLPULL_API_KEY }} 16 - run: intlpull pull 17 - uses: stefanzweifel/git-auto-commit-action@v4 18 with: 19 commit_message: 'chore: sync translations'
Testing Localization
Vitest Unit Tests
TypeScript1import { describe, it, expect } from 'vitest'; 2import { mountSuspended } from '@nuxt/test-utils/runtime'; 3import HomePage from '~/pages/index.vue'; 4 5describe('HomePage', () => { 6 it('renders welcome message in English', async () => { 7 const wrapper = await mountSuspended(HomePage, { 8 global: { 9 mocks: { 10 $i18n: { 11 locale: 'en', 12 t: (key: string) => key === 'home.title' ? 'Welcome to MyApp' : key, 13 }, 14 }, 15 }, 16 }); 17 18 expect(wrapper.text()).toContain('Welcome to MyApp'); 19 }); 20});
Playwright E2E Tests
TypeScript1import { test, expect } from '@playwright/test'; 2 3test('language switcher changes locale', async ({ page }) => { 4 await page.goto('/'); 5 await page.click('button:has-text("Español")'); 6 await expect(page.locator('h1')).toContainText('Bienvenido'); 7 await expect(page).toHaveURL('/es'); 8}); 9 10test('hreflang tags present', async ({ page }) => { 11 await page.goto('/about'); 12 const hreflang = page.locator('link[rel="alternate"][hreflang="es"]'); 13 await expect(hreflang).toHaveAttribute('href', /\/es\/about/); 14});
Best Practices
1. Use Composition API
VUE<script setup> const { t, locale, setLocale } = useI18n(); </script>
2. Avoid Runtime Key Concatenation
Bad:
VUE<template> <p>{{ t('errors.' + errorCode) }}</p> </template>
Good:
VUE1<script setup> 2const errorMessage = computed(() => { 3 const messages = { 4 404: t('errors.notFound'), 5 500: t('errors.serverError'), 6 }; 7 return messages[errorCode] || t('errors.unknown'); 8}); 9</script>
3. Preload Critical Translations
TypeScript1// nuxt.config.ts 2export default defineNuxtConfig({ 3 i18n: { 4 precompile: { 5 strictMessage: false, 6 }, 7 }, 8});
4. Cache Translations in Production
TypeScript1export default defineNuxtConfig({ 2 routeRules: { 3 '/locales/**': { cache: { maxAge: 60 * 60 * 24 } }, 4 }, 5});
Common Pitfalls
Issue: Hydration Mismatch
Cause: Server renders one locale, client hydrates with another.
Solution: Ensure cookie detection is enabled:
TypeScript1export default defineNuxtConfig({ 2 i18n: { 3 detectBrowserLanguage: { 4 useCookie: true, 5 cookieKey: 'i18n_redirected', 6 alwaysRedirect: true, 7 }, 8 }, 9});
Issue: Missing Translations on SSG
Cause: Lazy loading not compatible with SSG.
Solution: Set lazy: false for SSG builds:
TypeScript1export default defineNuxtConfig({ 2 i18n: { 3 lazy: process.env.NODE_ENV === 'production' ? false : true, 4 }, 5});
Issue: Large Bundle Size
Cause: All translations loaded upfront.
Solution: Use namespace splitting and lazy loading.
Production Deployment Checklist
- All routes pre-rendered (SSG) or server-rendered (SSR)
- Hreflang tags verified
- Sitemap includes all locale URLs
- Lazy loading configured for large translation files
- IntlPull CLI integrated in CI/CD
- Browser language detection tested
- Cookie persistence verified
- 404 page translated
- Meta tags translated
- Fallback locale set
Frequently Asked Questions
How do I handle dynamic content from CMS?
Fetch localized content in composables:
TypeScript1export const useCMSContent = async (slug: string) => { 2 const { locale } = useI18n(); 3 const { data } = await useFetch(`/api/content/${slug}`, { 4 params: { locale: locale.value }, 5 }); 6 return data; 7};
Can I use multiple translation backends?
Yes, configure multiple sources:
TypeScript1export default defineNuxtConfig({ 2 i18n: { 3 locales: [ 4 { 5 code: 'en', 6 files: ['en/local.json', 'en/cms.json'], 7 }, 8 ], 9 }, 10});
How do I translate Nuxt UI components?
Override component locales:
TypeScript1export default defineNuxtConfig({ 2 ui: { 3 i18n: { 4 en: { 5 button: { label: 'Click me' }, 6 }, 7 }, 8 }, 9});
Does IntlPull support Nuxt 3?
Yes, IntlPull CLI fully supports Nuxt 3 with auto-detection and namespace handling.
How do I test different locales locally?
Set cookie manually or use query param:
http://localhost:3000?locale=es
Conclusion
Nuxt 3's @nuxtjs/i18n module provides the most comprehensive internationalization solution in the Vue ecosystem. With automatic SEO optimization, flexible routing strategies, lazy loading, and seamless SSR/SSG support, you can build performant multilingual applications that scale globally. Integration with IntlPull streamlines translation management, enabling automated workflows, team collaboration, and instant updates without manual JSON editing.
Start with basic module configuration, adopt lazy loading for performance, optimize SEO with hreflang tags, and integrate IntlPull CLI for automated translation management. Your international users will appreciate the fast, localized experience, and your team will appreciate the simplified workflow.
Ready to build a global Nuxt app? Try IntlPull free with 500 keys and 3 languages, or explore our Nuxt documentation for advanced patterns.
