Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
198
translation_helper/models/remote_translation_service.py
Normal file
198
translation_helper/models/remote_translation_service.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user