Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 055 / 100

Internationalization in Vue and Nuxt: a practical guide

Ship in many languages.

Expona Admin8 min read

Introduction

Building a product that reaches users across different countries and languages is a game-changer for revenue and engagement. Internationalization in Vue and Nuxt: a practical guide will walk you through the complete toolkit you need to ship in many languages without rewriting your entire codebase. Whether you're scaling from a single market to ten, or you need to support pluralization rules that English doesn't even have, we'll cover the strategies that teams use in production to keep translations manageable and SEO-friendly.

Understanding Internationalization vs. Localization

Before diving into code, it's worth clarifying the difference. Internationalization (i18n) is the process of preparing your application to handle multiple languages and regions. Localization (l10n) is the adaptation of your app to a specific locale—translating text, formatting numbers, adjusting currencies, and respecting cultural norms. We handle both with the same tools, but the distinction helps when planning your architecture.

Vue-i18n for Translations

vue-i18n is the industry standard for translation management in Vue applications. It provides a simple API for loading translation files, switching languages at runtime, and interpolating dynamic values into translated strings.

Setting up vue-i18n

Start by installing the package and creating a messages file structure. Here's how a typical project looks:

typescript
// locales/en.json
{
  "home": {
    "title": "Welcome to Our Store",
    "tagline": "Premium products at fair prices"
  },
  "cart": {
    "empty": "Your cart is empty",
    "items": "You have {count} items in your cart"
  }
}

// locales/de.json
{
  "home": {
    "title": "Willkommen in unserem Shop",
    "tagline": "Premium-Produkte zu fairen Preisen"
  },
  "cart": {
    "empty": "Ihr Warenkorb ist leer",
    "items": "Sie haben {count} Artikel in Ihrem Warenkorb"
  }
}

Next, initialize vue-i18n in your main Vue app:

typescript
// main.ts
import { createApp } from 'vue'
import i18n from 'vue-i18n'
import App from './App.vue'
import en from './locales/en.json'
import de from './locales/de.json'

const app = createApp(App)

const i18nInstance = i18n.createI18n({
  legacy: false, // Use composition API
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    en,
    de
  }
})

app.use(i18nInstance)
app.mount('#app')

In your Vue components, access translations through the $t method or the composition API:

typescript
// Options API
<template>
  <div>
    <h1>{{ $t('home.title') }}</h1>
    <p>{{ $t('cart.items', { count: 5 }) }}</p>
  </div>
</template>

// Composition API
<script setup lang="ts">
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
</script>

<template>
  <div>
    <h1>{{ t('home.title') }}</h1>
  </div>
</template>

Managing translation keys at scale

As your product grows, you'll have thousands of keys scattered across multiple languages. Teams in production often organize by feature or page rather than nesting too deeply. Use a flat or semi-flat structure, and consider loading translations asynchronously to avoid shipping every language to every user:

typescript
// Dynamic import pattern
const loadLocaleMessages = async (locale: string) => {
  const messages = await import(
    /* webpackChunkName: "locale-[request]" */
    `./locales/${locale}.json`
  )
  return messages.default
}

i18nInstance.global.setLocaleMessage(locale, await loadLocaleMessages(locale))

@nuxtjs/i18n with Localized Routes

Nuxt users have a specialized module, @nuxtjs/i18n, that goes beyond simple translation strings. It handles locale detection, route localization, and language switching while keeping your SEO in order.

Configuration and setup

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],

  i18n: {
    locales: [
      { code: 'en', iso: 'en-US', name: 'English' },
      { code: 'de', iso: 'de-DE', name: 'Deutsch' },
      { code: 'fr', iso: 'fr-FR', name: 'Français' }
    ],
    defaultLocale: 'en',
    strategy: 'prefix', // or 'prefix_except_default', 'no_prefix'
    langDir: 'locales/',
    vueI18n: {
      fallbackLocale: 'en'
    }
  }
})

With strategy: 'prefix', all routes get a locale prefix: /en/products, /de/products, /fr/products. The prefix_except_default strategy omits the prefix for your default locale, so English users see /products while German users see /de/products.

Localizing routes

Define route translations in your locale files to create readable URLs in every language:

typescript
// locales/en.json
{
  "routes": {
    "products": "products",
    "about": "about-us",
    "checkout": "checkout"
  }
}

// locales/de.json
{
  "routes": {
    "products": "produkte",
    "about": "über-uns",
    "checkout": "kasse"
  }
}

Then configure your Nuxt routes to use these translations:

typescript
// nuxt.config.ts (within i18n config)
routes: {
  en: {
    products: '/products',
    about: '/about-us',
    checkout: '/checkout'
  },
  de: {
    products: '/produkte',
    about: '/über-uns',
    checkout: '/kasse'
  }
}

Now you can link between locales safely with the useLocalePath composable:

typescript
<script setup lang="ts">
import { useLocalePath } from '#i18n'

