Quantity control layout: changed from wrapping flex to deliberate two-row
grid so the add-to-cart button sits full-width underneath the stepper,
avoiding accidental line breaks and giving the primary action more pulsing
area. Also removed the /Kg suffix's redundant font rules.
Order card delivery row: reordered to badge-then-date (visually centred),
removed the "Delivery" label, and swapped bg-primary to text-bg-primary for
proper contrast on the home-delivery badge (3.13:1 minimum).
Tooltip translations: moved hardcoded static tooltips from data-bs-title
(untranslatable) to title (QWeb-translatable). Handled the edge case of
"Save Cart" and "Back to Cart" which had translations but no model_terms
reference in the POT, causing the merge to discard them — added the view
reference so translations now apply.
Load-from-history page: added accessibility: a visible status message
("Loading your order…"), a <noscript> fallback with link to the group
order, lang and viewport meta tags, and localised strings for all three
languages. Gave the page a minimal inline style block since it does not
inherit the token system (no website.layout).
Translations: 11 new entries (es/eu/ca) for new/reworded UI strings, plus
two code catalogue entries (products found, Close) that needed the
#. odoo-python comment for _() resolution. Documented the silent-failure
pattern in docs/TRANSLATIONS.md: the POT merge, untranslatable attributes,
and missing code comments.
Tests: 246 passing. Pre-commit: clean.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
12 KiB
Sistema de Traducciones - Guía Completa
Estado Actual
✅ El sistema de traducciones está funcionando correctamente
Todos los addons custom incluyen traducciones para:
- Español (es) - Obligatorio
- Euskera (eu) - Obligatorio
- Catalán (ca), Gallego (gl), Portugués (pt), Francés (fr), Italiano (it) - Opcional
Estructura de Archivos
Cada addon debe tener la siguiente estructura:
addon_name/
├── i18n/
│ ├── es.po # Español (obligatorio)
│ ├── eu.po # Euskera (obligatorio)
│ ├── ca.po # Catalán (opcional)
│ ├── gl.po # Gallego (opcional)
│ ├── pt.po # Portugués (opcional)
│ ├── fr.po # Francés (opcional)
│ ├── it.po # Italiano (opcional)
│ └── addon_name.pot # Template (generado automáticamente)
Reglas Importantes
❌ NO Hacer
NUNCA usar _() en definiciones de campos a nivel de módulo:
# ❌ INCORRECTO - Causa warnings
from odoo import _
class MyModel(models.Model):
_name = 'my.model'
name = fields.Char(string=_("Name")) # ❌ MAL
description = fields.Text(string=_("Description")) # ❌ MAL
Por qué está mal: _() se ejecuta en tiempo de importación del módulo, antes de que el sistema de traducciones esté inicializado, causando warnings.
✅ Hacer
Usar strings literales en definiciones de campos:
# ✅ CORRECTO
from odoo import models, fields
class MyModel(models.Model):
_name = 'my.model'
name = fields.Char(string="Name") # ✅ BIEN
description = fields.Text(string="Description") # ✅ BIEN
Usar _() solo en métodos y código ejecutable:
# ✅ CORRECTO
from odoo import _, models
class MyModel(models.Model):
_name = 'my.model'
def action_confirm(self):
message = _("Order confirmed successfully") # ✅ BIEN
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'), # ✅ BIEN
'message': message,
'type': 'success',
}
}
def _compute_display_name(self):
for record in self:
record.display_name = _("Order %s") % record.name # ✅ BIEN
Cómo Generar/Actualizar Traducciones
1. Exportar Términos Traducibles
NO HACER:
# Exportar términos del addon
docker-compose exec odoo odoo \
--addons-path=/mnt/extra-addons \
--i18n-export=/tmp/addon_name.pot \
--modules=addon_name \
--db=odoo \
--stop-after-init
De alguna forma, exporta todas las cadenas de Odoo.
PEDIR AL USUARIO QUE GENERE EL POT DESDE LA UI DE ODOO
# Copiar el archivo generado
docker-compose cp odoo:/tmp/addon_name.pot ./addon_name/i18n/
El .pot no es opcional: Odoo fusiona los .po contra él
El .pot es un artefacto generado y está en .gitignore (*.pot), pero no es
prescindible: al importar, Odoo fusiona cada .po contra el .pot del módulo antes
de leerlo.
# odoo/tools/translate.py, PoFileReader.__init__
if pot_path:
self.pofile.merge(polib.pofile(pot_path))
merge() de polib sustituye las ocurrencias de cada entrada por las del POT, y
marca como obsoletas las entradas que el POT no tiene (el lector se salta las
obsoletas). Consecuencias prácticas:
- Añadir una ocurrencia
model_terms:ir.ui.view,...solo al.pono sirve de nada: el merge la descarta y la traducción nunca llega a la vista. - Una cadena nueva que no esté en el POT se ignora entera, por muy traducida que esté en los tres idiomas.
- Por eso, tras mover una cadena a un atributo traducible o añadir una nueva, hay que regenerar el POT antes de que la traducción surta efecto.
⚠️ Al exportar contra una BD con datos demo, el POT se lleva los nombres de los registros demo (productos, impuestos, nombres de pedidos). Genera el POT contra una BD limpia.
2. Actualizar Archivos .po Existentes
no usar msmerge, corrompe el po. Usa polib.
Al crear entradas con polib hay dos detalles que fallan en silencio:
entry = polib.POEntry(
msgid="products found",
msgstr="produktu aurkitu dira",
# 1. Sin `module:` el lector revienta; sin `odoo-python` la cadena se lee
# del .po y se descarta acto seguido (ver Troubleshooting).
comment="module: website_sale_aplicoop\nodoo-python",
# 2. El número de línea es obligatorio en ocurrencias `code:`.
occurrences=[("code:addons/website_sale_aplicoop/models/js_translations.py", "0")],
)
3. Traducir Términos Nuevos
Editar los archivos .po y completar las traducciones:
#: models/my_model.py:25
msgid "Order confirmed successfully"
msgstr "Pedido confirmado correctamente" # Español
#: models/my_model.py:30
msgid "Success"
msgstr "Éxito"
4. Cargar Traducciones en Odoo
# Actualizar addon con nuevas traducciones
docker-compose exec odoo odoo -d odoo -u addon_name --stop-after-init
# O reiniciar Odoo para que cargue los cambios
docker-compose restart odoo
Formato de Archivos .po
Cabecera Requerida
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * addon_name
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-12 10:00+0000\n"
"PO-Revision-Date: 2026-02-12 10:00+0000\n"
"Last-Translator: Your Name <your.email@example.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
Campos Traducibles
Odoo automáticamente detecta y exporta:
- String en definiciones de campos:
fields.Char(string="Name") - Help text:
fields.Char(help="Enter the product name") - Selection options:
fields.Selection([('draft', 'Draft'), ('done', 'Done')]) - Texto en vistas XML:
<label string="Customer Name"/> - Botones en vistas:
<button string="Confirm"/> - Mensajes en código:
_("Message text") - Excepciones:
raise ValidationError(_("Invalid value"))
Verificación
Comprobar que las Traducciones Funcionan
-
Activar el idioma en Odoo:
- Settings > Translations > Load a Translation
- Seleccionar el idioma (ej: Español)
-
Cambiar idioma de usuario:
- Settings > Users > [Usuario]
- Language: Español
-
Verificar en la interfaz:
- Abrir el módulo
- Verificar que los textos aparecen traducidos
Logs de Carga
Al actualizar el módulo, verificar en logs:
docker-compose logs odoo | grep "loading translation"
Debe mostrar:
odoo.modules.loading: loading translation file for language 'es': addon_name/i18n/es.po
odoo.modules.loading: loading translation file for language 'eu': addon_name/i18n/eu.po
Addons con Traducciones Completas
Addons Custom
| Addon | es | eu | Otros |
|---|---|---|---|
| account_invoice_triple_discount_readonly | ✅ | ✅ | - |
| product_price_category_supplier | ✅ | ✅ | - |
| product_sale_price_from_pricelist | ✅ | - | - |
| website_sale_aplicoop | ✅ | ✅ | ca, gl, pt, fr, it |
Addons OCA
Los addons OCA ya incluyen traducciones en múltiples idiomas.
Troubleshooting
Warning: "_() called at import time"
Síntoma:
WARNING: _() called at import time at module.path:line
Causa: _() usado en definición de campo a nivel de módulo
Solución: Eliminar _() de la definición del campo:
# Cambiar:
name = fields.Char(string=_("Name"))
# Por:
name = fields.Char(string="Name")
Traducciones No Aparecen
Posibles causas:
- Archivo .po mal formado: Verificar encoding UTF-8
- Módulo no actualizado:
docker-compose exec odoo odoo -d odoo -u addon_name --stop-after-init - Idioma no activado: Settings > Translations > Load Translation
- Usuario con idioma incorrecto: Verificar preferencias de usuario
- Cache: Reiniciar Odoo:
docker-compose restart odoo
La cadena SÍ está traducida en el .po y sigue saliendo en inglés
Los tres casos siguientes fallan sin dar ningún error, ni en el log ni al importar.
a) El atributo no es traducible. QWeb solo traduce los de esta lista
(odoo/tools/translate.py, TRANSLATED_ATTRS), más sus variantes t-attf-:
string help placeholder alt title label data-tooltip confirm
aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext
data-bs-title no está: un tooltip escrito ahí nunca se traduce. Usa title
(Bootstrap lo respeta igual) o pasa el texto desde el endpoint i18n. Tampoco están
aria-description ni data-bs-* en general.
b) Falta la referencia a la vista en el POT. Para que una traducción se aplique
a una vista, su entrada necesita una ocurrencia
model_terms:ir.ui.view,arch_db:modulo.xmlid en el POT (ver el apartado del
merge más arriba). Síntoma típico: dos botones contiguos, uno traducido y el otro
no, con las dos cadenas presentes en el .po. Se comprueba así:
grep -B 4 '^msgid "Save Cart"$' addon/i18n/addon.pot # ¿tiene model_terms?
c) Falta el comentario #. odoo-python. Las cadenas de _() se leen del .po
directamente (no de la BD), y CodeTranslations._load_python_translations filtra
por ese comentario:
def filter_func(row):
return row.get('value') and PYTHON_TRANSLATION_COMMENT in row['comments']
Sin él la entrada se lee y se descarta. La entrada tiene que quedar así:
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "products found"
msgstr "produktu aurkitu dira"
Y ojo con el :0 final: el lector hace int(line_number) sobre las ocurrencias
code:, así que una sin número de línea lanza ValueError y tumba la carga del
catálogo entero, no solo la de esa entrada.
Estas cadenas se cachean por proceso, así que después de tocarlas hay que
reiniciar Odoo (docker-compose restart odoo), no basta con -u.
Caracteres Especiales
Usar escape correcto en .po:
# Comillas
msgstr "Haga clic en \"Confirmar\""
# Saltos de línea
msgstr "Primera línea\n"
"Segunda línea"
# Caracteres especiales (á, é, í, ó, ú, ñ, ç)
# Usar directamente, el archivo debe ser UTF-8
msgstr "Configuración"
Herramientas Útiles
Editores de .po
- Poedit: https://poedit.net/ (GUI, recomendado)
- VS Code: Con extensión "gettext" para syntax highlighting
- Lokalize: Editor KDE para traducciones
Comandos Útiles
# Verificar formato de archivo .po
msgfmt -c -v -o /dev/null addon_name/i18n/es.po
# Estadísticas de traducción
msgfmt --statistics addon_name/i18n/es.po
# Extraer solo términos sin traducir
msgattrib --untranslated addon_name/i18n/es.po
Mejores Prácticas
- ✅ Mantener es.po y eu.po siempre actualizados
- ✅ Usar términos consistentes en todo el proyecto
- ✅ Incluir contexto en comentarios cuando sea necesario
- ✅ Verificar traducciones antes de commit
- ✅ Nunca usar
_()en definiciones de campos - ✅ Usar encoding UTF-8 en todos los archivos .po
- ✅ Generar .pot después de cambios importantes
- ✅ Documentar términos técnicos específicos del dominio
Referencias
- Odoo Translation Guidelines: https://www.odoo.com/documentation/18.0/developer/reference/frontend/translations.html
- GNU gettext Manual: https://www.gnu.org/software/gettext/manual/
- OCA Translation Guidelines: https://github.com/OCA/maintainer-tools/wiki/Translations
Última Actualización: 2026-02-12