Files
public/translation_helper/wizards/local_translation_sync_wizard.py

218 lines
7.7 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
Wizard: Загрузка перевода из локального хранилища.
Scans the module's own ``translations/<lang>/*.po`` folder, lists each found
file, lets the user drop unwanted rows, and applies the remaining PO files to
the current database using the same ``translation.remote.service.apply_po``
pipeline as the remote wizard. Dropping a row only excludes it from this run —
the file on disk is never deleted.
"""
import logging
import os
from datetime import datetime
import polib
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.modules.module import get_module_path
_logger = logging.getLogger(__name__)
class LocalTranslationSyncLine(models.TransientModel):
_name = "translation.local.sync.line"
_description = "Строка локальной загрузки перевода"
wizard_id = fields.Many2one(
"translation.local.sync.wizard",
string="Визард",
required=True,
ondelete="cascade",
)
module_name = fields.Char(
string="Модуль",
readonly=True,
)
file_path = fields.Char(
string="Путь",
readonly=True,
)
file_date = fields.Datetime(
string="Дата файла",
readonly=True,
)
class LocalTranslationSyncWizard(models.TransientModel):
_name = "translation.local.sync.wizard"
_description = "Загрузка перевода из локального хранилища"
language = fields.Many2one(
"res.lang",
string="Язык",
required=True,
default=lambda self: self.env["res.lang"].search(
[("code", "=", "ru_RU")], limit=1
),
)
line_ids = fields.One2many(
"translation.local.sync.line",
"wizard_id",
string="Файлы переводов",
)
overwrite = fields.Boolean(
string="Перезаписать существующие переводы",
default=True,
help="Заменять уже имеющиеся переводы значениями из локальных PO-файлов",
)
state = fields.Selection(
[("form", "Форма"), ("done", "Готово")],
default="form",
readonly=True,
)
result_message = fields.Text(
string="Результат",
readonly=True,
)
# ------------------------------------------------------------------
# Scan helpers
# ------------------------------------------------------------------
@api.model
def _scan_local_po(self, lang):
"""Scan ``translations/<folder>/*.po`` for *lang* and return a list of
``(0, 0, vals)`` command tuples for the one2many ``line_ids``.
The on-disk folder name is derived from the language's ``iso_code``
(falling back to the first segment of its ``code``), matching the
layout written by this module (e.g. ``ru`` for ``ru_RU``).
"""
if not lang:
return []
base = get_module_path("translation_helper", display_warning=False)
if not base:
return []
folder = lang.iso_code or (lang.code or "").split("_")[0]
if not folder:
return []
po_dir = os.path.join(base, "translations", folder)
if not os.path.isdir(po_dir):
return []
commands = []
for fname in sorted(os.listdir(po_dir)):
if not fname.endswith(".po"):
continue
full = os.path.join(po_dir, fname)
if not os.path.isfile(full):
continue
mtime = datetime.fromtimestamp(os.path.getmtime(full))
commands.append((0, 0, {
"module_name": fname[:-3],
"file_path": full,
"file_date": fields.Datetime.to_string(mtime),
}))
return commands
@api.model
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
res_lang = self.env["res.lang"].browse(defaults.get("language"))
if not res_lang:
res_lang = self.env["res.lang"].search(
[("code", "=", "ru_RU")], limit=1
)
defaults["line_ids"] = self._scan_local_po(res_lang)
defaults.setdefault("language", res_lang.id)
return defaults
@api.onchange("language")
def _onchange_language(self):
"""Rebuild the file list when the selected language changes."""
if self.language:
self.line_ids = [(5, 0, 0)] + self._scan_local_po(self.language)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_rescan(self):
"""Server-side rescan of the local folder for the chosen language."""
self.ensure_one()
self.line_ids = [(5, 0, 0)] + self._scan_local_po(self.language)
return self._reopen()
def action_sync(self):
"""Read each remaining PO file and apply it via the shared service."""
self.ensure_one()
if not self.line_ids:
raise UserError(_("Нет файлов для загрузки."))
service = self.env["translation.remote.service"]
lang_code = self.language.code
modules = []
total_entries = 0
translated_entries = 0
errors = []
for line in self.line_ids:
try:
with open(line.file_path, "rb") as fh:
po_bytes = fh.read()
service.apply_po(po_bytes, lang_code, overwrite=self.overwrite)
po = polib.pofile(po_bytes.decode("utf-8", errors="replace"))
total_entries += len(po)
translated_entries += sum(1 for e in po if e.msgstr and e.msgid)
modules.append(line.module_name)
except Exception as exc: # noqa: BLE001 - isolate per-file failures
_logger.exception(
"Local translation import failed for %s", line.file_path
)
errors.append(_("%(module)s: %(err)s",
module=line.module_name, err=exc))
msg = _(
"Загрузка завершена!\n\n"
"Файлов обработано: %(files)d\n"
"Модули: %(modules)s\n"
"Язык: %(lang)s\n"
"Переводов в файлах: %(total)d\n"
"Из них переведено: %(translated)d\n"
"Перезапись: %(overwrite)s",
files=len(modules),
modules=", ".join(modules) or _("нет"),
lang=self.language.name,
total=total_entries,
translated=translated_entries,
overwrite=_("да") if self.overwrite else _("нет"),
)
if errors:
msg += "\n\n" + _("Ошибки:") + "\n" + "\n".join(errors)
self.write({"result_message": msg, "state": "done"})
_logger.info(
"Local translation sync completed for %s (lang=%s)",
modules, lang_code,
)
return self._reopen()
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _reopen(self):
"""Return action that re-opens this wizard record."""
return {
"name": _("Загрузка перевода из локального хранилища"),
"type": "ir.actions.act_window",
"res_model": self._name,
"res_id": self.id,
"view_mode": "form",
"target": "new",
}