218 lines
7.7 KiB
Python
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",
|
|
}
|