Internacionalización (i18n)
La internacionalización consiste en servir tu app en el idioma del usuario.
Rustango la divide en dos mitades: resolver qué locale quiere una petición
(cookie → Accept-Language → valor por defecto — tratado en
Middleware), y traducir cadenas a ese locale, que es el objeto
de esta guía. El Translator carga catálogos de mensajes por locale, sustituye
{placeholders}, gestiona plurales y hace un fallback elegante — el gettext /
{% trans %} de Django, en Rust.
¿Nuevo con algún término aquí? locale, catálogo, Accept-Language, pluralización, RTL — consulta el glosario.
Fuente:
rustango::i18n(Translator,Locale,negotiate_language,plural_category,is_rtl_language,language_native_name, y los bindings de templatetera_tags) — siempre compilado. El middleware de locale/zona horaria por petición vive enrustango::i18n::middleware/::timezone(consulta Middleware).Versión ejecutable: cada fragmento está copiado de
i18n_doc.rs(cargo test -p rustango --test i18n_doc); las sobrescrituras de traducción respaldadas por base de datos se prueban en condiciones reales coni18n_db_overrides_sqlite_live.rs.
Tabla de contenidos
- Las dos mitades de la i18n
- Paso 1 — Construir un Translator
- Paso 2 — Traducir cadenas (gettext)
- Placeholders
- Pluralización
- Orden de fallback
- Negociar el idioma
- Idiomas de derecha a izquierda
- Traducir en templates (Tera)
- Cargar catálogos + sobrescrituras en tiempo de ejecución
- Véase también
Las dos mitades de la i18n
| Preocupación | Dónde | Qué hace |
|---|---|---|
| Resolver el locale | i18n::middleware::LocaleMiddleware | elige un locale por petición (cookie → Accept-Language → por defecto) y expone el extractor ActiveLocale |
| Traducir cadenas | i18n::Translator (esta guía) | busca una clave de mensaje en el catálogo del locale activo |
| Renderizar fechas en la TZ del usuario | i18n::timezone | el filtro {{ ts | localtime }} |
Cableas el middleware una vez, luego traduces usando el locale que resolvió.
Paso 1 — Construir un Translator
Un Translator mantiene un catálogo (un mapa key → message) por locale.
Constrúyelo con un locale por defecto, luego añade catálogos:
use rustango::i18n::{Locale, Translator};
use std::collections::HashMap;
let mut en = HashMap::new();
en.insert("greeting".to_owned(), "Hello".to_owned());
en.insert("welcome".to_owned(), "Welcome, {name}!".to_owned());
let mut fr = HashMap::new();
fr.insert("greeting".to_owned(), "Bonjour".to_owned());
let translator = Translator::new(Locale::new("en")) // default locale
.add_locale(Locale::new("en"), en)
.add_locale(Locale::new("fr"), fr);
Mantén el Translator en el estado de tu app (es barato de clonar-compartir) y
busca cadenas con el ActiveLocale que
resolvió el middleware.
Paso 2 — Traducir cadenas (gettext)
gettext(locale, key) devuelve el mensaje para ese locale — y degrada de forma
segura cuando falta algo:
translator.gettext("en", "greeting"); // "Hello"
translator.gettext("fr", "greeting"); // "Bonjour"
translator.gettext("en", "missing.key"); // "missing.key" — returns the key, never panics
translator.gettext("de", "greeting"); // "Hello" — unknown locale → default
Una clave faltante devuelve la clave misma, así que una cadena sin traducir aparece de forma visible en lugar de tumbar la página.
Placeholders
Los mensajes pueden llevar placeholders {name}; translate los sustituye a
partir de pares clave/valor:
// catalog: "welcome" = "Welcome, {name}!"
translator.translate("en", "welcome", &[("name", "Ada")]); // "Welcome, Ada!"
gettext es el atajo sin placeholders; translate (y gettext_fmt) toman
parámetros.
Pluralización
Los plurales difieren según el idioma y el conteo. ngettext es el atajo de dos
formas: elige la clave singular para la categoría CLDR one y la clave plural en
caso contrario, enlazando {count} automáticamente:
// catalog: "cart.one" = "1 item", "cart.other" = "{count} items"
translator.ngettext("en", "cart.one", "cart.other", 1); // "1 item"
translator.ngettext("en", "cart.one", "cart.other", 5); // "5 items"
Usa ngettext_fmt para pasar placeholders adicionales junto a {count}.
Idiomas con más de dos formas
El polaco, el ucraniano, el ruso, el árabe y otros tienen formas few / many
que un par singular/plural no puede expresar. plural_category(locale, n) devuelve
la categoría CLDR (one / few / many / other), y translate_plural elige la
forma que coincide de un catálogo por categoría — una entrada por clave que
contiene todas sus formas:
use rustango::i18n::plural_category;
plural_category("en", 1); // "one"
plural_category("fr", 0); // "one" — French treats 0 as singular
plural_category("pl", 2); // "few" — Polish 2–4
plural_category("pl", 5); // "many"
// pl plural catalog: "deleted_pages" → { one, few, many }
let n = 5;
translator.translate_plural("pl", "deleted_pages", n, &[("count", &n.to_string())]);
// → "Usunięto 5 stron." (the `many` form)
Una forma faltante cae de vuelta a other, luego al lookup escalar (y finalmente a
la clave), así que un idioma sin traducir aún se renderiza. Los idiomas del este
asiático (zh, ja, …) tienen una única forma other.
Orden de fallback
Los lookups recorren una cadena para que una traducción parcial nunca deje un
hueco: el locale solicitado → su idioma base → la cadena de fallback → el locale
por defecto → la clave misma. Así una petición franco-canadiense se resuelve
contra el catálogo fr:
// only "fr" is registered, not "fr-CA"
translator.gettext("fr-CA", "greeting"); // "Bonjour" — base-language fallback
Establece fallbacks adicionales con
Translator::new(default).with_fallback_chain(&["en"]).
Negociar el idioma
Si resuelves el locale por tu cuenta (fuera del middleware), negotiate_language
parsea un header Accept-Language del navegador y elige la mejor coincidencia
entre los locales que soportas:
use rustango::i18n::negotiate_language;
negotiate_language("fr-FR,fr;q=0.9,en;q=0.8", &["en", "fr"]); // Some("fr")
negotiate_language("de,ja;q=0.5", &["en", "fr"]); // None — fall back to default
Esto es exactamente lo que usa LocaleMiddleware por debajo.
Idiomas de derecha a izquierda
is_rtl_language (y Locale::direction()) te dicen si debes poner dir="rtl" en
la página — para el árabe, el hebreo, el persa, etc.:
use rustango::i18n::is_rtl_language;
is_rtl_language("ar"); // true
is_rtl_language("en"); // false
En un template: <html dir="{{ direction }}">, alimentado desde el ActiveLocale.
Consulta la nota RTL en Middleware.
Traducir en templates (Tera)
Todo lo anterior es la API de Rust; en los templates Tera — vistas HTML y la UI del
admin — el mismo Translator se expone como filtros/funciones. Regístralos una vez
contra el translator, y el locale activo (la variable de contexto LANG que fija
el middleware) dirige cada lookup:
rustango::i18n::tera_tags::register(&mut tera, translator.clone());
{{ "Save" | translate(locale=LANG) }}
<button title="{{ "Delete" | translate(locale=LANG) }}">…</button>
{# count-aware: pass the count both as the selector `n` and as a {count} arg #}
{{ translate_plural(key="deleted_pages", n=num, locale=LANG, count=num) }}
<html lang="{{ LANG }}" dir="{{ get_text_direction(locale=LANG) }}">
La clave es la cadena fuente en inglés (estilo gettext), así que una cadena sin traducir se renderiza en inglés en lugar de en blanco — envuelves una cadena de UI primero y la traduces después, sin un registro de claves que mantener.
Así es exactamente como rustango-cms localiza su admin: cada cadena del chrome
pasa por translate / translate_plural, los catálogos se envían por locale bajo
src/admin/locales/, el locale activo se resuelve por la cadena cookie →
preferencia por usuario → Accept-Language → por defecto por tenant, y un
conmutador en la barra lateral más un ajuste Preferences → Language por usuario
permiten que los editores elijan. Las ediciones del operador vía la capa de
sobrescritura en BD (abajo) surten efecto sin un redeploy.
Cargar catálogos + sobrescrituras en tiempo de ejecución
Rara vez construyes mapas a mano en producción. Dos cargadores:
Translator::from_directory(dir, default)— carga un archivo de catálogo por locale desde un directorio al arrancar.Translator::from_settings(&settings.i18n)— lo cablea desde tu config.
Y los operadores pueden editar traducciones en tiempo de ejecución sin un
redeploy: set_override(locale, key, value) (o load_overrides(rows) desde una
tabla) se superponen a los catálogos de archivo y ganan para esa clave. Este flujo
de sobrescritura en BD impulsa el editor de traducciones del admin y se prueba en
condiciones reales en i18n_db_overrides_sqlite_live.rs.
Véase también
- Middleware — resolver el locale por petición (
LocaleMiddleware,ActiveLocale) y el filtro de zona horaria{{ ts | localtime }}. - Vistas HTML · El admin — dónde afloran las cadenas traducidas y el editor de traducciones.
- Glosario — locale, catálogo, RTL y compañía en lenguaje llano.
