El Problema
Has construido una hermosa aplicación Vue. Ahora tu equipo de producto dice "necesitamos soportar español, francés y alemán."
¿Por dónde empiezas? ¿Cómo:
- Extraer todas esas cadenas codificadas?
- Cambiar idiomas sin recargas de página?
- Manejar plurales de manera diferente en cada idioma?
- Cargar traducciones de forma perezosa para mantener tu bundle pequeño?
Esta guía recorre todo, usando vue-i18n, la biblioteca oficial de localización de Vue.
Lo Que Construirás
Al final de este tutorial, tendrás una aplicación Vue que:
- ✅ Cambia idiomas instantáneamente
- ✅ Maneja plurales correctamente en más de 40 idiomas
- ✅ Formatea fechas, números y monedas por locale
- ✅ Carga traducciones de forma perezosa para rendimiento
- ✅ Funciona con Vue Router para URLs amigables con SEO
- ✅ Se sincroniza con un sistema de gestión de traducciones
Stack tecnológico:
- Vue 3 (Composition API)
- vue-i18n v9
- Vite
- TypeScript (opcional pero recomendado)
Paso 1: Instalar vue-i18n
Primero, instala la biblioteca:
Terminalnpm install vue-i18n@9
¿Por qué vue-i18n? Es el plugin oficial de i18n para Vue, mantenido por el equipo de Vue. Tiene:
- Más de 5M de descargas semanales
- Soporte integrado para la Composition API de Vue 3
- Soporte SSR para Nuxt
- Formato de mensaje ICU (igual que React/Angular)
Paso 2: Crear Archivos de Traducción
Crea una carpeta locales con tus archivos de traducción:
src/
├── locales/
│ ├── en.json
│ ├── es.json
│ └── fr.json
└── i18n.ts
en.json:
JSON1{ 2 "nav": { 3 "home": "Home", 4 "about": "About", 5 "pricing": "Pricing" 6 }, 7 "hero": { 8 "title": "Build apps faster", 9 "subtitle": "Ship features your users love", 10 "cta": "Get started" 11 }, 12 "product": { 13 "addToCart": "Add to cart", 14 "itemCount": "No items | {n} item | {n} items", 15 "price": "{amount} per month" 16 } 17}
es.json:
JSON1{ 2 "nav": { 3 "home": "Inicio", 4 "about": "Acerca de", 5 "pricing": "Precios" 6 }, 7 "hero": { 8 "title": "Crea aplicaciones más rápido", 9 "subtitle": "Lanza funciones que tus usuarios aman", 10 "cta": "Empezar" 11 }, 12 "product": { 13 "addToCart": "Añadir al carrito", 14 "itemCount": "Sin artículos | {n} artículo | {n} artículos", 15 "price": "{amount} por mes" 16 } 17}
Consejos de estructura de claves:
- Anidar por característica (
nav,hero,product) no por página - Usar claves semánticas (
addToCart) no posiciones de UI (button_1) - Formato consistente en todos los idiomas
Paso 3: Configurar vue-i18n
Crear src/i18n.ts:
TypeScript1import { createI18n } from 'vue-i18n'; 2import en from './locales/en.json'; 3import es from './locales/es.json'; 4import fr from './locales/fr.json'; 5 6export const i18n = createI18n({ 7 legacy: false, // Usar modo Composition API 8 locale: 'en', // Idioma predeterminado 9 fallbackLocale: 'en', // Respaldo si falta traducción 10 messages: { 11 en, 12 es, 13 fr, 14 }, 15 // Habilitar advertencias en desarrollo 16 missingWarn: import.meta.env.DEV, 17 fallbackWarn: import.meta.env.DEV, 18});
¿Por qué legacy: false? Esto habilita el soporte de Composition API. Si estás usando Vue 3, siempre establece esto.
Paso 4: Agregar a Tu Aplicación Vue
En main.ts:
TypeScript1import { createApp } from 'vue'; 2import App from './App.vue'; 3import { i18n } from './i18n'; 4 5const app = createApp(App); 6app.use(i18n); 7app.mount('#app');
Eso es todo. Ahora cada componente tiene acceso a las traducciones.
Paso 5: Usar Traducciones en Componentes
Uso Básico (Composition API)
VUE1<script setup> 2import { useI18n } from 'vue-i18n'; 3 4const { t } = useI18n(); 5</script> 6 7<template> 8 <nav> 9 <a href="/">{{ $t('nav.home') }}</a> 10 <a href="/about">{{ $t('nav.about') }}</a> 11 <a href="/pricing">{{ $t('nav.pricing') }}</a> 12 </nav> 13 14 <section> 15 <h1>{{ $t('hero.title') }}</h1> 16 <p>{{ $t('hero.subtitle') }}</p> 17 <button>{{ $t('hero.cta') }}</button> 18 </section> 19</template>
Con Interpolación
Pasar variables usando el segundo argumento:
VUE1<script setup> 2const { t } = useI18n(); 3const price = 29; 4</script> 5 6<template> 7 <p>{{ $t('product.price', { amount: `$${price}` }) }}</p> 8 <!-- Salida: "$29 por mes" --> 9</template>
Traducción:
JSON1{ 2 "product": { 3 "price": "{amount} por mes" 4 } 5}
Paso 6: Manejar Pluralización
Diferentes idiomas tienen diferentes reglas de plural. El inglés tiene 2 formas (1 item, 2 items). El polaco tiene 3. ¡El árabe tiene 6!
vue-i18n maneja esto automáticamente usando la sintaxis de pipe:
JSON1{ 2 "product": { 3 "itemCount": "Sin artículos | {n} artículo | {n} artículos" 4 } 5}
En tu componente:
VUE1<script setup> 2const { t } = useI18n(); 3const count = ref(0); 4</script> 5 6<template> 7 <p>{{ $t('product.itemCount', count) }}</p> 8 <!-- count=0: "Sin artículos" --> 9 <!-- count=1: "1 artículo" --> 10 <!-- count=5: "5 artículos" --> 11</template>
Para plurales complejos, usa formato de mensaje ICU:
JSON1{ 2 "cart": { 3 "summary": "{count, plural, =0 {Tu carrito está vacío} one {# artículo en el carrito} other {# artículos en el carrito}}" 4 } 5}
Paso 7: Formatear Fechas, Números, Monedas
vue-i18n incluye formateadores Intl:
VUE1<script setup> 2import { useI18n } from 'vue-i18n'; 3 4const { t, n, d } = useI18n(); 5const price = 1299.99; 6const releaseDate = new Date('2026-01-15'); 7</script> 8 9<template> 10 <!-- Formato de números --> 11 <p>{{ n(price, 'currency') }}</p> 12 <!-- en: "$1,299.99" --> 13 <!-- es: "1.299,99 €" --> 14 15 <!-- Formato de fechas --> 16 <p>{{ d(releaseDate, 'long') }}</p> 17 <!-- en: "January 15, 2026" --> 18 <!-- es: "15 de enero de 2026" --> 19</template>
Configurar formatos en i18n.ts:
TypeScript1export const i18n = createI18n({ 2 // ... otras opciones 3 numberFormats: { 4 en: { 5 currency: { 6 style: 'currency', 7 currency: 'USD', 8 }, 9 }, 10 es: { 11 currency: { 12 style: 'currency', 13 currency: 'EUR', 14 }, 15 }, 16 }, 17 datetimeFormats: { 18 en: { 19 short: { year: 'numeric', month: 'short', day: 'numeric' }, 20 long: { year: 'numeric', month: 'long', day: 'numeric' }, 21 }, 22 es: { 23 short: { year: 'numeric', month: 'short', day: 'numeric' }, 24 long: { year: 'numeric', month: 'long', day: 'numeric' }, 25 }, 26 }, 27});
Paso 8: Cambiar Idiomas
Crear un componente selector de idioma:
VUE1<script setup> 2import { useI18n } from 'vue-i18n'; 3 4const { locale, availableLocales } = useI18n(); 5 6const changeLanguage = (lang: string) => { 7 locale.value = lang; 8 // Persistir en localStorage 9 localStorage.setItem('user-locale', lang); 10}; 11</script> 12 13<template> 14 <div class="language-switcher"> 15 <button 16 v-for="lang in availableLocales" 17 :key="lang" 18 :class="{ active: locale === lang }" 19 @click="changeLanguage(lang)" 20 > 21 {{ lang.toUpperCase() }} 22 </button> 23 </div> 24</template>
Detectar idioma preferido del usuario al cargar la aplicación:
TypeScript1// En i18n.ts 2function getInitialLocale(): string { 3 // 1. Verificar localStorage 4 const saved = localStorage.getItem('user-locale'); 5 if (saved) return saved; 6 7 // 2. Verificar idioma del navegador 8 const browserLang = navigator.language.split('-')[0]; 9 if (['en', 'es', 'fr'].includes(browserLang)) { 10 return browserLang; 11 } 12 13 // 3. Predeterminado 14 return 'en'; 15} 16 17export const i18n = createI18n({ 18 locale: getInitialLocale(), 19 // ... 20});
Paso 9: Carga Perezosa de Traducciones
Cargar todos los idiomas por adelantado aumenta el tamaño de tu bundle. Carga perezosa de idiomas bajo demanda:
TypeScript1// i18n.ts 2import { createI18n } from 'vue-i18n'; 3 4export const i18n = createI18n({ 5 legacy: false, 6 locale: 'en', 7 fallbackLocale: 'en', 8 messages: { 9 en: {}, // Comenzar vacío 10 }, 11}); 12 13// Cargar locale dinámicamente 14export async function loadLocale(locale: string) { 15 // Verificar si ya está cargado 16 if (i18n.global.availableLocales.includes(locale)) { 17 return; 18 } 19 20 // Carga perezosa del archivo de locale 21 const messages = await import(`./locales/${locale}.json`); 22 i18n.global.setLocaleMessage(locale, messages.default); 23} 24 25// Cambiar idioma con carga perezosa 26export async function setLocale(locale: string) { 27 await loadLocale(locale); 28 i18n.global.locale.value = locale; 29 localStorage.setItem('user-locale', locale); 30}
Uso:
VUE1<script setup> 2import { setLocale } from '@/i18n'; 3 4const switchLanguage = async (lang: string) => { 5 await setLocale(lang); 6}; 7</script>
Resultado: Solo el inglés se carga inicialmente. Español/francés se cargan cuando el usuario los selecciona.
Paso 10: URLs Amigables con SEO con Vue Router
Para SEO, usa URLs basadas en locale:
example.com/en/productsexample.com/es/productos
Configurar Vue Router:
TypeScript1import { createRouter, createWebHistory } from 'vue-router'; 2import { setLocale } from './i18n'; 3 4const routes = [ 5 { 6 path: '/:locale', 7 component: () => import('./layouts/LocaleLayout.vue'), 8 children: [ 9 { path: '', name: 'home', component: () => import('./views/Home.vue') }, 10 { path: 'products', name: 'products', component: () => import('./views/Products.vue') }, 11 { path: 'about', name: 'about', component: () => import('./views/About.vue') }, 12 ], 13 }, 14 // Redirigir raíz al locale predeterminado 15 { path: '/', redirect: '/en' }, 16]; 17 18const router = createRouter({ 19 history: createWebHistory(), 20 routes, 21}); 22 23// Actualizar locale en cambio de ruta 24router.beforeEach(async (to) => { 25 const locale = to.params.locale as string; 26 if (locale && ['en', 'es', 'fr'].includes(locale)) { 27 await setLocale(locale); 28 } 29}); 30 31export default router;
LocaleLayout.vue:
VUE1<script setup> 2import { useRoute } from 'vue-router'; 3import { watchEffect } from 'vue'; 4import { useI18n } from 'vue-i18n'; 5 6const route = useRoute(); 7const { locale } = useI18n(); 8 9// Sincronizar locale desde URL 10watchEffect(() => { 11 const urlLocale = route.params.locale as string; 12 if (urlLocale && locale.value !== urlLocale) { 13 locale.value = urlLocale; 14 } 15}); 16</script> 17 18<template> 19 <div> 20 <router-view /> 21 </div> 22</template>
Agregar etiquetas hreflang para SEO:
TypeScript1// En tu router o layout 2const addHreflangTags = (locale: string) => { 3 const head = document.querySelector('head'); 4 const existingTags = head?.querySelectorAll('link[rel="alternate"]'); 5 existingTags?.forEach(tag => tag.remove()); 6 7 const locales = ['en', 'es', 'fr']; 8 locales.forEach(lang => { 9 const link = document.createElement('link'); 10 link.rel = 'alternate'; 11 link.hreflang = lang; 12 link.href = `https://example.com/${lang}${route.path}`; 13 head?.appendChild(link); 14 }); 15};
Paso 11: Soporte de TypeScript
Obtener autocompletado para claves de traducción:
TypeScript1// types/i18n.d.ts 2import en from '@/locales/en.json'; 3 4type MessageSchema = typeof en; 5 6declare module 'vue-i18n' { 7 export interface DefineLocaleMessage extends MessageSchema {} 8}
Ahora TypeScript dará error si usas t('invalid.key'):
TypeScriptconst { t } = useI18n(); t('nav.home'); // ✅ Válido t('nav.invalid'); // ❌ Error de TypeScript
Paso 12: Optimización de Producción
1. División de Código
Dividir traducciones por ruta:
TypeScript1// En lugar de cargar todas las claves 2const messages = await import('./locales/en.json'); 3 4// Cargar solo lo que necesitas 5const common = await import('./locales/common/en.json'); 6const products = await import('./locales/products/en.json'); 7 8i18n.global.setLocaleMessage('en', { 9 ...common.default, 10 ...products.default, 11});
2. Precargar Siguiente Idioma
Si la mayoría de los usuarios cambian de inglés a español, precarga español:
TypeScript1// Precargar probable siguiente idioma 2if (locale.value === 'en') { 3 setTimeout(() => loadLocale('es'), 2000); 4}
3. Compilar Mensajes
vue-i18n puede precompilar mensajes para tiempo de ejecución más rápido:
Terminalnpm install @intlify/unplugin-vue-i18n -D
vite.config.ts:
TypeScript1import { defineConfig } from 'vite'; 2import vue from '@vitejs/plugin-vue'; 3import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 4import { resolve } from 'path'; 5 6export default defineConfig({ 7 plugins: [ 8 vue(), 9 VueI18nPlugin({ 10 include: resolve(__dirname, './src/locales/**'), 11 compositionOnly: true, 12 }), 13 ], 14});
Paso 13: Conectar con IntlPull
Gestionar archivos JSON manualmente no escala. Usa un Sistema de Gestión de Traducciones (TMS):
Instalar CLI de IntlPull:
Terminalnpm install -D @intlpullhq/cli
Configurar:
JSON1// .intlpull.json 2{ 3 "projectId": "your-project-id", 4 "sourceLanguage": "en", 5 "targetLanguages": ["es", "fr", "de"], 6 "format": "json", 7 "outputDir": "src/locales" 8}
Flujo de trabajo:
- Subir tus traducciones fuente:
Terminalnpx @intlpullhq/cli upload
- Enviar a IntlPull para traducción:
Terminalnpx @intlpullhq/cli upload
- Descargar traducciones:
Terminalnpx @intlpullhq/cli download
- Auto-sincronizar en CI/CD:
YAML1# .github/workflows/i18n.yml 2name: Sync Translations 3on: 4 schedule: 5 - cron: '0 */6 * * *' # Cada 6 horas 6 7jobs: 8 sync: 9 runs-on: ubuntu-latest 10 steps: 11 - uses: actions/checkout@v3 12 - run: npm install 13 - run: npx @intlpullhq/cli download 14 - run: | 15 git config user.name "i18n-bot" 16 git add src/locales 17 git commit -m "chore: sync translations" || exit 0 18 git push
Beneficios:
- ✅ Los traductores trabajan en una UI, no en JSON
- ✅ La memoria de traducción ahorra costos
- ✅ Pre-traducción IA para nuevas claves
- ✅ Flujos de trabajo de aprobación para calidad
- ✅ Cero participación del desarrollador después de la configuración
Errores Comunes
1. Olvidar Esperar Cargas Perezosas
TypeScript1// ❌ Malo - el locale puede no estar cargado 2locale.value = 'es'; 3 4// ✅ Bueno - esperar por ello 5await setLocale('es');
2. Cadenas Codificadas en Componentes
Ejecutar una verificación de lint:
Terminalnpx eslint . --rule 'no-literal-string: error'
O usar la verificación de estado de IntlPull:
Terminalnpx @intlpullhq/cli status
3. No Manejar Claves Faltantes
Siempre establecer un respaldo:
TypeScript1createI18n({ 2 fallbackLocale: 'en', 3 missing: (locale, key) => { 4 console.warn(`Traducción faltante: ${key} en ${locale}`); 5 return key; // Mostrar clave en lugar de vacío 6 }, 7});
4. Ignorar Idiomas RTL
Si soportas árabe/hebreo, agrega CSS RTL:
VUE1<template> 2 <div :dir="locale === 'ar' || locale === 'he' ? 'rtl' : 'ltr'"> 3 <!-- Tu aplicación --> 4 </div> 5</template>
Bonus de Nuxt 3
Si estás usando Nuxt 3, usa @nuxtjs/i18n:
Terminalnpm install @nuxtjs/i18n@next
nuxt.config.ts:
TypeScript1export default defineNuxtConfig({ 2 modules: ['@nuxtjs/i18n'], 3 i18n: { 4 locales: [ 5 { code: 'en', file: 'en.json' }, 6 { code: 'es', file: 'es.json' }, 7 { code: 'fr', file: 'fr.json' }, 8 ], 9 defaultLocale: 'en', 10 strategy: 'prefix', // URLs: /en/products, /es/productos 11 langDir: 'locales/', 12 lazy: true, // Carga perezosa de traducciones 13 detectBrowserLanguage: { 14 useCookie: true, 15 cookieKey: 'i18n_redirected', 16 }, 17 }, 18});
Uso en componentes:
VUE<script setup> const { t, locale, setLocale } = useI18n(); </script>
Misma API que vue-i18n, pero con SSR, enrutamiento automático y SEO integrado.
Benchmarks de Rendimiento
Comparación de tamaño de bundle (build de producción):
| Configuración | Bundle Inicial | Después de Carga Perezosa |
|---|---|---|
| Todos los locales eager | 450 KB | 450 KB |
| Carga perezosa de locales | 180 KB | 230 KB (cuando se necesita) |
| Mejora | 60% más pequeño | ~50% más pequeño |
Rendimiento en tiempo de ejecución:
- Búsqueda de traducción: < 0.1ms
- Cambio de idioma: < 5ms (en caché), < 100ms (carga perezosa)
- Impacto insignificante en el rendimiento de la aplicación
Lista de Verificación: ¿Está Tu Aplicación Vue Lista para i18n?
- ✅ Todas las cadenas de cara al usuario usan la función
t() - ✅ Sin concatenación de cadenas (usar interpolación)
- ✅ Los plurales usan sintaxis de pipe o formato ICU
- ✅ Fechas/números usan formateadores
d()yn() - ✅ Traducciones cargadas de forma perezosa por ruta
- ✅ SEO: etiquetas hreflang + URLs de locale
- ✅ Tipos de TypeScript para autocompletado
- ✅ Locale de respaldo configurado
- ✅ CI/CD sincroniza traducciones automáticamente
- ✅ TMS (IntlPull) integrado para escala
Próximos Pasos
Ahora tienes una configuración de localización de Vue lista para producción. Aquí está qué hacer a continuación:
- Agregar más idiomas: Solo crea nuevos archivos JSON y agrégalos a
availableLocales - Configurar IntlPull: Automatizar flujo de trabajo de traducción
- Monitorear cobertura: Usar análisis de IntlPull para rastrear progreso de traducción
- Optimizar bundle: División de código por ruta si traducciones > 100KB
- Prueba A/B: Medir tasas de conversión por idioma
¿Quieres saltarte el trabajo manual? Prueba la plantilla inicial de Vue de IntlPull. Viene con vue-i18n preconfigurado, flujos de trabajo CI/CD y actualizaciones OTA.
Lectura Adicional
- Documentación de vue-i18n
- Módulo Nuxt i18n
- Formato de Mensaje ICU
- Guía de Vue de IntlPull
- Reglas de Plural CLDR
Construir aplicaciones Vue multilingües no tiene que ser doloroso. Con vue-i18n y el flujo de trabajo correcto, puedes enviar productos globales más rápido.
¿Preguntas? Únete a nuestro Discord o contáctanos.
