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
Locale Links
<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
- Use prefix_except_default strategy - Better UX for default locale users
- Enable browser detection - Auto-redirect to user's preferred language
- Provide language switcher - Always visible on every page
- Use lazy loading for large apps - Load translations on demand
- Organize translations by feature - Use nested keys (
nav.home,products.title) - Include locale in SEO - Use
hreflanglinks andog:locale - Handle missing translations - Provide fallback locale
- Use placeholders consistently -
{name}for simple,{n}for pluralization - Test all locales - Verify layout with longer translations (German, French)
- Keep keys consistent - Same structure across all locale files
Troubleshooting
Translations Not Loading
Check:
@nuxtjs/i18ninmodulesarray- Locale files in correct
langDirpath - File names match
fileproperty in config - JSON is valid (no trailing commas)
Routes Not Localized
- Verify
strategyis set correctly - Check
defaultLocalematches one of your locales - 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
- Vue I18n v11 - Upgraded from v10 with JIT compilation as default
- Improved Nuxt 4 Support - Better compatibility with Nuxt 4
- Custom Routes - Use
definePageMetafor per-page locale configuration - Server-Side Redirects - Improved server-side redirection behavior
- Strict SEO - Experimental strict SEO head management
- Fixed Behaviors -
strategyandredirectOncombinations now work as expected
Migration Notes
$tc()API integrated into$t()(from Vue I18n v10→v11 upgrade)- JIT compilation now default (no need for
jitoption) - New directory structure: i18n files resolved from
<rootDir>/i18n(configurable withrestructureDir) - Context functions require
$prefix: use$localePath()notlocalePath()in templates
Official Resources
- Documentation: https://i18n.nuxtjs.org
- Migration Guide: https://i18n.nuxtjs.org/docs/guide/migrating
- API Reference: https://i18n.nuxtjs.org/api
- Vue I18n Docs: https://vue-i18n.intlify.dev
- Examples: https://i18n.nuxtjs.org/examples
- GitHub: https://github.com/nuxt-modules/i18n
Note: This reference covers Nuxt I18n v10 (latest as of 2025-11) with Vue I18n v11. For v9 projects, consult the migration guide.