Files
gh-lttr-claude-marketplace-…/skills/nuxt/references/nuxt-i18n.md
2025-11-30 08:38:06 +08:00

15 KiB

Nuxt I18n Reference

Last Updated: 2025-11 (v10.2.0)

Check: @nuxtjs/i18n in package.json

Nuxt I18n v10 provides internationalization (i18n) for Nuxt applications with auto-imports, locale routing, SEO support, and integration with Vue I18n v11.

Installation & Setup

pnpm add @nuxtjs/i18n

nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["@nuxtjs/i18n"],

  i18n: {
    locales: [
      { code: "en", iso: "en-US", name: "English", file: "en.json" },
      { code: "fr", iso: "fr-FR", name: "Français", file: "fr.json" },
      { code: "es", iso: "es-ES", name: "Español", file: "es.json" },
    ],
    defaultLocale: "en",
    langDir: "locales/",
    strategy: "prefix_except_default", // or 'prefix', 'no_prefix'
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: "i18n_redirected",
      redirectOn: "root",
    },
  },
})

Directory Structure

locales/
├── en.json
├── fr.json
└── es.json

Example locale file (en.json):

{
  "welcome": "Welcome",
  "hello": "Hello {name}",
  "nav": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  },
  "products": {
    "title": "Our Products",
    "description": "Browse our collection"
  }
}

Basic Usage

Translation in Templates

<template>
  <div>
    <!-- Simple translation -->
    <h1>{{ $t("welcome") }}</h1>

    <!-- With parameters -->
    <p>{{ $t("hello", { name: "John" }) }}</p>

    <!-- Nested keys -->
    <nav>
      <NuxtLink to="/">{{ $t("nav.home") }}</NuxtLink>
      <NuxtLink to="/about">{{ $t("nav.about") }}</NuxtLink>
    </nav>

    <!-- Pluralization -->
    <p>{{ $t("items", { count: 5 }) }}</p>

    <!-- Number formatting -->
    <p>{{ $n(1000, "currency") }}</p>

    <!-- Date formatting -->
    <p>{{ $d(new Date(), "long") }}</p>
  </div>
</template>

Translation in Script

<script setup lang="ts">
const { t, locale, locales, setLocale } = useI18n()

// Get translation
const welcomeMessage = t("welcome")

// With parameters
const greeting = t("hello", { name: "John" })

// Current locale
console.log(locale.value) // 'en'

// Available locales
console.log(locales.value) // [{ code: 'en', ... }, ...]

// Change locale
async function switchLanguage(code: string) {
  await setLocale(code)
}
</script>

Routing

Route Strategies

prefix_except_default (recommended):

  • Default locale: /about
  • Other locales: /fr/about, /es/about

prefix:

  • All locales: /en/about, /fr/about, /es/about

no_prefix:

  • All locales use same path: /about
  • Locale detected from cookie/browser
<script setup lang="ts">
const localePath = useLocalePath()
const switchLocalePath = useSwitchLocalePath()
</script>

<template>
  <div>
    <!-- Link with current locale -->
    <NuxtLink :to="localePath('/')">
      {{ $t("nav.home") }}
    </NuxtLink>

    <NuxtLink :to="localePath('/about')">
      {{ $t("nav.about") }}
    </NuxtLink>

    <!-- Switch locale links -->
    <NuxtLink :to="switchLocalePath('fr')"> Français </NuxtLink>

    <NuxtLink :to="switchLocalePath('es')"> Español </NuxtLink>

    <!-- With named routes -->
    <NuxtLink :to="localePath({ name: 'products-id', params: { id: '123' } })">
      Product
    </NuxtLink>
  </div>
</template>

Programmatic Navigation

<script setup lang="ts">
const localePath = useLocalePath()
const router = useRouter()

async function goToAbout() {
  await router.push(localePath("/about"))
}

async function goToProduct(id: string) {
  await navigateTo(localePath({ name: "products-id", params: { id } }))
}
</script>

Composables

useI18n

Main composable for translations:

<script setup lang="ts">
const {
  t, // Translation function
  locale, // Current locale ref
  locales, // Available locales
  setLocale, // Change locale function
  n, // Number formatting
  d, // Date formatting
  tm, // Translation messages
  te, // Translation exists check
  getLocaleCookie, // Get locale cookie
} = useI18n()

// Check if translation exists
if (te("optional.message")) {
  console.log(t("optional.message"))
}

// Get all translations for a key
const navTranslations = tm("nav")
console.log(navTranslations) // { home: 'Home', about: 'About', ... }
</script>

useLocalePath

Generate localized paths:

<script setup lang="ts">
const localePath = useLocalePath()

// Simple path
const homePath = localePath("/")

// With route object
const productPath = localePath({
  name: "products-id",
  params: { id: "123" },
  query: { tab: "reviews" },
})

// With specific locale
const frenchPath = localePath("/about", "fr")
</script>

useSwitchLocalePath

