La solución estándar es vue-i18n para apps Vue y @nuxtjs/i18n para Nuxt. Proporcionan cambio reactivo de locale, interpolación de mensajes, pluralización, formateo de fechas y números, y traducciones cargadas de forma diferida. Para necesidades más sencillas, puedes construir un sistema i18n ligero con un composable y archivos JSON.
Configuración de vue-i18n
npm install vue-i18n// i18n/index.ts
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
import es from './locales/es.json'
export const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: { en, es }
})// i18n/locales/en.json
{
"greeting": "Hello, {name}!",
"items": "No items | One item | {count} items",
"nav": {
"home": "Home",
"about": "About"
}
}// i18n/locales/es.json
{
"greeting": "¡Hola, {name}!",
"items": "Sin elementos | Un elemento | {count} elementos",
"nav": {
"home": "Inicio",
"about": "Acerca de"
}
}// main.ts
import { i18n } from './i18n'
const app = createApp(App)
app.use(i18n)
app.mount('#app')Usar traducciones en componentes
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<p>{{ t('greeting', { name: 'Miguel' }) }}</p>
<p>{{ t('items', 5) }}</p>
<nav>
<a href="/">{{ t('nav.home') }}</a>
<a href="/about">{{ t('nav.about') }}</a>
</nav>
<select v-model="locale">
<option value="en">English</option>
<option value="es">Español</option>
</select>
</template>Cambiar locale actualiza reactivamente todas las traducciones en la app.
Pluralización
vue-i18n usa formas separadas por pipe: cero | uno | muchos.
{
"messages": "No messages | 1 message | {count} messages"
}<p>{{ t('messages', 0) }}</p> <!-- No messages -->
<p>{{ t('messages', 1) }}</p> <!-- 1 message -->
<p>{{ t('messages', 42) }}</p> <!-- 42 messages -->Formateo de fechas y números
const i18n = createI18n({
locale: 'en',
datetimeFormats: {
en: {
short: { year: 'numeric', month: 'short', day: 'numeric' }
},
es: {
short: { year: 'numeric', month: 'short', day: 'numeric' }
}
},
numberFormats: {
en: {
currency: { style: 'currency', currency: 'USD' }
},
es: {
currency: { style: 'currency', currency: 'EUR' }
}
}
})<p>{{ d(new Date(), 'short') }}</p> <!-- Jun 1, 2026 / 1 jun 2026 -->
<p>{{ n(99.99, 'currency') }}</p> <!-- $99.99 / 99,99 € -->Carga diferida de traducciones
Para apps con muchos locales, carga las traducciones bajo demanda en lugar de incluirlas todas en el bundle:
async function loadLocale(locale: string) {
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
i18n.global.locale.value = locale
}<select @change="loadLocale(($event.target as HTMLSelectElement).value)">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>Solo el locale activo está en el bundle. Los demás se cargan cuando se seleccionan.
Nuxt i18n
@nuxtjs/i18n añade enrutamiento, SEO y soporte SSR sobre vue-i18n:
npx nuxi module add i18n// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'en', file: 'en.json', name: 'English' },
{ code: 'es', file: 'es.json', name: 'Español' }
],
defaultLocale: 'en',
lazy: true,
langDir: 'locales/',
strategy: 'prefix_except_default'
}
})Esto proporciona:
/aboutpara inglés,/es/aboutpara español<html lang="es">configurado automáticamenteuseLocalePath()para enlaces que respetan el locale- Traducciones cargadas de forma diferida por ruta
<script setup>
const localePath = useLocalePath()
const { locale, setLocale } = useI18n()
</script>
<template>
<NuxtLink :to="localePath('/about')">{{ $t('nav.about') }}</NuxtLink>
<button @click="setLocale('es')">Español</button>
</template>Enfoque DIY ligero
Para apps pequeñas que no necesitan pluralización ni formateo, un composable con archivos JSON funciona bien:
// composables/useI18n.ts
const locale = ref('en')
const messages: Record<string, Record<string, string>> = {}
export function useI18n() {
function t(key: string): string {
return messages[locale.value]?.[key] ?? key
}
async function setLocale(lang: string) {
if (!messages[lang]) {
const mod = await import(`../locales/${lang}.json`)
messages[lang] = mod.default
}
locale.value = lang
}
return { t, locale, setLocale }
}Es más sencillo, pero carece de pluralización, interpolación, formateo de fechas y el soporte del ecosistema de vue-i18n.
Cuándo usar cada opción
| Necesidad | Solución |
|---|---|
| i18n completo con pluralización, formateo y herramientas | vue-i18n |
| Nuxt con rutas localizadas y SEO | @nuxtjs/i18n |
| App pequeña, pocas cadenas, sin reglas de plural | Composable DIY |
| Sitio estático con pocas páginas por idioma | Archivos markdown separados por locale |