Pythoni18nLocalizaciónNiceGUI

i18n sin gettext: traducciones en JSON con claves de punto

Publicado el 2026-07-17 · Xiliux

Quieres que tu app hable español e inglés. Buscas cómo, y el ecosistema te empuja a gettext o Babel: ficheros .po, un paso de compilación a .mo, herramientas de extracción. Potente, sí. Pero para una app pequeña o mediana es un peaje que no querías pagar — solo necesitabas un t() honesto.

Lo resolví tantas veces que lo empaqueté: dotkey-i18n, Python puro, sin dependencias. Tus traducciones son JSON que cualquiera puede editar:

// locales/es.json
{
  "login": { "welcome": "Hola, {name}", "submit": "Entrar" },
  "menu":  { "reports": "Informes", "settings": "Ajustes" }
}
from dotkey_i18n import Translator

tr = Translator("locales", default_lang="es")
tr.t("login.welcome", name="Juan")     # "Hola, Juan"
tr.t("menu.reports", lang="en")        # "Reports"

Tres detalles que marcan la diferencia

Claves con notación de punto. t("login.submit") navega el JSON anidado. Agrupas las cadenas por pantalla o módulo sin claves planas kilométricas.

Fallback al idioma por defecto. Si una clave falta en el idioma pedido, se busca en el idioma por defecto antes de rendirse. Tus traducciones pueden ir incompletas —la vida real— sin dejar huecos en blanco en la interfaz.

Nunca revienta la interfaz. Una clave que no existe devuelve la propia clave (un marcador visible, no una excepción a mitad de render). Una interpolación con un campo que falta devuelve el texto sin formatear. Un JSON corrupto se trata como vacío. Nada de esto tumba la pantalla.

Agnóstico del framework

El idioma actual entra por un lang_getter inyectable, así el mismo Translator sirve en NiceGUI, Flask, FastAPI o un script suelto:

# NiceGUI: idioma desde la sesión del usuario
tr = Translator("locales", default_lang="es",
                lang_getter=lambda: app.storage.user.get("idioma"))

# Flask
tr = Translator("locales", lang_getter=lambda: session.get("lang"))

La prioridad es clara: lang= explícito → lang_getter() → idioma por defecto.

De dónde viene

Salió del servicio i18n de un sistema de gestión de informes bilingüe. Estaba atado al framework —leía el idioma directamente de la sesión de NiceGUI—; al extraerlo se desacopló con el lang_getter, y se le añadió el fallback al idioma por defecto que el original no tenía. Extraer para publicar suele mejorar el código original: lo obliga a no depender de su casa.

Es software libre. Desde agosto de 2026 el código no se publica en registros ni en GitHub —trabajamos en seguridad y que nos extraigan el fuente sería un argumento en contra del producto—: se entrega a pedido, con firma y SHA-256, en contacto@xiliux.com.

Código y tests (notación de punto, fallback, claves ausentes, JSON corrupto; verificados por mutación): **.

Preguntas frecuentes

¿Por qué no usar gettext/Babel?

Son potentes, pero para una app pequeña o mediana traen ficheros .po, un paso de compilación a .mo y herramientas de extracción — un peaje que no querías pagar. Si solo necesitas un t() sobre unos textos, es maquinaria de más.

¿Cómo se ven las traducciones?

JSON que cualquiera puede editar, con claves anidadas por punto: t('login.welcome', name='Juan') devuelve 'Hola, Juan'. Sin compilar, sin herramientas: editas el JSON y ya. Un traductor no técnico puede tocarlo sin romper nada.

¿Soporta interpolación y fallback de idioma?

Sí: interpolas con {name} y pasas lang para elegir idioma; si una clave falta en un idioma, cae al idioma por defecto en vez de romper. Son los detalles que separan un t() de juguete de uno usable.

¿Cuándo NO usar dotkey-i18n?

Cuando ya vives en un ecosistema gettext (Django con su i18n, plurales complejos, un pipeline de traducción establecido). Es para el hueco 'app pequeña que solo quiere un t() sobre JSON', no para reemplazar toda una plataforma.

← Ver más artículosCotizar un proyecto