Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions web_html_field_translate_dialog/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

===============================
Web HTML Field Translate Dialog
===============================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5a13aaec2fb305abd1c814fdaf78669366e648119b3e025a37fc47ebc2a5b682
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/19.0/web_html_field_translate_dialog
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-19-0/web-19-0-web_html_field_translate_dialog
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module improves the usability of translating **HTML** fields.

The standard translation dialog of an HTML field splits its content into
individual source terms (chunks) and asks the user to translate each one
separately. This is technical and hard to use for end users, especially
on rich content.

With this module, the translation button next to an HTML field opens a
dialog that shows one **full rich-text editor per installed language**.
The user reads and edits the whole translation of each language as a
single block, exactly as the field is edited in the form.

The standard term-by-term dialog is kept untouched for ``char`` and
``text`` translatable fields.

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_html_field_translate_dialog%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* ForgeFlow

Contributors
------------

- Jordi Ballester <jordi.ballester@forgeflow.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/web <https://github.com/OCA/web/tree/19.0/web_html_field_translate_dialog>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions web_html_field_translate_dialog/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
23 changes: 23 additions & 0 deletions web_html_field_translate_dialog/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "Web HTML Field Translate Dialog",
"summary": "Translate HTML fields with a per-language rich-text editor "
"instead of the technical term-by-term dialog",
"version": "19.0.1.0.0",
"category": "Web",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
"license": "AGPL-3",
"depends": ["web", "html_editor"],
"installable": True,
"auto_install": False,
"assets": {
"web.assets_backend": [
"web_html_field_translate_dialog/static/src/**/*.js",
"web_html_field_translate_dialog/static/src/**/*.xml",
"web_html_field_translate_dialog/static/src/**/*.scss",
],
"web.assets_unit_tests": [
"web_html_field_translate_dialog/static/tests/**/*.test.js",
],
},
}
1 change: 1 addition & 0 deletions web_html_field_translate_dialog/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import base
103 changes: 103 additions & 0 deletions web_html_field_translate_dialog/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright 2025 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from psycopg2.extras import Json

from odoo import models
from odoo.tools import SQL


class Base(models.AbstractModel):
_inherit = "base"

def _web_translation_langs(self):
"""Return the language codes a translation dialog should expose.

These are the installed (active) languages plus ``en_US``, which is
always the source language even when it is not active.
"""
langs = [code for code, _name in self.env["res.lang"].get_installed()]
if "en_US" not in langs:
langs.insert(0, "en_US")
return langs

def web_get_field_translations_full(self, field_name):
"""Return the *whole* field value for each language.

Unlike :meth:`~odoo.models.Model.get_field_translations`, which for
``html`` fields (whose ``translate`` attribute is a callable) splits the
content into individual source terms, this returns the full value per
language so it can be edited as a single rich-text block.

:param str field_name: name of the translatable field.
:return: list of ``{"lang": code, "value": html}`` dictionaries, one per
language.
"""
self.ensure_one()
self.check_access("read")
record = self.with_context(check_translations=True, prefetch_langs=True)
return [
{"lang": lang, "value": record.with_context(lang=lang)[field_name] or ""}
for lang in self._web_translation_langs()
]

def web_set_field_translations_full(self, field_name, translations):
"""Write the full value of ``field_name`` for each given language.

:param str field_name: name of the translatable field.
:param dict translations: mapping ``{lang: value}`` with the full value
for each language.

Each language is stored as an independent full value. We write the
``jsonb`` translations column directly (as :meth:`update_field_translations`
does for ``translate=True`` fields) instead of writing the field per
language context. The latter would, for model-term translated (``html``)
fields, re-synchronise terms and overwrite the ``en_US`` source whenever
a translation no longer shares the source structure.
"""
self.ensure_one()
field = self._fields[field_name]
if not field.translate:
return False
self.check_access("write")
self._check_field_access(field, "write")
valid_langs = set(self._web_translation_langs())

# Sanitise every value through the field itself before storing it.
values = {}
for lang, value in translations.items():
if lang not in valid_langs:
continue
record_lang = self.with_context(lang=lang)
values[lang] = field.convert_to_cache(value, record_lang) or None
if not values:
return False

# Keep a meaningful ``en_US`` fallback so the source is never lost.
fallback = (
values.get("en_US")
or self.with_context(lang="en_US")[field_name]
or next((v for v in values.values() if v is not None), None)
)
self.invalidate_recordset([field_name])
self.env.cr.execute(
SQL(
""" UPDATE %(table)s
SET %(field)s = NULLIF(
jsonb_strip_nulls(
%(fallback)s
|| COALESCE(%(field)s, '{}'::jsonb)
|| %(value)s
),
'{}'::jsonb)
WHERE id = %(id)s
""",
table=SQL.identifier(self._table),
field=SQL.identifier(field_name),
fallback=Json({"en_US": fallback}),
value=Json(values),
id=self.id,
)
)
self.modified([field_name])
return True
3 changes: 3 additions & 0 deletions web_html_field_translate_dialog/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions web_html_field_translate_dialog/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Jordi Ballester \<<jordi.ballester@forgeflow.com>\>
14 changes: 14 additions & 0 deletions web_html_field_translate_dialog/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
This module improves the usability of translating **HTML** fields.

The standard translation dialog of an HTML field splits its content into
individual source terms (chunks) and asks the user to translate each one
separately. This is technical and hard to use for end users, especially on
rich content.

With this module, the translation button next to an HTML field opens a dialog
that shows one **full rich-text editor per installed language**. The user reads
and edits the whole translation of each language as a single block, exactly as
the field is edited in the form.

The standard term-by-term dialog is kept untouched for `char` and `text`
translatable fields.
Loading
Loading