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