# Part of Odoo. See LICENSE file for full copyright and licensing details. """ Wizard: Загрузка перевода из локального хранилища. Scans the module's own ``translations//*.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//*.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", }