const localePath = useLocalePath()
</script>

<template>
  <router-link :to="localePath('/products')">
    Products
  </router-link>
</template>

Pluralization and Number Formatting

English has two plural forms: singular and plural. Russian has three. Welsh has six. vue-i18n handles these rules automatically when you use the plural syntax.

Handling plurals

typescript
// locales/en.json
{
  "messages": {
    "apples": "no apples | one apple | {count} apples"
  }
}

// locales/ru.json
{
  "messages": {
    "apples": "нет яблок | одно яблоко | {count} яблока | {count} яблок"
  }
}

// In your component
{{ $t('messages.apples', count) }}

vue-i18n automatically selects the correct plural form based on the count parameter and the locale's plural rules.

Number and currency formatting

Numbers and currencies vary by locale. Use the NumberFormat API that vue-i18n provides:

typescript
<script setup lang="ts">
import { useI18n } from 'vue-i18n'

const { n, d } = useI18n()
</script>

<template>
  <p>Price: {{ n(19.99, 'currency') }}</p>
  <p>Date: {{ d(new Date(), 'short') }}</p>
</template>

Configure formatters in your i18n initialization:

typescript
const i18nInstance = i18n.createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: { /* ... */ },
  numberFormats: {
    en: {
      currency: {
        style: 'currency',
        currency: 'USD'
      }
    },
    de: {
      currency: {
        style: 'currency',
        currency: 'EUR'
      }
    }
  },
  datetimeFormats: {
    en: {
      short: {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
      }
    },
    de: {
      short: {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
      }
    }
  }
})

hreflang Tags for SEO

Search engines need to know which pages are translations of each other. The hreflang tag tells Google and other crawlers to associate your English product page with its German and French equivalents, preventing duplicate content penalties.

Adding hreflang to your pages

In Nuxt, use the useHead composable to inject hreflang tags:

typescript
<script setup lang="ts">
import { useRoute } from '#app'
import { useSeoMeta, useHead } from '#head'

const route = useRoute()
const baseUrl = 'https://example.com'

const locales = ['en', 'de', 'fr']
const links = locales.map(locale => ({
  rel: 'alternate',
  hreflang: locale,
  href: `${baseUrl}/${locale}${route.path}`
}))

// Add x-default for search engines as fallback
links.push({
  rel: 'alternate',
  hreflang: 'x-default',
  href: `${baseUrl}/en${route.path}`
})

useHead({
  link: links
})

useSeoMeta({
  ogUrl: `${baseUrl}${route.path}`,
  canonicalUrl: `${baseUrl}/${route.locale}${route.path}`
})
</script>

If using @nuxtjs/i18n, the module can handle this automatically with proper configuration:

typescript
i18n: {
  locales: [
    { code: 'en', iso: 'en-US', name: 'English' },
    { code: 'de', iso: 'de-DE', name: 'Deutsch' }
  ],
  baseUrl: 'https://example.com',
  detectBrowserLanguage: false, // Disable auto-detection if desired
  strategy: 'prefix'
}

The module generates hreflang tags automatically on every route. Always include the baseUrl to ensure crawlers can follow the links.

Common Mistakes

  • Hardcoding strings: It's tempting to leave some labels untranslated "for now." Once you launch in a second market, this debt compounds quickly. Extract every user-facing string into your locale files from the start.
  • Forgetting the x-default hreflang: Without it, search engines may not know which version to rank in ambiguous regions. Always include hreflang="x-default" pointing to your primary language.
  • Inconsistent locale codes: Using en-US in one place and en in another causes routing mismatches. Pick a format (e.g., lowercase with hyphen) and stick to it across configs, route files, and APIs.
  • Loading all locales upfront: Shipping every translation to every user bloats your bundle. Use dynamic imports and lazy-load locale messages based on the user's selected language.
  • Ignoring currency and number formats: A price of "1000" means different things in the US (one thousand) and Germany (one). Always use locale-aware formatters, not string concatenation.
  • Not testing plural edge cases: Test with counts of 0, 1, 2, and unusual numbers (like 21 or 101) in every language. Plural rules are language-specific and easy to get wrong.

Key Takeaways

  • vue-i18n provides a solid foundation for translations; @nuxtjs/i18n adds route localization and SEO helpers on top.
  • Organize locale files by feature or domain rather than language-first, and load translations dynamically to keep bundles lean.
  • Pluralization rules are language-specific. Use vue-i18n's built-in plural syntax instead of manually checking counts.
  • Number and currency formatting must respect locale conventions. Use NumberFormat and DatetimeFormat APIs rather than string templates.
  • hreflang tags prevent duplicate content issues and guide search engines to rank the correct version for each market. Always include x-default and verify tags in Google Search Console.
  • Test localization early and across multiple locales, not just your primary market. Edge cases in plurals, formatting, and routing surface quickly in real usage.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles