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>
398 lines
12 KiB
Markdown
398 lines
12 KiB
Markdown
# 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:**
|
|
|
|
```python
|
|
# ❌ 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:**
|
|
|
|
```python
|
|
# ✅ 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:**
|
|
|
|
```python
|
|
# ✅ 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:
|
|
```bash
|
|
# 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.
|
|
|
|
```python
|
|
# 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 `.po` **no 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**:
|
|
|
|
```python
|
|
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:
|
|
|
|
```po
|
|
#: 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```po
|
|
# 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:
|
|
|
|
1. **String en definiciones de campos**: `fields.Char(string="Name")`
|
|
2. **Help text**: `fields.Char(help="Enter the product name")`
|
|
3. **Selection options**: `fields.Selection([('draft', 'Draft'), ('done', 'Done')])`
|
|
4. **Texto en vistas XML**: `<label string="Customer Name"/>`
|
|
5. **Botones en vistas**: `<button string="Confirm"/>`
|
|
6. **Mensajes en código**: `_("Message text")`
|
|
7. **Excepciones**: `raise ValidationError(_("Invalid value"))`
|
|
|
|
## Verificación
|
|
|
|
### Comprobar que las Traducciones Funcionan
|
|
|
|
1. **Activar el idioma en Odoo**:
|
|
- Settings > Translations > Load a Translation
|
|
- Seleccionar el idioma (ej: Español)
|
|
|
|
2. **Cambiar idioma de usuario**:
|
|
- Settings > Users > [Usuario]
|
|
- Language: Español
|
|
|
|
3. **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:
|
|
|
|
```bash
|
|
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:
|
|
```python
|
|
# Cambiar:
|
|
name = fields.Char(string=_("Name"))
|
|
# Por:
|
|
name = fields.Char(string="Name")
|
|
```
|
|
|
|
### Traducciones No Aparecen
|
|
|
|
**Posibles causas**:
|
|
|
|
1. **Archivo .po mal formado**: Verificar encoding UTF-8
|
|
2. **Módulo no actualizado**: `docker-compose exec odoo odoo -d odoo -u addon_name --stop-after-init`
|
|
3. **Idioma no activado**: Settings > Translations > Load Translation
|
|
4. **Usuario con idioma incorrecto**: Verificar preferencias de usuario
|
|
5. **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-`:
|
|
|
|
```text
|
|
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í:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```python
|
|
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í:
|
|
|
|
```po
|
|
#. 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:
|
|
|
|
```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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
1. ✅ Mantener es.po y eu.po siempre actualizados
|
|
2. ✅ Usar términos consistentes en todo el proyecto
|
|
3. ✅ Incluir contexto en comentarios cuando sea necesario
|
|
4. ✅ Verificar traducciones antes de commit
|
|
5. ✅ Nunca usar `_()` en definiciones de campos
|
|
6. ✅ Usar encoding UTF-8 en todos los archivos .po
|
|
7. ✅ Generar .pot después de cambios importantes
|
|
8. ✅ 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
|