IntlPull
Tutorial
13 min read

Nuxt i18n: Complete Vue Localization Guide for 2026

Production-ready Nuxt internationalization with @nuxtjs/i18n module. Master lazy loading, SEO hreflang tags, route strategies, SSR/SSG rendering, Pinia integration, and automated translation workflows.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Production-ready Nuxt internationalization with @nuxtjs/i18n module. Master lazy loading, SEO hreflang tags, route strategies, SSR/SSG rendering, Pinia integration, and automated translation workflows.

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:

  1. @nuxtjs/i18n Module: Official Nuxt module wrapping vue-i18n with enhanced features
  2. Route Middleware: Automatic locale detection and routing
  3. 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

Terminal
npm install @nuxtjs/i18n@next

Step 2: Configure Module

Update nuxt.config.ts:

TypeScript
1export 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/about
  • no_prefix: /about (all locales, uses cookie/header detection)

Step 3: Create Translation Files

Create locales/en.json:

JSON
1{
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:

JSON
1{
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:

VUE
1<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:

TypeScript
1export 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:

VUE
1<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

TypeScript
1// 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:

VUE
1<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

VUE
1<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:

Terminal
npm install @nuxtjs/sitemap
TypeScript
1export 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

TypeScript
1export 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:

VUE
1<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

VUE
1<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:

TypeScript
1import { 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:

VUE
1<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:

VUE
1<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:

Terminal
npm run generate

Output Structure:

.output/public/
├── index.html (en)
├── about.html (en)
├── es/
│   ├── index.html
│   └── about.html
└── fr/
    └── ...

Hybrid Rendering

Mix SSR and SSG:

VUE
1<script setup>
2definePageMeta({
3  prerender: true, // Prerender this route
4});
5</script>

IntlPull CLI Integration

Installation

Terminal
npm install -g @intlpullhq/cli
cd my-nuxt-app
intlpull init --framework nuxt

Configuration (intlpull.config.json):

JSON
1{
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

Terminal
1# 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

YAML
1# .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

TypeScript
1import { 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

TypeScript
1import { 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:

VUE
1<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

TypeScript
1// nuxt.config.ts
2export default defineNuxtConfig({
3  i18n: {
4    precompile: {
5      strictMessage: false,
6    },
7  },
8});

4. Cache Translations in Production

TypeScript
1export 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:

TypeScript
1export 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:

TypeScript
1export 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:

TypeScript
1export 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:

TypeScript
1export 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:

TypeScript
1export 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.

Tags
nuxt
vue
i18n
localization
nuxt-i18n
ssr
seo
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.