Rustango docs
← Guías

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.

i18n en Rustango: un Translator mantiene catálogos por locale; gettext busca una clave con fallback (locale → idioma base → por defecto → la clave misma), sustituye placeholders {name}, y ngettext elige singular vs. plural

¿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 template tera_tags) — siempre compilado. El middleware de locale/zona horaria por petición vive en rustango::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 con i18n_db_overrides_sqlite_live.rs.

Tabla de contenidos


Las dos mitades de la i18n

PreocupaciónDóndeQué hace
Resolver el localei18n::middleware::LocaleMiddlewareelige un locale por petición (cookie → Accept-Language → por defecto) y expone el extractor ActiveLocale
Traducir cadenasi18n::Translator (esta guía)busca una clave de mensaje en el catálogo del locale activo
Renderizar fechas en la TZ del usuarioi18n::timezoneel 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.