Files
public/translation_helper/models/remote_translation_service.py

199 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
Service for fetching translations from a remote Odoo 19 instance.
Odoo 16+ / 19 stores translations in JSONB columns, not in ir.translation.
The only reliable cross-version way to export / import translations is through
the standard PO pipeline:
Remote: base.language.export → PO bytes (base64)
Local: TranslationImporter → write JSONB values
This module automates the whole round-trip via XML-RPC.
"""
import base64
import logging
import tempfile
from xmlrpc.client import ServerProxy
from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.tools.translate import TranslationImporter
_logger = logging.getLogger(__name__)
class RemoteTranslationService(models.AbstractModel):
_name = "translation.remote.service"
_description = "Remote Translation Service"
# ------------------------------------------------------------------
# Remote connection helpers
# ------------------------------------------------------------------
@api.model
def _rpc_authenticate(self, url, db, user, password):
"""Authenticate on remote Odoo via XML-RPC. Returns uid."""
common = ServerProxy(f"{url}/xmlrpc/2/common")
try:
uid = common.authenticate(db, user, password, {})
except Exception as exc:
raise UserError(
_("Could not connect to %(url)s:\n%(err)s",
url=url, err=exc)
) from exc
if not uid:
raise UserError(
_("Authentication failed for user '%(user)s' on database '%(db)s'.",
user=user, db=db)
)
return uid
@api.model
def _rpc_object(self, url, db, uid, password):
"""Return a ServerProxy pointing at /xmlrpc/2/object."""
return ServerProxy(f"{url}/xmlrpc/2/object")
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
@api.model
def fetch_po_from_remote(self, remote_url, remote_db, remote_user,
remote_password, module_names, lang_code):
"""
Connect to remote Odoo and export PO for *module_names* / *lang_code*.
Returns
-------
bytes
Raw PO file content (decoded from base64).
"""
if not all([remote_url, remote_db, remote_user, remote_password]):
raise UserError(_("Не заполнены все поля подключения к удалённому серверу."))
if not module_names:
raise UserError(_("Выберите хотя бы один модуль."))
remote_url = remote_url.rstrip("/")
uid = self._rpc_authenticate(remote_url, remote_db,
remote_user, remote_password)
obj = self._rpc_object(remote_url, remote_db, uid, remote_password)
_logger.info(
"Remote export: modules=%s lang=%s url=%s db=%s",
module_names, lang_code, remote_url, remote_db,
)
try:
# 1. Find module ids on remote
remote_module_ids = obj.execute_kw(
remote_db, uid, remote_password,
"ir.module.module", "search",
[[("name", "in", module_names), ("state", "=", "installed")]],
)
if not remote_module_ids:
raise UserError(
_("На удалённом сервере не найдены установленные модули: %s")
% ", ".join(module_names)
)
# 2. Create export wizard on remote
wiz_id = obj.execute_kw(
remote_db, uid, remote_password,
"base.language.export", "create",
[{
"lang": lang_code,
"format": "po",
"export_type": "module",
"modules": [(6, 0, remote_module_ids)],
}],
)
# 3. Execute act_getfile — generates the PO and stores in data field
obj.execute_kw(
remote_db, uid, remote_password,
"base.language.export", "act_getfile",
[[wiz_id]],
)
# 4. Read the resulting base64 data
result = obj.execute_kw(
remote_db, uid, remote_password,
"base.language.export", "read",
[[wiz_id], ["data", "name"]],
)
if not result or not result[0].get("data"):
raise UserError(
_("Удалённый сервер вернул пустой PO-файл.")
)
po_b64 = result[0]["data"]
filename = result[0].get("name", "export.po")
po_bytes = base64.b64decode(po_b64)
_logger.info(
"Fetched PO from remote: file=%s size=%d bytes modules=%s",
filename, len(po_bytes), module_names,
)
return po_bytes, filename
except UserError:
raise
except Exception as exc:
_logger.exception("Remote PO export failed")
raise UserError(
_("Ошибка экспорта переводов с удалённого сервера:\n%s") % exc
) from exc
@api.model
def apply_po(self, po_bytes, lang_code, overwrite=True):
"""
Import PO bytes into the current database using the standard
TranslationImporter pipeline.
Returns
-------
int
Number of translation entries processed (approximate).
"""
importer = TranslationImporter(self.env.cr)
with tempfile.NamedTemporaryFile(suffix=".po", delete=True) as tmp:
tmp.write(po_bytes)
tmp.flush()
tmp.seek(0)
importer.load(tmp, "po", lang_code)
importer.save(overwrite=overwrite)
self.env.cr.precommit.run() # flush pending writes
_logger.info(
"Applied PO (%d bytes) for lang=%s overwrite=%s",
len(po_bytes), lang_code, overwrite,
)
return len(po_bytes)
@api.model
def preview_po(self, po_bytes):
"""
Parse a PO file and return a short textual preview.
"""
import polib
po = polib.pofile(po_bytes.decode("utf-8", errors="replace"))
translated = [e for e in po if e.msgstr and e.msgid]
lines = []
lines.append(_("Всего записей: %d") % len(po))
lines.append(_("Из них переведено: %d") % len(translated))
lines.append("")
lines.append("%-50s │ %s" % (_("Оригинал"), _("Перевод")))
lines.append("─" * 50 + "─┼─" + "─" * 50)
for entry in translated[:25]:
src = (entry.msgid or "")[:50]
val = (entry.msgstr or "")[:50]
lines.append(f"{src:50s} │ {val}")
if len(translated) > 25:
lines.append("")
lines.append(_("… и ещё %d записей") % (len(translated) - 25))
return "\n".join(lines)