Generate paths for locale switching:

<script setup lang="ts">
const switchLocalePath = useSwitchLocalePath()
const route = useRoute()

// Get path for switching to French on current page
const frenchPath = switchLocalePath("fr")

// Custom route
const customPath = switchLocalePath("es", "/about")
</script>

useRouteBaseName

Get route name without locale prefix:

<script setup lang="ts">
const getRouteBaseName = useRouteBaseName()
const route = useRoute()

// Current route: /fr/products/123
const baseName = getRouteBaseName(route) // 'products-id'
</script>

useBrowserLocale

Detect browser locale:

<script setup lang="ts">
const browserLocale = useBrowserLocale()
console.log(browserLocale) // 'en-US'
</script>

Common Patterns

Language Switcher

<script setup lang="ts">
const { locale, locales, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const availableLocales = computed(() =>
  locales.value.filter((l) => l.code !== locale.value),
)
</script>

<template>
  <div class="flex gap-2">
    <NuxtLink
      v-for="loc of availableLocales"
      :key="loc.code"
      :to="switchLocalePath(loc.code)"
      class="px-3 py-1 rounded hover:bg-gray-100"
    >
      {{ loc.name }}
    </NuxtLink>
  </div>
</template>

Dropdown Language Switcher

<script setup lang="ts">
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()

const currentLocale = computed(() =>
  locales.value.find((l) => l.code === locale.value),
)
</script>

<template>
  <UDropdown>
    <template #trigger>
      <UButton>
        {{ currentLocale?.name }}
      </UButton>
    </template>

    <div class="p-2">
      <NuxtLink
        v-for="loc of locales"
        :key="loc.code"
        :to="switchLocalePath(loc.code)"
        class="block px-4 py-2 hover:bg-gray-100"
      >
        {{ loc.name }}
      </NuxtLink>
    </div>
  </UDropdown>
</template>

SEO with I18n

<script setup lang="ts">
const { locale, locales, t } = useI18n()
const route = useRoute()
const switchLocalePath = useSwitchLocalePath()

useSeoMeta({
  title: t("seo.title"),
  description: t("seo.description"),
  ogLocale: locale.value,
  ogTitle: t("seo.ogTitle"),
  ogDescription: t("seo.ogDescription"),
})

useHead({
  htmlAttrs: {
    lang: locale.value,
  },
  link: [
    // Alternate language links for SEO
    ...locales.value.map((loc) => ({
      rel: "alternate",
      hreflang: loc.iso,
      href: `https://example.com${switchLocalePath(loc.code)}`,
    })),
  ],
})
</script>

Per-Page Translations

<script setup lang="ts">
const { t } = useI18n({
  useScope: "local",
})
</script>

<template>
  <div>
    <h1>{{ t("title") }}</h1>
    <p>{{ t("description") }}</p>
  </div>
</template>

<i18n lang="json">
{
  "en": {
    "title": "About Us",
    "description": "Learn more about our company"
  },
  "fr": {
    "title": "À propos",
    "description": "En savoir plus sur notre entreprise"
  }
}
</i18n>

Dynamic Content Translation

<script setup lang="ts">
const { data: products } = await useFetch("/api/products")
const { locale } = useI18n()

// Assuming API returns translations
const localizedProducts = computed(() =>
  products.value?.map((product) => ({
    ...product,
    name: product.translations[locale.value]?.name || product.name,
    description:
      product.translations[locale.value]?.description || product.description,
  })),
)
</script>

<template>
  <div v-for="product of localizedProducts" :key="product.id">
    <h3>{{ product.name }}</h3>
    <p>{{ product.description }}</p>
  </div>
</template>

Lazy Loading Translations

For large translation files:

// nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    lazy: true,
    langDir: "locales/",
    locales: [
      { code: "en", file: "en.json" },
      { code: "fr", file: "fr.json" },
      { code: "es", file: "es.json" },
    ],
  },
})

Number & Date Formatting

Number Formatting

Define formats in config:

// nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    numberFormats: {
      en: {
        currency: {
          style: "currency",
          currency: "USD",
        },
        decimal: {
          style: "decimal",
          minimumFractionDigits: 2,
        },
      },
      fr: {
        currency: {
          style: "currency",
          currency: "EUR",
        },
        decimal: {
          style: "decimal",
          minimumFractionDigits: 2,
        },
      },
    },
  },
})

Usage:

<template>
  <div>
    <!-- Currency -->
    <p>{{ $n(1234.56, "currency") }}</p>
    <!-- en: $1,234.56 -->
    <!-- fr: 1 234,56  -->

    <!-- Decimal -->
    <p>{{ $n(123456.789, "decimal") }}</p>
    <!-- en: 123,456.79 -->
    <!-- fr: 123 456,79 -->
  </div>
</template>

Date Formatting

Define formats in config:

// nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    datetimeFormats: {
      en: {
        short: {
          year: "numeric",
          month: "short",
          day: "numeric",
        },
        long: {
          year: "numeric",
          month: "long",
          day: "numeric",
          weekday: "long",
        },
      },
      fr: {
        short: {
          year: "numeric",
          month: "short",
          day: "numeric",
        },
        long: {
          year: "numeric",
          month: "long",
          day: "numeric",
          weekday: "long",
        },
      },
    },
  },
})

Usage:

<script setup lang="ts">
const date = new Date("2025-01-04")
</script>

<template>
  <div>
    <!-- Short format -->
    <p>{{ $d(date, "short") }}</p>
    <!-- en: Jan 4, 2025 -->
    <!-- fr: 4 janv. 2025 -->

    <!-- Long format -->
    <p>{{ $d(date, "long") }}</p>
    <!-- en: Saturday, January 4, 2025 -->
    <!-- fr: samedi 4 janvier 2025 -->
  </div>
</template>

Pluralization

Translation file:

{
  "items": "no items | one item | {count} items",
  "cart": "You have {n} item in your cart | You have {n} items in your cart"
}

Usage:

<template>
  <div>
    <p>{{ $t("items", 0) }}</p>
    <!-- "no items" -->
    <p>{{ $t("items", 1) }}</p>
    <!-- "one item" -->
    <p>{{ $t("items", 5) }}</p>
    <!-- "5 items" -->

    <p>{{ $t("cart", { n: 1 }) }}</p>
    <!-- "You have 1 item in your cart" -->
    <p>{{ $t("cart", { n: 3 }) }}</p>
    <!-- "You have 3 items in your cart" -->
  </div>
</template>

TypeScript Support

Typed Translations

// types/i18n.ts
export interface LocaleMessages {
  welcome: string
  hello: (params: { name: string }) => string
  nav: {
    home: string
    about: string
    contact: string
  }
}

Usage:

<script setup lang="ts">
import type { LocaleMessages } from "~/types/i18n"

const { t } = useI18n<LocaleMessages>()

// TypeScript will check keys and parameters
const welcome = t("welcome")
const hello = t("hello", { name: "John" })
const navHome = t("nav.home")
</script>

API Routes with I18n

// server/api/products.get.ts
export default defineEventHandler(async (event) => {
  const locale = getCookie(event, "i18n_redirected") || "en"

  const products = await db.products.findMany({
    include: {
      translations: {
        where: { locale },
      },
    },
  })

  return products.map((product) => ({
    ...product,
    name: product.translations[0]?.name || product.name,
    description: product.translations[0]?.description || product.description,
  }))
})

Best Practices

  1. Use prefix_except_default strategy - Better UX for default locale users
  2. Enable browser detection - Auto-redirect to user's preferred language
  3. Provide language switcher - Always visible on every page
  4. Use lazy loading for large apps - Load translations on demand
  5. Organize translations by feature - Use nested keys (nav.home, products.title)
  6. Include locale in SEO - Use hreflang links and og:locale
  7. Handle missing translations - Provide fallback locale
  8. Use placeholders consistently - {name} for simple, {n} for pluralization
  9. Test all locales - Verify layout with longer translations (German, French)
  10. Keep keys consistent - Same structure across all locale files

Troubleshooting

Translations Not Loading

Check:

  1. @nuxtjs/i18n in modules array
  2. Locale files in correct langDir path
  3. File names match file property in config
  4. JSON is valid (no trailing commas)

Routes Not Localized

  1. Verify strategy is set correctly
  2. Check defaultLocale matches one of your locales
  3. Ensure you're using localePath() for links

Browser Detection Not Working

// nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: "i18n_redirected",
      redirectOn: "root",
      alwaysRedirect: true,
      fallbackLocale: "en",
    },
  },
})

Missing Translations

Enable warnings in development:

// nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    compilation: {
      strictMessage: false,
    },
    vueI18n: "./i18n.config.ts",
  },
})
// i18n.config.ts
export default {
  legacy: false,
  locale: "en",
  missingWarn: true,
  fallbackWarn: true,
}

v10 Changes from v9

Key Updates

  1. Vue I18n v11 - Upgraded from v10 with JIT compilation as default
  2. Improved Nuxt 4 Support - Better compatibility with Nuxt 4
  3. Custom Routes - Use definePageMeta for per-page locale configuration
  4. Server-Side Redirects - Improved server-side redirection behavior
  5. Strict SEO - Experimental strict SEO head management
  6. Fixed Behaviors - strategy and redirectOn combinations now work as expected

Migration Notes

  • $tc() API integrated into $t() (from Vue I18n v10→v11 upgrade)
  • JIT compilation now default (no need for jit option)
  • New directory structure: i18n files resolved from <rootDir>/i18n (configurable with restructureDir)
  • Context functions require $ prefix: use $localePath() not localePath() in templates

Official Resources


Note: This reference covers Nuxt I18n v10 (latest as of 2025-11) with Vue I18n v11. For v9 projects, consult the migration guide.