Capítulo 34 - Parte 1: i18n nativo con @angular/localize¶
Parte 1 de 4 · Capítulo 34 · PARTE XIV - Arquitectura y Patrones Avanzados
La internacionalización -i18n- es uno de esos temas que parece sencillo hasta que el proyecto crece y el equipo empieza a pedir que la app funcione en francés, árabe o japonés. Angular incluye soporte nativo a través de @angular/localize, una solución robusta basada en compilación que garantiza que cada idioma produzca un bundle optimizado y completamente independiente. Veamos cómo funciona de punta a punta.
Instalación y configuración inicial¶
El primer paso es agregar el paquete al proyecto. Angular CLI lo registra automáticamente:
Este comando modifica polyfills.ts (o angular.json en proyectos más recientes) para importar @angular/localize/init. También actualiza tsconfig.app.json para incluir los tipos necesarios. Tras ejecutarlo, el proyecto ya está listo para marcar texto traducible.
Marcar texto en templates HTML¶
El mecanismo central del i18n nativo es el atributo i18n. Lo añadimos directamente sobre cualquier elemento del template cuyo contenido textual queramos traducir:
<!-- productos.component.html -->
<h1 i18n>Catálogo de productos</h1>
<p i18n>
Encuentra aquí todos los artículos disponibles para tu región.
</p>
<button i18n>Agregar al carrito</button>
Cuando ejecutamos la extracción, Angular lee estos atributos y genera un archivo XLIFF con cada cadena lista para ser enviada al traductor.
Contexto e ID estable¶
El formato completo del atributo i18n acepta una descripción y un ID único separados por @@:
<!-- La descripción ayuda al traductor a entender el contexto -->
<!-- El ID estable (@@ prefix) evita que el ID cambie si el texto cambia -->
<h2 i18n="Título de la sección de bienvenida@@bienvenida-titulo">
Bienvenido a nuestra tienda
</h2>
<span i18n="Etiqueta del precio con descuento@@precio-descuento">
Precio especial
</span>
<p i18n="Mensaje de error cuando no hay stock@@producto-sin-stock">
Este producto no está disponible en este momento.
</p>
Sin el ID @@, Angular genera un hash basado en el contenido. Si el texto cambia aunque sea una coma, el ID cambia y se pierden las traducciones existentes. Usar @@id-estable es una buena práctica obligatoria en proyectos serios.
Atributos traducibles¶
Para traducir atributos HTML (no solo contenido), usamos la variante i18n-nombreAtributo:
<img
src="/assets/logo.png"
i18n-alt="Texto alternativo del logo@@logo-alt"
alt="Logo de nuestra empresa"
/>
<input
type="search"
i18n-placeholder="Placeholder del buscador@@buscador-placeholder"
placeholder="Buscar productos..."
/>
$localize en TypeScript¶
Muchas veces necesitamos texto traducible fuera de los templates: en servicios, en mensajes de error o en notificaciones. Para eso usamos la función etiquetada $localize:
// notificaciones.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class NotificacionesService {
obtenerMensajeBienvenida(nombreUsuario: string): string {
return $localize`Bienvenido, ${nombreUsuario}:nombreUsuario:.
Tu sesión está activa.`;
}
obtenerErrorGenerico(): string {
// Con ID estable usando :@@id: después del texto
return $localize`:Mensaje de error genérico@@error-generico:
Ocurrió un error inesperado. Intenta nuevamente.`;
}
}
La sintaxis de interpolación usa ${variable}:nombrePlaceholder: para que el archivo XLIFF incluya el nombre del placeholder, lo que ayuda enormemente a los traductores a entender qué va en ese lugar.
Extracción de mensajes¶
Una vez marcadas todas las cadenas, extraemos el archivo de mensajes base:
Esto genera src/locale/messages.xlf con formato XLIFF 1.2 por defecto. También podemos usar XLIFF 2 o XMB:
El archivo resultante se ve así:
<!-- src/locale/messages.xlf -->
<trans-unit id="bienvenida-titulo" datatype="html">
<source>Bienvenido a nuestra tienda</source>
<context-group purpose="location">
<context context-type="sourcefile">
src/app/inicio/inicio.component.html
</context>
<context context-type="linenumber">5</context>
</context-group>
<note priority="1" from="description">
Título de la sección de bienvenida
</note>
</trans-unit>
Crear y editar archivos de traducción¶
Copiamos messages.xlf por cada idioma objetivo y añadimos el elemento <target>:
<!-- src/locale/messages.es.xlf -->
<trans-unit id="bienvenida-titulo" datatype="html">
<source>Bienvenido a nuestra tienda</source>
<target state="translated">
Bienvenido a nuestra tienda
</target>
</trans-unit>
<!-- src/locale/messages.fr.xlf -->
<trans-unit id="bienvenida-titulo" datatype="html">
<source>Bienvenido a nuestra tienda</source>
<target state="translated">
Bienvenue dans notre boutique
</target>
</trans-unit>
En proyectos grandes es común usar herramientas especializadas como Phrase, Lokalise o Transifex que leen y escriben XLIFF directamente.
Configurar angular.json para múltiples locales¶
Registramos cada locale en la configuración del proyecto:
// angular.json (fragmento relevante)
{
"projects": {
"mi-app": {
"i18n": {
"sourceLocale": "en-US",
"locales": {
"es": {
"translation": "src/locale/messages.es.xlf",
"baseHref": "/es/"
},
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
}
}
},
"architect": {
"build": {
"configurations": {
"production": {
"localize": true
}
}
}
}
}
}
}
Construir todos los locales¶
Con la configuración lista, un solo comando genera un build optimizado por idioma:
La salida queda en carpetas separadas:
Cada carpeta contiene un bundle completamente independiente con las cadenas ya incrustadas. No hay ningún overhead en runtime: las traducciones viven en el código JavaScript compilado.
Servir por subdominio o subfolder¶
La estrategia más común es usar el baseHref configurado y servir desde un servidor web o CDN:
# nginx.conf - subfolder por idioma
server {
listen 80;
location /es/ {
root /var/www/mi-app/browser;
try_files $uri $uri/ /es/index.html;
}
location /fr/ {
root /var/www/mi-app/browser;
try_files $uri $uri/ /fr/index.html;
}
# Redirigir raíz según Accept-Language
location = / {
return 302 /es/;
}
}
Para subdominios (es.miapp.com, fr.miapp.com), el deploy de cada carpeta va al subdominio correspondiente. Es una decisión de infraestructura, no de Angular.
La limitación principal: rebuild por idioma¶
El enfoque nativo de Angular es compile-time: las traducciones se "hornean" en el bundle. Esto significa que para cambiar el idioma o agregar uno nuevo, se necesita un nuevo build completo. No existe un selector de idioma en runtime que cambie las cadenas sin recargar la página y servir el bundle correcto.
flowchart LR
A[Código fuente<br/>+ messages.es.xlf] --> B[ng build --localize]
B --> C[bundle-es.js<br/>cadenas en español]
A2[Código fuente<br/>+ messages.fr.xlf] --> B
B --> C2[bundle-fr.js<br/>cadenas en francés]
C --> D[Servidor web]
C2 --> D
D --> E{Navegador solicita /es/}
E --> C
D --> F{Navegador solicita /fr/}
F --> C2
Esta arquitectura tiene ventajas claras: cero overhead en runtime, bundles perfectamente optimizados, y soporte completo de SSR por locale. Pero si el proyecto necesita cambio de idioma sin recarga de página, la solución es Transloco (lo vemos en la siguiente parte).
Puntos clave¶
ng add @angular/localizeprepara el proyecto y modificatsconfig.app.jsonautomáticamente.- Usar
i18n="descripción@@id-estable"en cada cadena evita perder traducciones cuando el texto fuente cambia. $localizepermite marcar cadenas en TypeScript con la misma semántica que los templates.ng extract-i18ngenera el archivo XLIFF base; copiamos y traducimos por cada idioma.ng build --configuration=productioncon"localize": truegenera un bundle independiente por idioma: máximo rendimiento, cero runtime overhead.
¿Qué sigue?¶
En la siguiente parte conoceremos Transloco, la biblioteca que resuelve el caso de uso que el i18n nativo no cubre: cambio de idioma en runtime sin rebuilds ni recargas de página.