Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

3
translation_helper/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Скачанные/применённые переводы версионируем в git (решение от 2026-10).
# Ранее PO игнорировались как генерируемые в рантайме — теперь храним,
# чтобы переносить переводы вместе с кодом.

View File

@@ -0,0 +1,290 @@
=====================
Помощник по переводам
=====================
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
|badge1| |badge2|
.. warning::
Модуль находится на стадии бета-тестирования. Использование в промышленной
эксплуатации требует осторожности.
Модуль для Odoo 19 Community Edition. Помогает переводчику работать прямо в
интерфейсе Odoo: переводить поля, представления и меню «на лету», накапливать
переводы в локальных PO-файлах и переносить переводы между базами данных.
**Содержание**
.. contents::
:local:
Назначение
==========
Модуль решает две задачи:
#. **Ручной перевод** полей, представлений и меню прямо из интерфейса Odoo —
без ручного редактирования PO-файлов и без перезапуска сервера для
большинства случаев.
#. **Перенос переводов** из внешних источников: из Weblate (старая интеграция)
и — что нового — напрямую с удалённой базы Odoo по XML-RPC.
Ключевая архитектурная предпосылка: в Odoo 16+/19 модели ``ir.translation``
больше нет. Переводы хранятся в JSONB-колонках самих моделей. Поэтому перенос
переводов между базами выполняется только через штатный PO-pipeline
(export → PO → import), а не через чтение/запись ``ir.translation``. Подробности —
в разделе «Техническая архитектура».
Возможности
===========
* Ручной перевод полей через контекстное меню в UI («Режим переводчика»).
* Перевод нескольких полей одновременно.
* Поддержка переводимых полей типа ``char``, ``text`` и ``html``.
* Перевод меню и элементов представлений (метки полей, подсказки,
текст-заполнитель, метки кнопок, заголовки групп).
* Сохранение переводов в локальные PO-файлы модулей
(``translations/<lang>/<module>.po``).
* Локальные PO-оверрайды: модуль подменяет штатные PO-файлы Odoo своими файлами
из каталога ``translations/`` через монкипатчи ``odoo.tools.translate``.
* Интеграция с Weblate (старая интеграция; на момент ревью известно нерабочая,
см. соответствующий раздел).
* **Новое:** «Загрузка перевода из удалённой базы» — тянет переводы выбранных
модулей с удалённого Odoo-сервера по XML-RPC и применяет их локально.
Активация и ручной перевод из UI
=================================
#. Установите модуль.
#. Перейдите в настройки и активируйте **«Режим переводчика»** — он работает
аналогично режиму разработчика.
#. В правом верхнем углу появится дополнительная иконка. Нажмите на неё, чтобы
открыть меню доступных действий перевода.
#. У каждого пункта меню появится иконка перевода. При изменении перевода он
применяется, и страница автоматически перезагружается.
#. У каждого поля появится кнопка перевода. Можно перевести:
* название поля;
* подсказку (tooltip), которая показывается при наведении на знак вопроса.
Перевод применяется немедленно.
#. Все переводы сохраняются:
* в базу данных (в JSONB-колонки моделей);
* в ``.po``-файлы выбранного языка, с именем модуля, в котором объявлен
термин.
Например, перевод термина из модуля ``crm`` попадёт в файл::
translation_helper/translations/<код_языка>/crm.po
Локальные PO-оверрайды
======================
Модуль переопределяет внутренние точки ``odoo.tools.translate`` в ``__init__.py``
(монкипатчи ``get_po_paths`` и ``TranslationReader._push_translation``), а также
``_load_module_terms`` в ``ir.module.module``. Благодаря этому при загрузке
переводов Odoo обращается не к штатному файлу модуля, а к локальному оверрайду
модуля ``translation_helper``.
Например, при выборе русского языка система «на лету» подменяет обращение к файлу
``odoo/addons/crm/i18n/ru.po`` на ваш собственный файл
``translation_helper/translations/ru/crm.po``.
Это позволяет накапливать собственные переводы, независимые от апстрима, и
переносить их между инстансами простым копированием модуля.
.. important::
**Хрупкая точка:** монкипатчи повторяют приватную логику Odoo. При обновлении
Odoo сигнатуры этих внутренних функций могут измениться, что молча сломает
подмену переводов. Проверяйте совместимость после обновлений.
Интеграция с Weblate
====================
Модуль содержит старую интеграцию с Weblate (``models/translation_service.py``,
``wizards/translation_helper_wizard.py``, настройки Weblate в
``res.config.settings``). Она формирует ссылки на проект Weblate для выбранных
модулей и языка.
.. note::
**Статус:** на момент ревью Weblate-путь известно нерабочий (вероятные
причины — ошибка области видимости переменной при сборке ссылки, жёсткая
привязка к внешнему серверу, пустая запись ``res.lang``). Используйте с
осторожностью; основные рабочие сценарии — ручной перевод и загрузка из
удалённой базы.
Загрузка перевода из удалённой базы
===================================
Позволяет вытянуть переводы выбранных модулей с удалённого сервера Odoo и
применить их в текущей базе — без ручного экспорта/импорта PO-файлов.
Где находится
-------------
Меню: **Настройки → Переводы → «Загрузка перевода из удаленной базы»**
(пункт меню доступен только пользователям группы «Администратор настроек»,
``base.group_system``).
Поля визарда
------------
.. list-table::
:header-rows: 1
:widths: 25 75
* - Поле
- Назначение
* - ``remote_url``
- URL удалённого сервера Odoo (например, ``http://192.168.1.10:8069``).
* - ``remote_db``
- Имя базы данных на удалённом сервере.
* - ``remote_user``
- Логин на удалённом сервере (по умолчанию ``admin``).
* - ``remote_password``
- Пароль (или API-ключ) удалённого пользователя.
* - ``modules``
- Список модулей, переводы которых нужно загрузить (только установленные
на удалённой стороне).
* - ``language``
- Язык (``res.lang``), по умолчанию ``ru_RU``.
* - ``overwrite``
- Перезаписывать существующие переводы значениями из удалённой базы.
* - ``save_credentials``
- Запомнить URL, базу, логин и пароль для повторного использования.
Как пользоваться (по шагам)
---------------------------
#. Откройте **Настройки → Переводы → «Загрузка перевода из удаленной базы»**.
#. Заполните параметры подключения: ``remote_url``, ``remote_db``,
``remote_user``, ``remote_password``.
#. Выберите модули (``modules``) и язык (``language``).
#. При необходимости включите ``overwrite`` и/или ``save_credentials``.
#. Нажмите **«Просмотр»** (режим ``preview``). Модуль подключится к удалённому
серверу, выполнит экспорт PO и покажет сводку: сколько всего записей, сколько
переведено, и первые строки «оригинал → перевод». Скачанный PO кэшируется в
визарде.
#. Нажмите **«Загрузить»** (режим ``sync``). Переводы импортируются в текущую
базу через штатный ``TranslationImporter``. По завершении визард показывает
итоговое сообщение (модули, язык, число записей, режим перезаписи).
Режимы
------
* **preview** — только читает переводы с удалённой базы и показывает сводку,
ничего не меняя локально. Скачанный PO кэшируется для последующего применения.
* **sync** — применяет переводы к текущей базе. Если PO уже скачан на шаге
preview, используется кэш; иначе экспорт выполняется заново.
Что делает ``overwrite``
------------------------
* ``overwrite = True`` (по умолчанию) — существующие локальные переводы
заменяются значениями из удалённой базы.
* ``overwrite = False`` — заполняются только отсутствующие переводы, уже
имеющиеся локальные значения сохраняются.
Сохранение учётных данных
-------------------------
При включённом ``save_credentials`` параметры подключения (``remote_url``,
``remote_db``, ``remote_user``, ``remote_password``) сохраняются в
``ir.config_parameter`` и подставляются при следующем открытии визарда.
.. warning::
**Безопасность:** пароль сохраняется в ``ir.config_parameter`` в открытом
виде — он читается администраторами и попадает в дампы/бэкапы БД.
Рекомендуется использовать для удалённого доступа отдельный API-ключ (который
можно отозвать) и отключать ``save_credentials`` на боевых серверах.
Соединение идёт по указанному URL; для защиты учётных данных используйте
``https://``.
Требования
==========
* Odoo 19 Community Edition.
* Внешние Python-зависимости: ``siphashc``, ``polib``.
* Для новой фичи: сетевой доступ к XML-RPC-эндпоинтам удалённого сервера Odoo
(``/xmlrpc/2/common``, ``/xmlrpc/2/object``) и учётные данные с правами на
экспорт переводов на удалённой стороне.
Техническая архитектура
========================
Хранение переводов в Odoo 19
----------------------------
В Odoo 16+/19 модели ``ir.translation`` **не существует**. Переводы хранятся
прямо в JSONB-колонках самих моделей: значение колонки — это ``JSON``-словарь
вида ``{"en_US": "Foo", "ru_RU": "Фу"}``, где ключ — код языка. Ключ ``en_US``
используется по умолчанию. Поэтому единственный надёжный, кросс-версионный путь
переноса переводов между базами — штатный PO-pipeline, а не прямое
чтение/запись ``ir.translation``.
Поток новой фичи
----------------
Передача переводов между базами идёт только через PO:
#. **Экспорт на удалённой базе** — через штатный визард ``base.language.export``
(режим ``module``, формат ``po``).
#. **Передача** PO по XML-RPC в виде base64.
#. **Локальный импорт** — через ``TranslationImporter``, который пишет значения
в JSONB-колонки.
XML-RPC-поток
-------------
* Аутентификация: ``POST /xmlrpc/2/common`` → ``authenticate(db, user,
password)`` возвращает ``uid``.
* Вызовы моделей: ``/xmlrpc/2/object`` → ``execute_kw(db, uid, password, model,
method, args)``.
Последовательность вызовов при экспорте с удалённой стороны:
#. ``ir.module.module.search`` — найти установленные модули по именам.
#. ``base.language.export.create`` — создать визард экспорта (``lang``,
``format=po``, ``export_type=module``, список модулей).
#. ``base.language.export.act_getfile`` — сгенерировать PO и записать его в поле
``data``.
#. ``base.language.export.read(['data', 'name'])`` — прочитать base64-содержимое
PO.
Полученный PO декодируется из base64 и импортируется локально через
``TranslationImporter.load()`` + ``save(overwrite=...)`` с последующим
``cr.precommit.run()``.
Разделение по слоям
-------------------
* ``models/remote_translation_service.py`` — ``AbstractModel``
``translation.remote.service``: XML-RPC-транспорт и PO-pipeline
(``fetch_po_from_remote``, ``apply_po``, ``preview_po``).
* ``wizards/remote_translation_sync_wizard.py`` — ``TransientModel``
``translation.remote.sync.wizard``: форма, предпросмотр, применение,
сохранение учётных данных.
* ``views/remote_translation_sync_wizard_views.xml`` — форма визарда (состояния
``form → preview → done``), action и пункт меню в
``base.menu_translation_export``.
* ``security/ir.model.access.csv`` — ACL визарда для ``base.group_system``.
Авторы
======
* MK.Lab, RuOdoo
.. image:: https://ruodoo.ru/logo.png
:alt: MK.Lab, RuOdoo
:target: https://ruodoo.ru

View File

@@ -0,0 +1,80 @@
# This is monkeypatch for changing default get_po_paths function
# from odoo.tools.translate import get_po_paths
import sys
import os
import re
from contextlib import suppress
from os.path import join
from pathlib import Path
from odoo.tools.misc import file_path
dir_path = os.path.dirname(os.path.realpath(__file__))
def get_po_paths_fixed(module_name: str, lang: str):
lang_base = lang.split("_")[0]
if lang_base == "es" and lang != "es_ES":
# force es_419 as fallback language for the spanish variations
if lang == "es_419":
langs = ["es_419"]
else:
langs = ["es_419", lang]
else:
langs = [lang_base, lang]
po_paths = [
path
for lang_ in langs
for dir_ in ("i18n", "i18n_extra")
if (path := join(module_name, dir_, lang_ + ".po"))
]
po_files_dir = os.path.join(dir_path, "translations", lang_base.strip())
list_of_supported_modules = []
if os.path.exists(po_files_dir):
list_of_supported_modules = [
Path(file_name).stem for file_name in os.listdir(po_files_dir)
]
for path in po_paths:
if module_name in list_of_supported_modules:
path = join(po_files_dir, module_name + ".po")
with suppress(FileNotFoundError):
yield file_path(path)
def _push_translation_fixed(
self, module, ttype, name, res_id, source, comments=None, record_id=None, value=None
):
"""Insert a translation that will be used in the file generation
In po file will create an entry
#: <ttype>:<name>:<res_id>
#, <comment>
msgid "<source>"
record_id is the database id of the record being translated
"""
# empty and one-letter terms are ignored, they probably are not meant to be
# translated, and would be very hard to translate anyway.
sanitized_term = (source or "").strip()
# remove non-alphanumeric chars
sanitized_term = re.sub(r"\W+", "", sanitized_term)
if not sanitized_term or len(sanitized_term) < 1:
return
self._to_translate.append(
(module, source, name, res_id, ttype, tuple(comments or ()), record_id, value)
)
module = sys.modules["odoo.addons.base.models.ir_module"]
module.get_po_paths = get_po_paths_fixed
module = sys.modules["odoo.tools.translate"]
module.get_po_paths = get_po_paths_fixed
module = sys.modules["odoo.tools.translate"]
module.TranslationReader._push_translation = _push_translation_fixed
from . import models # noqa
from . import wizards # noqa

View File

@@ -0,0 +1,68 @@
{
"name": "Translation Helper",
"summary": "Ручной перевод полей и представлений Odoo и загрузка переводов с удалённой базы по XML-RPC",
"description": """
Помощник по переводам
=====================
Инструмент для ручного перевода полей, представлений и меню Odoo 19 с
сохранением в локальные PO-файлы модулей и переносом переводов между базами
данных.
Возможности:
------------
* Ручной перевод полей, представлений и меню через контекстное меню в UI
(«Режим переводчика»)
* Перевод нескольких полей одновременно
* Поддержка переводимых полей типа char, text и html
* Сохранение переводов в локальные PO-файлы модулей
(translations/<lang>/<module>.po)
* Локальные PO-оверрайды: подмена штатных PO-файлов Odoo своими
* Интеграция с Weblate (старая интеграция)
* НОВОЕ: «Загрузка перевода из удалённой базы» — тянет переводы выбранных
модулей с удалённого Odoo-сервера по XML-RPC и применяет их локально
Загрузка перевода из удалённой базы:
------------------------------------
* Меню: Настройки > Переводы > «Загрузка перевода из удаленной базы»
* Визард с параметрами подключения (URL, база, логин, пароль), выбором модулей
и языка, опциями перезаписи и сохранения учётных данных
* Режимы предпросмотра (preview) и применения (sync)
* В Odoo 16+/19 модели ir.translation нет: переводы хранятся в JSONB-колонках,
поэтому перенос между базами идёт через штатный PO-pipeline — удалённый
экспорт через base.language.export, передача PO по XML-RPC и локальный импорт
через TranslationImporter
Технические детали:
-------------------
* Расширяет модели ir.http, ir.qweb, base, ir.model.fields, ir.module.module
* JavaScript-компоненты для интеграции с UI
* Монкипатчи odoo.tools.translate для локальных PO-оверрайдов
* XML-RPC-транспорт и PO-pipeline для синхронизации переводов с удалённой базы
""",
"author": "MK.Lab, RuOdoo",
"website": "https://ruodoo.ru",
"category": "Productivity/Translations",
"version": "19.0.2026.09.17",
"depends": ["web"],
"external_dependencies": {
"python": ["siphashc", "polib"],
},
"license": "LGPL-3",
"data": [
"security/ir.model.access.csv",
"views/res_config_settings_views.xml",
"views/templates.xml",
"views/remote_translation_sync_wizard_views.xml",
"views/local_translation_sync_wizard_views.xml",
"wizards/translation_helper_wizard.xml",
],
"assets": {
"web.assets_backend": [
"translation_helper/static/src/**/*",
],
},
"installable": True,
"application": False,
"maintainers": ["mklab", "ruodoo"],
}

View File

@@ -0,0 +1,435 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * translation_helper
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-25 16:08+0000\n"
"PO-Revision-Date: 2025-05-25 16:08+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "(change)"
msgstr "(изменить)"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "(create)"
msgstr "(создать)"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/settings/res_config_dev_tool.xml:0
#, python-format
msgid "Activate the translate mode"
msgstr "Активируйте режим переводчика"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_server_address
msgid "Address"
msgstr "Адрес"
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Address of your Weblate server"
msgstr "Адрес вашего weblate сервера"
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Alias of your Weblate project"
msgstr "Алиас вашего Weblate проекта"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "All users"
msgstr "Все пользователи"
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.translation_helper_wizard_form
msgid "Apply"
msgstr "Применить"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_base
msgid "Base"
msgstr "База"
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.translation_helper_wizard_form
msgid "Cancel"
msgstr "Отменить"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.js:0
#, python-format
msgid "Choose a translate command..."
msgstr "Выберите команду перевода..."
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Close"
msgstr "Закрыть"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Condition:"
msgstr "Состояние"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_res_config_settings
msgid "Config Settings"
msgstr "Параметры конфигурации"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__create_uid
msgid "Created by"
msgstr "Создал"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__create_date
msgid "Created on"
msgstr "Создано"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Creation Date:"
msgstr "Дата создания"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Creation User:"
msgstr "Кто создал:"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/settings/res_config_dev_tool.xml:0
#, python-format
msgid "Deactivate the translate mode"
msgstr "Деактивируйте режим переводчика"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Default:"
msgstr "По-умолчанию:"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__display_name
msgid "Display Name"
msgstr "Оторажаемое имя"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_model_fields
msgid "Fields"
msgstr "Поля"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_items.js:0
#, python-format
msgid "Get View"
msgstr "Получить представление"
#. module: translation_helper
#: model:ir.model.fields.selection,name:translation_helper.selection__res_config_settings__translation_weblate_server_protocol__http
msgid "HTTP"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_http
msgid "HTTP Routing"
msgstr "Маршрутизация HTTP"
#. module: translation_helper
#: model:ir.model.fields.selection,name:translation_helper.selection__res_config_settings__translation_weblate_server_protocol__https
msgid "HTTPS"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/field_translation.xml:0
#, python-format
msgid "Help"
msgstr "Атрибут поля Help"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__id
msgid "ID"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "ID:"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__modules
msgid "In Apps"
msgstr "В приложениях"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__language_id
msgid "Language"
msgstr "Язык"
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Language of your Weblate project"
msgstr "Язык для вашего Weblate проекта"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__write_uid
msgid "Last Updated by"
msgstr "Последний раз обновил"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__write_date
msgid "Last Updated on"
msgstr "Последний раз обновлялось"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Latest Modification Date:"
msgstr "Дата последнего изменения"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Latest Modification by:"
msgstr "Кто последний изменил:"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.js:0
#, python-format
msgid "Leave the Translate Tools"
msgstr "Выйти из режима переводчика"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__weblate_link
msgid "Link to current term on Weblate server"
msgstr "Ссылка на текущйи термин в вашем Weblate сервере"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__modules
msgid "List of modules in which the translation term is defined"
msgstr "Список модулей в которых определен данный термин"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__metadata
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__metadata
msgid "Metadata"
msgstr "Метаданные"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_module_module
msgid "Module"
msgstr "Модуль"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "No Update:"
msgstr "Нельзя обновить:"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.js:0
#, python-format
msgid "No translate command found"
msgstr "Не найдено никакой команды для режима переводчика"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Only you"
msgstr "Только вы"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.xml:0
#, python-format
msgid "Open translate tools"
msgstr "Открыть инструменты переводчика"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_project_alias
msgid "Project Alias"
msgstr "Алиас проекта"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_project_language
msgid "Project Language"
msgstr "Язык проекта"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_server_protocol
msgid "Protocol"
msgstr "Протокол"
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Protocol of your Weblate server"
msgstr "Протокол вашего Weblate сервера"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_qweb
msgid "Qweb"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Save default"
msgstr "Сохранить параметры по умолчанию"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_project_language
msgid "Select your Weblate project alias"
msgstr "Выберите алиас вашего Weblate проекта"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_server_protocol
msgid "Select your Weblate server protocol"
msgstr "Выберите протокол вашего Weblate сервера"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/field_translation.xml:0
#, python-format
msgid "String"
msgstr "Строка"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__term_value
msgid "Term"
msgstr "Термин"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__term_value
msgid "Term value to translate"
msgstr "Значение терминя для перевода"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__language_id
msgid "The language for which the translation will be applied"
msgstr "Язык для которго будет прменяться перевод"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_items.js:0
#, python-format
msgid "Translate SearchView"
msgstr "Перевести SearchView"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_items.js:0
#, python-format
msgid "Translate View: "
msgstr "Перевести представление:"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.js:0
#, python-format
msgid "Translate tools..."
msgstr "Инструменты переводчика..."
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/fields/translation_dialog.js:0
#, python-format
msgid "Translate: %s"
msgstr "Перевести: %s"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__translation_value
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Translation"
msgstr "Перевод"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_translation_service
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Translation Helper"
msgstr "Помощник для переводчика"
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_translation_helper_wizard
msgid "Translation Helper Wizard"
msgstr "Помощник для переводчика"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__translation_value
msgid "Translation for term value"
msgstr "Перевод для значения термина"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/settings/res_config_dev_tool.xml:0
#, python-format
msgid "Translator Tools"
msgstr "Инструменты переводчика"
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__weblate_link
msgid "Weblate Link"
msgstr "Ссылка на Weblate"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_project_alias
msgid "Write your Weblate project alias"
msgstr "Запишите алиас вашего Weblate проекта"
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_server_address
msgid "Write your Weblate server address"
msgstr "Запишите адрес вашего Weblate сервера"
#. module: translation_helper
#. odoo-python
#: code:addons/translation_helper/models/translation_service.py:0
#, python-format
msgid "Write your translation"
msgstr "Запишите ваш перевод"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "XML ID:"
msgstr ""

View File

@@ -0,0 +1,435 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * translation_helper
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-25 16:08+0000\n"
"PO-Revision-Date: 2025-05-25 16:08+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "(change)"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "(create)"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/settings/res_config_dev_tool.xml:0
#, python-format
msgid "Activate the translate mode"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_server_address
msgid "Address"
msgstr ""
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Address of your Weblate server"
msgstr ""
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Alias of your Weblate project"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "All users"
msgstr ""
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.translation_helper_wizard_form
msgid "Apply"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_base
msgid "Base"
msgstr ""
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.translation_helper_wizard_form
msgid "Cancel"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.js:0
#, python-format
msgid "Choose a translate command..."
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Close"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Condition:"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__create_uid
msgid "Created by"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__create_date
msgid "Created on"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Creation Date:"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Creation User:"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/settings/res_config_dev_tool.xml:0
#, python-format
msgid "Deactivate the translate mode"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Default:"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__display_name
msgid "Display Name"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_model_fields
msgid "Fields"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_items.js:0
#, python-format
msgid "Get View"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields.selection,name:translation_helper.selection__res_config_settings__translation_weblate_server_protocol__http
msgid "HTTP"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_http
msgid "HTTP Routing"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields.selection,name:translation_helper.selection__res_config_settings__translation_weblate_server_protocol__https
msgid "HTTPS"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/field_translation.xml:0
#, python-format
msgid "Help"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__id
msgid "ID"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "ID:"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__modules
msgid "In Apps"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__language_id
msgid "Language"
msgstr ""
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Language of your Weblate project"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__write_uid
msgid "Last Updated by"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__write_date
msgid "Last Updated on"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Latest Modification Date:"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Latest Modification by:"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.js:0
#, python-format
msgid "Leave the Translate Tools"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__weblate_link
msgid "Link to current term on Weblate server"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__modules
msgid "List of modules in which the translation term is defined"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__metadata
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__metadata
msgid "Metadata"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_module_module
msgid "Module"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "No Update:"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.js:0
#, python-format
msgid "No translate command found"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Only you"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.xml:0
#, python-format
msgid "Open translate tools"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_project_alias
msgid "Project Alias"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_project_language
msgid "Project Language"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_res_config_settings__translation_weblate_server_protocol
msgid "Protocol"
msgstr ""
#. module: translation_helper
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Protocol of your Weblate server"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_ir_qweb
msgid "Qweb"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "Save default"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_project_language
msgid "Select your Weblate project alias"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_server_protocol
msgid "Select your Weblate server protocol"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/field_translation.xml:0
#, python-format
msgid "String"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__term_value
msgid "Term"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__term_value
msgid "Term value to translate"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__language_id
msgid "The language for which the translation will be applied"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_items.js:0
#, python-format
msgid "Translate SearchView"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_items.js:0
#, python-format
msgid "Translate View: "
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu.js:0
#, python-format
msgid "Translate tools..."
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/fields/translation_dialog.js:0
#, python-format
msgid "Translate: %s"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__translation_value
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Translation"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_translation_service
#: model_terms:ir.ui.view,arch_db:translation_helper.res_config_settings_view_form_budget
msgid "Translation Helper"
msgstr ""
#. module: translation_helper
#: model:ir.model,name:translation_helper.model_translation_helper_wizard
msgid "Translation Helper Wizard"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_translation_helper_wizard__translation_value
msgid "Translation for term value"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/settings/res_config_dev_tool.xml:0
#, python-format
msgid "Translator Tools"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,field_description:translation_helper.field_translation_helper_wizard__weblate_link
msgid "Weblate Link"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_project_alias
msgid "Write your Weblate project alias"
msgstr ""
#. module: translation_helper
#: model:ir.model.fields,help:translation_helper.field_res_config_settings__translation_weblate_server_address
msgid "Write your Weblate server address"
msgstr ""
#. module: translation_helper
#. odoo-python
#: code:addons/translation_helper/models/translation_service.py:0
#, python-format
msgid "Write your translation"
msgstr ""
#. module: translation_helper
#. odoo-javascript
#: code:addons/translation_helper/static/src/translate/translate_menu_items.xml:0
#, python-format
msgid "XML ID:"
msgstr ""

View File

@@ -0,0 +1,8 @@
from . import base
from . import translation_service
from . import remote_translation_service
from . import res_config_settings
from . import ir_module
from . import ir_model_fields
from . import ir_http
from . import ir_qweb

View File

@@ -0,0 +1,87 @@
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class Model(models.AbstractModel):
_inherit = "base"
def _get_record_modules(self):
"""Возвращает список модулей, в которых определены текущие записи."""
xml_items = (
self.env["ir.model.data"]
.sudo()
.search(
[
("res_id", "in", self.ids),
("model", "=", self._name),
]
)
)
return list({item.module for item in xml_items if item.module})
def update_field_translations(self, field_name, translations):
"""OVERRIDE
Update the values of a translated field.
:param str field_name: field name
:param dict translations: if the field has ``translate=True``, it should be a dictionary
like ``{lang: new_value}``; if ``translate`` is a callable, it should be like
``{lang: {old_term: new_term}}``
Odoo 19: calls super().update_field_translations() to preserve original behaviour,
which internally dispatches to _update_field_translations.
"""
modules = self._get_record_modules()
if not modules:
_logger.warning(
"Не удалось определить модуль для модели %s (ids: %s)",
self._name,
self.ids,
)
return super().update_field_translations(field_name, translations)
original_value = self.with_context(lang=None)[field_name]
if not original_value:
_logger.warning(
"Оригинальное значение поля '%s' пустое для модели %s (ids: %s)",
field_name,
self._name,
self.ids,
)
return super().update_field_translations(field_name, translations)
# Обновляем перевод для каждой локали
for lang, translated_value in translations.items():
if not translated_value:
_logger.info("Пропущен пустой перевод для языка: %s", lang)
continue
lang_record = (
self.env["res.lang"].sudo().search([("code", "=", lang)], limit=1)
)
iso_code = lang_record.iso_code or lang.split("_")[0]
source_value = original_value
translated_value_string = translated_value
if isinstance(translated_value, dict):
source_value = translated_value.get("source", original_value)
translated_value_string = list(translated_value.values())[0]
self.env[
"translation.helper.wizard"
].sudo().update_term_translation_in_module(
modules[0], source_value, translated_value_string, iso_code
)
_logger.info(
"Обновлен перевод для '%s' (%s -> %s) [lang: %s]",
field_name,
original_value,
translated_value,
lang,
)
# Odoo 19: use super() instead of calling _update_field_translations directly,
# as the internal method signature may have changed between versions.
return super().update_field_translations(field_name, translations)

View File

@@ -0,0 +1,41 @@
from odoo import models
from odoo.http import request
from odoo.tools.misc import str2bool
ALLOWED_TRANSLATE_MODES = ["", "1"]
class IrHttp(models.AbstractModel):
_inherit = "ir.http"
@classmethod
def _handle_translate(cls):
"""Reads ?translate= from request params and stores in session['translate'].
Only updates session if the parameter is explicitly present in the request.
"""
translate = request.httprequest.args.get("translate")
if translate is not None:
request.session["translate"] = ",".join(
mode
if mode in ALLOWED_TRANSLATE_MODES
else "1"
if str2bool(mode, mode)
else ""
for mode in (translate or "").split(",")
)
@classmethod
def _pre_dispatch(cls, rule, args):
"""OVERRIDE: calls _handle_translate after super() to store translate in session."""
res = super()._pre_dispatch(rule, args)
cls._handle_translate()
return res
def session_info(self):
"""OVERRIDE: adds translate to bundle_params in session_info."""
session_info = super().session_info()
translate = request.session.get("translate", "")
if "bundle_params" not in session_info:
session_info["bundle_params"] = {}
session_info["bundle_params"]["translate"] = translate
return session_info

View File

@@ -0,0 +1,20 @@
from odoo import api, models, tools
class IrModelFields(models.Model):
_inherit = "ir.model.fields"
@api.model
@tools.ormcache("model_name")
def get_field_string(self, model_name):
"""
Переопределение функции нам нужно для того, чтобы обойти ограничение платформы.
В нашем случае при вызове оригинальной функции перевод берется из кэша системы,
что приводи к тому, что для его появления на экране нужно перезагружать экземпляр.
Для того, чтобы обойти это мы очищаем кэш.
"""
# TODO Необходимо будет сделать сброс кэша не безусловным, а по какому либо флагу,
# например сделать режим переводчика, по аналогии с режимом разработчика
self.env.registry.clear_cache()
return super().get_field_string(model_name)

View File

@@ -0,0 +1,47 @@
import logging
import os
from odoo import api, models
from odoo.modules import get_module_path
from odoo.tools.translate import TranslationImporter
_logger = logging.getLogger(__name__)
module_lname = os.path.basename(
os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
)
class Module(models.Model):
_inherit = "ir.module.module"
@api.model
def _load_module_terms(self, modules, langs, overwrite=False):
if module_lname not in modules:
return super()._load_module_terms(modules, langs, overwrite=overwrite)
# load i18n files
translation_importer = TranslationImporter(self.env.cr, verbose=False)
for module_name in modules:
modpath = get_module_path(module_name)
if not modpath:
continue
for lang in langs:
po_paths = []
for subdir in ("i18n", "i18n_extra"):
po_path = os.path.join(modpath, subdir, "%s.po" % lang)
if os.path.exists(po_path):
po_paths.append(po_path)
for po_path in po_paths:
_logger.info(
"module %s: loading translation file %s for language %s",
module_name,
po_path,
lang,
)
translation_importer.load_file(po_path, lang)
if lang != "en_US" and not po_paths:
_logger.info(
"module %s: no translation for language %s", module_name, lang
)
translation_importer.save(overwrite=overwrite, force_overwrite=overwrite)

View File

@@ -0,0 +1,13 @@
from odoo import models
from odoo.http import request
class IrQWeb(models.AbstractModel):
_inherit = "ir.qweb"
def _prepare_environment(self, values):
result = super()._prepare_environment(values)
if not values.get("minimal_qcontext"):
translate = request.session.get("translate", "") if request else ""
values.setdefault("translate", translate)
return result

View 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)

View File

@@ -0,0 +1,33 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
translation_weblate_server_address = fields.Char(
string="Адрес",
help="Запишите адрес вашего Weblate сервера",
config_parameter="translation_helper.translation_weblate_server_address",
)
translation_weblate_server_protocol = fields.Selection(
string="Протокол",
help="Выберите протокол вашего Weblate сервера",
selection=[
("http", "HTTP"),
("https", "HTTPS"),
],
config_parameter="translation_helper.translation_weblate_server_protocol",
)
translation_weblate_project_alias = fields.Char(
string="Алиас проекта",
help="Запишите алиас вашего Weblate проекта",
config_parameter="translation_helper.translation_weblate_project_alias",
)
translation_weblate_project_language = fields.Many2one(
string="Язык проекта",
help="Выберите язык вашего алиаса проекта",
comodel_name="res.lang",
config_parameter="translation_helper.translation_weblate_project_language",
)

View File

@@ -0,0 +1,135 @@
from siphashc import siphash
from odoo import _, api, models
from odoo.exceptions import UserError
DEFAULT_WEBLATE_ADDRESS = "weblate.rudoo.ru"
DEFAULT_WEBLATE_PROTOCOL = "https"
DEFAULT_WEBLATE_PROJECT_ALIAS = "testovyj-proekt"
def raw_hash(*parts: str) -> int:
"""Calculate checksum identifying translation."""
if not parts:
data = ""
elif len(parts) == 1:
data = parts[0]
else:
data = "".join(part for part in parts)
return siphash("Weblate Sip Hash", data)
def calculate_checksum(*parts: str):
"""Calculate siphashc checksum for given strings."""
return format(raw_hash(*parts), "016x")
class TranslationHelper(models.AbstractModel):
_name = "translation.service"
_description = "Translation Helper"
@api.model
def get_action(self, data=None):
model_name = data.get("resModel")
field_name = data.get("field", {}).get("name")
# Use current user's language as the default language (Odoo 19 compatible)
default_lang = self.env.user.lang
query = """
SELECT field_description
FROM ir_model_fields
WHERE model = %s AND name = %s
"""
self.env.cr.execute(query, [model_name, field_name])
translation_field_data = self.env.cr.fetchone()
current_user_language = self.env.context.get("lang", default_lang)
translation_value = translation_field_data[0].get(current_user_language)
term_to_translate = translation_field_data[0].get(default_lang)
if not term_to_translate:
term_to_translate = data.get("label")
data["translation_field_data"] = translation_field_data[0]
field = (
self.env["ir.model.fields"]
.sudo()
.search([("model", "=", model_name), ("name", "=", field_name)])
)
if not field:
raise UserError(
_("Field '%s' not found on model '%s'.") % (field_name, model_name)
)
# field.modules may be empty or a comma-separated string in Odoo 19
if field.modules:
module_names_list = field.modules.split(", ")
else:
module_names_list = []
checksum = calculate_checksum(term_to_translate)
weblate_address = (
self.env["ir.config_parameter"]
.sudo()
.get_param(
"translation_helper.translation_weblate_server_address",
default=DEFAULT_WEBLATE_ADDRESS,
)
)
weblate_protocol = (
self.env["ir.config_parameter"]
.sudo()
.get_param(
"translation_helper.translation_weblate_server_protocol",
default=DEFAULT_WEBLATE_PROTOCOL,
)
)
weblate_project_alias = (
self.env["ir.config_parameter"]
.sudo()
.get_param(
"translation_helper.translation_weblate_project_alias",
default=DEFAULT_WEBLATE_PROJECT_ALIAS,
)
)
weblate_project_language = (
self.env["res.lang"]
.sudo()
.search(
[
("code", "=", current_user_language),
"|",
("active", "=", True),
("active", "=", False),
]
)
)
for module_name in module_names_list:
url_for_module = f"{weblate_protocol}://{weblate_address}/translate/{weblate_project_alias}/{module_name}/{weblate_project_language.iso_code}/?checksum={checksum}"
wizard_record = (
self.env["translation.helper.wizard"]
.sudo()
.create(
{
"weblate_link": url_for_module,
"term_value": term_to_translate,
"language_id": weblate_project_language.id,
"translation_value": translation_value,
"modules": field.modules,
"metadata": data,
}
)
)
window_action = {
"name": _("Write your translation"),
"target": "new",
"view_mode": "form",
"res_model": "translation.helper.wizard",
"type": "ir.actions.act_window",
"res_id": wizard_record.id,
"views": [
[
self.env.ref(
"translation_helper.translation_helper_wizard_form"
).id,
"form",
]
],
}
return window_action

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_translation_helper_wizard,translation.helper.wizard,model_translation_helper_wizard,base.group_user,1,1,1,1
access_remote_translation_sync_wizard,translation.remote.sync.wizard,model_translation_remote_sync_wizard,base.group_system,1,1,1,1
access_local_translation_sync_wizard,translation.local.sync.wizard,model_translation_local_sync_wizard,base.group_system,1,1,1,1
access_local_translation_sync_line,translation.local.sync.line,model_translation_local_sync_line,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_translation_helper_wizard translation.helper.wizard model_translation_helper_wizard base.group_user 1 1 1 1
3 access_remote_translation_sync_wizard translation.remote.sync.wizard model_translation_remote_sync_wizard base.group_system 1 1 1 1
4 access_local_translation_sync_wizard translation.local.sync.wizard model_translation_local_sync_wizard base.group_system 1 1 1 1
5 access_local_translation_sync_line translation.local.sync.line model_translation_local_sync_line base.group_system 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,13 @@
/** @odoo-module **/
import * as envModule from "@web/env";
import {patch} from "@web/core/utils/patch";
const originalMakeEnv = envModule.makeEnv;
patch(envModule, {
async makeEnv() {
const env = await originalMakeEnv(...arguments);
env.translate = odoo.translate;
return env;
},
});

View File

@@ -0,0 +1,54 @@
/** @odoo-module */
import {Dropdown} from "@web/core/dropdown/dropdown";
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
import {FormLabel} from "@web/views/form/form_label";
import {TranslationDialog} from "@web/views/fields/translation_dialog";
import {patch} from "@web/core/utils/patch";
import {browser} from "@web/core/browser/browser";
patch(FormLabel, {
components: {
...FormLabel.components,
Dropdown,
DropdownItem,
},
});
patch(FormLabel.prototype, {
setup() {
super.setup(...arguments);
this.hasTranslation = this.getHasTranslation();
},
getHasTranslation() {
return Boolean(odoo.translate);
},
async onClickTranslate(attributeName) {
const modelId = await this.env.services.orm.searchRead("ir.model", [["model", "=", this.props.record.resModel]], ["id"]);
const fieldRecordId = await this.env.services.orm.searchRead(
"ir.model.fields",
[
["model_id", "=", modelId[0].id],
["name", "=", this.props.fieldName],
],
["id"]
);
this.env.services.dialog.add(TranslationDialog, {
fieldName: attributeName,
resId: fieldRecordId[0].id,
resModel: "ir.model.fields",
userLanguageValue: "",
userUserCurrentLang: true,
isComingFromTranslationAlert: false,
onSave: async () => {
this.env.bus.trigger("CLEAR-CACHES");
// Full page reload — preserves current URL including ?translate=1
browser.location.reload();
},
});
},
});
FormLabel.template = "translation_link_generation.FormLabel";

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="translation_link_generation.FormLabel" t-inherit="web.FormLabel">
<xpath expr="//sup" position="after">
<Dropdown t-if="hasTranslation">
<button type="button" class="btn btn-link btn-primary py-0 px-1">
<i class="fa fa-language" />
</button>
<t t-set-slot="content">
<t t-if="tooltipHelp">
<DropdownItem onSelected="() => this.onClickTranslate('help')">Help</DropdownItem>
</t>
<DropdownItem onSelected="() => this.onClickTranslate('field_description')">String</DropdownItem>
</t>
</Dropdown>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,96 @@
/** @odoo-module */
import {_t, loadLanguages} from "@web/core/l10n/translation";
import {jsToPyLocale} from "@web/core/l10n/utils";
import {TranslationDialog} from "@web/views/fields/translation_dialog";
import {user} from "@web/core/user";
import {onWillStart} from "@odoo/owl";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
// Add userUserCurrentLang prop that doesn't exist in Odoo 19's TranslationDialog
TranslationDialog.props = {
...TranslationDialog.props,
userUserCurrentLang: {type: Boolean, optional: true},
};
patch(TranslationDialog.prototype, {
setup() {
this.title = _t("Translate: %s", this.props.fieldName);
this.user = user;
this.orm = useService("orm");
this.terms = [];
this.updatedTerms = {};
this.translateIsCallable = false;
onWillStart(async () => {
const allLanguages = await loadLanguages(this.orm);
const languages = this.props.userUserCurrentLang
? allLanguages.filter((l) => l[0] === jsToPyLocale(user.lang))
: allLanguages;
const [translations, context] = await this.loadTranslations(languages);
this.translateIsCallable = Boolean(context.translation_show_source);
let id = 1;
translations.forEach((t) => (t.id = id++));
this.props.isText = context.translation_type === "text";
this.props.showSource = context.translation_show_source;
this.terms = translations.map((term) => {
const relatedLanguage = languages.find((l) => l[0] === term.lang);
const termInfo = {
...term,
langName: relatedLanguage ? relatedLanguage[1] : term.lang,
value: term.value || "",
};
if (
term.lang === jsToPyLocale(user.lang) &&
!this.props.showSource &&
!this.props.isComingFromTranslationAlert &&
this.props.userLanguageValue
) {
this.updatedTerms[term.id] = this.props.userLanguageValue;
termInfo.value = this.props.userLanguageValue;
}
return termInfo;
});
this.terms.sort((a, b) => a.langName.localeCompare(b.langName));
});
},
async loadTranslations(languages) {
const langs = languages.map((l) => l[0]);
return this.orm.call(
this.props.resModel,
"get_field_translations",
[[this.props.resId], this.props.fieldName, langs]
);
},
async onSave() {
const translations = {};
this.terms.forEach((term) => {
const updatedTermValue = this.updatedTerms[term.id];
if (term.id in this.updatedTerms && term.value !== updatedTermValue) {
if (this.translateIsCallable) {
if (!translations[term.lang]) {
translations[term.lang] = {};
}
const oldTermValue = term.value || term.source;
translations[term.lang][oldTermValue] = updatedTermValue || term.source;
translations[term.lang].source = term.source;
} else {
translations[term.lang] = updatedTermValue || false;
}
}
});
await this.orm.call(
this.props.resModel,
"update_field_translations",
[[this.props.resId], this.props.fieldName, translations]
);
await this.props.onSave();
this.props.close();
},
});

View File

@@ -0,0 +1 @@
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 5h12m-6-2v2m1.0482 9.5c-1.52737-1.5822-2.76747-3.4435-3.63633-5.5m6.08813 9h7m-8.5 3 5-10 5 10m-8.2489-16c-.968 5.7702-4.68141 10.6095-9.7511 13.129" stroke="#4a5568" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -0,0 +1,53 @@
/** @odoo-module **/
// In Odoo 19, Dropdown uses an inline xml`` template (no QWeb template named web.Dropdown).
// MenuTranslationButton is injected into DropdownItem via JS patch + XML template inheritance
// of web.DropdownItem (NOT web.NavBar.DropdownItem which is unused as a component template).
// NavBar is also patched to support translate buttons on section buttons with children.
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
import {NavBar} from "@web/webclient/navbar/navbar";
import {MenuTranslationButton} from "./menu_translation_button";
import {patch} from "@web/core/utils/patch";
// Patch DropdownItem — for leaf menu items (no children)
patch(DropdownItem, {
components: {
...DropdownItem.components,
MenuTranslationButton,
},
});
patch(DropdownItem.prototype, {
setup() {
super.setup(...arguments);
this.hasTranslation = this.getHasTranslation();
},
onClick(ev) {
if (ev.target.classList.contains("translate-middle-y")) {
return;
}
super.onClick(ev);
},
getHasTranslation() {
return Boolean(odoo.translate);
},
});
// Patch NavBar — for section buttons with children (Dropdown-based)
patch(NavBar, {
components: {
...NavBar.components,
MenuTranslationButton,
},
});
patch(NavBar.prototype, {
setup() {
super.setup(...arguments);
// expose to template via __translate__ variable
this.__translate__ = Boolean(odoo.translate);
},
});

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<!-- Patch SectionsMenu: add translate button inside Dropdown-based section buttons (items with children) -->
<t t-name="translation_helper.NavBar.SectionsMenu" t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension">
<xpath expr="//button[@t-att-data-menu-xmlid]/span" position="after">
<MenuTranslationButton t-if="__translate__" menuXmlId="section.xmlid" />
</xpath>
</t>
<!-- Patch MenuSlot: add translate button after group headers (items with children inside dropdown) -->
<t t-name="translation_helper.NavBar.SectionsMenu.Dropdown.MenuSlot" t-inherit="web.NavBar.SectionsMenu.Dropdown.MenuSlot" t-inherit-mode="extension">
<xpath expr="//div[hasclass('dropdown-menu_group')]" position="after">
<MenuTranslationButton t-if="__translate__ and item.xmlid" menuXmlId="item.xmlid" />
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="translation_helper.DropdownItem" t-inherit="web.DropdownItem" t-inherit-mode="extension">
<xpath expr="//t[@t-slot='default']" position="after">
<MenuTranslationButton t-if="hasTranslation and props.attrs and props.attrs['data-menu-xmlid']" menuXmlId="props.attrs['data-menu-xmlid']" />
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,92 @@
/** @odoo-module **/
import {Component, useState} from "@odoo/owl";
import {useOwnedDialogs, useService} from "@web/core/utils/hooks";
import {TranslationDialog} from "@web/views/fields/translation_dialog";
/**
* @param {Object} env - OWL environment from the component (this.env)
* @returns {Function} openTranslationDialog
*/
export function useTranslationDialog(env) {
const addDialog = useOwnedDialogs();
async function openTranslationDialog({name, id}) {
addDialog(TranslationDialog, {
fieldName: "name",
resId: id,
resModel: "ir.ui.menu",
userLanguageValue: name || "",
isComingFromTranslationAlert: false,
userUserCurrentLang: true,
onSave: async () => {
env.bus.trigger("CLEAR-CACHES");
await env.services.action.doAction({
type: "ir.actions.client",
tag: "reload",
});
},
});
}
return openTranslationDialog;
}
export class MenuTranslationButton extends Component {
setup() {
this.orm = useService("orm");
this.state = useState({record: {}});
this.translationDialog = useTranslationDialog(this.env);
}
async onClick() {
const xmlId = this.props.menuXmlId;
const [module, name] = xmlId.split(".");
if (!module || !name) {
console.error("Неверный формат menuXmlId");
return;
}
try {
const [menuIdRecord] = await this.orm.searchRead(
"ir.model.data",
[
["model", "=", "ir.ui.menu"],
["name", "=", name],
["module", "=", module],
],
["res_id"]
);
if (!menuIdRecord) {
console.error("Меню не найдено по XML ID");
return;
}
const menuId = menuIdRecord.res_id;
const [menuRecord] = await this.orm.searchRead("ir.ui.menu", [["id", "=", menuId]], ["id", "name", "action"]);
if (!menuRecord) {
console.error("Запись меню не найдена");
return;
}
this.state.record = menuRecord;
this.translationDialog({
name: menuRecord.name,
id: menuRecord.id,
});
} catch (error) {
console.error("Ошибка при получении меню:", error);
}
}
}
MenuTranslationButton.template = "translation_helper.MenuTranslationButton";
MenuTranslationButton.props = {
menuXmlId: {
type: String,
optional: false,
},
};

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="translation_helper.MenuTranslationButton">
<i class="fa fa-language translate-middle-y" style="margin-left: 5px;" aria-hidden="true" t-on-click.prevent.stop="onClick" />
</t>
</templates>

View File

@@ -0,0 +1,14 @@
/** @odoo-module **/
import * as viewHookModule from "@web/views/view_hook";
import { patch } from "@web/core/utils/patch";
import { useComponent } from "@odoo/owl";
import { useTranslateCategory } from "@translation_helper/translate/translate_context";
const originalUseSetupView = viewHookModule.useSetupView;
patch(viewHookModule, {
useSetupView(params) {
useTranslateCategory("view", { component: useComponent() });
return originalUseSetupView(params);
},
});

View File

@@ -0,0 +1,12 @@
/** @odoo-module **/
import {registry} from "@web/core/registry";
async function handleResponseFromWizard(env) {
const actionService = env.services.action;
env.bus.trigger("CLEAR-CACHES");
await actionService.doAction({
type: "ir.actions.client",
tag: "soft_reload",
});
}
registry.category("actions").add("translation_helper.response_from_wizard", handleResponseFromWizard);

View File

@@ -0,0 +1,16 @@
/** @odoo-module */
import {ResConfigDevTool} from "@web/webclient/settings_form_view/widgets/res_config_dev_tool";
import {patch} from "@web/core/utils/patch";
import {router} from "@web/core/browser/router";
patch(ResConfigDevTool.prototype, {
setup() {
super.setup(...arguments);
this.isTranslate = Boolean(odoo.translate);
},
activateTranslate(value) {
router.pushState({translate: value}, {reload: true});
},
});

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="res_config_dev_tool" t-inherit-mode="extension">
<xpath expr="//div[@id='developer_tool']" position="after">
<div id="translator_tool">
<SettingsBlock title.translate="Translator Tools">
<Setting addLabel="false">
<a t-if="!isTranslate" class="d-block" t-on-click.prevent="() => this.activateTranslate(1)" href="#">Activate the translate mode</a>
<a t-if="isTranslate" class="d-block" t-on-click.prevent="() => this.activateTranslate(0)" href="#">Deactivate the translate mode</a>
</Setting>
</SettingsBlock>
</div>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,4 @@
/** @odoo-module **/
export * from "./translate_context";
export { TranslateMenu } from "./translate_menu";
export { TranslateMenuItems } from "./translate_menu_items";

View File

@@ -0,0 +1,6 @@
.o_dialog {
.o_translate_manager .dropdown-toggle {
padding: 0 4px;
margin: 2px 10px 2px 0;
}
}

View File

@@ -0,0 +1,57 @@
/** @odoo-module **/
import { useEffect, useEnv, useSubEnv } from "@odoo/owl";
import { registry } from "@web/core/registry";
const translateRegistry = registry.category("translate");
const translateContextSymbol = Symbol("translateContext");
class TranslateContext {
constructor(env, defaultCategories = []) {
this.env = env;
this.categories = new Map(defaultCategories.map(cat => [cat, [{}]]));
}
activateCategory(category, context) {
const contexts = this.categories.get(category) || new Set();
contexts.add(context);
this.categories.set(category, contexts);
return () => {
contexts.delete(context);
if (!contexts.size) this.categories.delete(category);
};
}
async getItems() {
return [...this.categories.entries()]
.flatMap(([category, contexts]) =>
translateRegistry
.category(category)
.getAll()
.map(factory => factory({ env: this.env, ...Object.assign({}, ...contexts) }))
)
.filter(Boolean)
.sort((a, b) => (a.sequence || 1000) - (b.sequence || 1000));
}
}
export function createTranslateContext(env, { categories = [] } = {}) {
return { [translateContextSymbol]: new TranslateContext(env, categories) };
}
export function useOwnTranslateContext({ categories = [] } = {}) {
useSubEnv(createTranslateContext(useEnv(), { categories }));
}
export function useEnvTranslateContext() {
const translateContext = useEnv()[translateContextSymbol];
if (!translateContext) throw new Error("No translate context in environment");
return translateContext;
}
export function useTranslateCategory(category, context = {}) {
const env = useEnv();
if (env.translate) {
const translateContext = useEnvTranslateContext();
useEffect(() => translateContext.activateCategory(category, context), () => []);
}
}

View File

@@ -0,0 +1,89 @@
/** @odoo-module **/
import {useEffect, useEnv, useSubEnv} from "@odoo/owl";
import {memoize} from "@web/core/utils/functions";
import {registry} from "@web/core/registry";
const translateRegistry = registry.category("translate");
const getAccessRights = memoize(async function getAccessRights(orm) {
const rightsToCheck = {
"ir.ui.view": "write",
"ir.rule": "read",
"ir.model.access": "read",
};
const proms = Object.entries(rightsToCheck).map(([model, operation]) => {
return orm.call(model, "check_access_rights", [], {
operation,
raise_exception: false,
});
});
const [canEditView, canSeeRecordRules, canSeeModelAccess] = await Promise.all(proms);
const accessRights = {canEditView, canSeeRecordRules, canSeeModelAccess};
return accessRights;
});
class TranslateContext {
constructor(env, defaultCategories) {
this.orm = env.services.orm;
this.categories = new Map(defaultCategories.map((cat) => [cat, new Set()]));
}
activateCategory(category, context) {
const contexts = this.categories.get(category) || new Set();
contexts.add(context);
this.categories.set(category, contexts);
return () => {
contexts.delete(context);
if (contexts.size === 0) {
this.categories.delete(category);
}
};
}
async getItems(env) {
const accessRights = await getAccessRights(this.orm);
return [...this.categories.entries()]
.flatMap(([category, contexts]) => {
return translateRegistry
.category(category)
.getAll()
.map((factory) => factory(Object.assign({env, accessRights}, ...contexts)));
})
.filter(Boolean)
.sort((x, y) => {
const xSeq = x.sequence || 1000;
const ySeq = y.sequence || 1000;
return xSeq - ySeq;
});
}
}
const translateContextSymbol = Symbol("translateContext");
export function createTranslateContext(env, {categories = []} = {}) {
return {[translateContextSymbol]: new TranslateContext(env, categories)};
}
export function useOwnTranslateContext({categories = []} = {}) {
useSubEnv(createTranslateContext(useEnv(), {categories}));
}
export function useEnvTranslateContext() {
const translateContext = useEnv()[translateContextSymbol];
if (!translateContext) {
throw new Error("There is no translate context available in the current environment.");
}
return translateContext;
}
export function useTranslateCategory(category, context = {}) {
const env = useEnv();
if (env.translate) {
const translateContext = useEnvTranslateContext();
useEffect(
() => translateContext.activateCategory(category, context),
() => []
);
}
}

View File

@@ -0,0 +1,82 @@
/** @odoo-module */
import {Component} from "@odoo/owl";
import {Dialog} from "@web/core/dialog/dialog";
import {_t} from "@web/core/l10n/translation";
import {editModelTranslate} from "@translation_helper/translate/translate_utils";
import {registry} from "@web/core/registry";
const translateRegistry = registry.category("translate");
function viewSeparator() {
return {type: "separator", sequence: 300};
}
// ------------------------------------------------------------------------------
// Get view
// ------------------------------------------------------------------------------
class GetViewDialog extends Component {
setup() {
this.title = _t("Get View");
}
}
GetViewDialog.template = "web.TranslateMenu.GetViewDialog";
GetViewDialog.components = {Dialog};
GetViewDialog.props = {
arch: {type: Element},
close: {type: Function},
};
// // ------------------------------------------------------------------------------
// // Edit View
// // ------------------------------------------------------------------------------
export function editView({accessRights, component, env}) {
if (!accessRights.canEditView) {
return null;
}
const {viewId, viewType: type} = component.env.config || {};
if (!type) {
return;
}
const displayName = type[0].toUpperCase() + type.slice(1);
const description = _t("Translate View: ") + displayName;
return {
type: "item",
description,
callback: () => {
editModelTranslate(env, description, "ir.ui.view", viewId, "soft_reload");
},
sequence: 350,
};
}
translateRegistry.category("view").add("editView", editView);
translateRegistry.category("view").add("viewSeparator", viewSeparator);
// ------------------------------------------------------------------------------
// Edit SearchView
// ------------------------------------------------------------------------------
export function editSearchView({accessRights, component, env}) {
if (!accessRights.canEditView) {
return null;
}
const {searchViewId} = component.props.info || {};
if (searchViewId === undefined) {
return null;
}
const description = _t("Translate SearchView");
return {
type: "item",
description,
callback: () => {
editModelTranslate(env, description, "ir.ui.view", searchViewId, "reload");
},
sequence: 350,
};
}
translateRegistry.category("view").add("editSearchView", editSearchView);

View File

@@ -0,0 +1,61 @@
/** @odoo-module **/
import {Dropdown} from "@web/core/dropdown/dropdown";
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
import {TranslateMenuBasic} from "@translation_helper/translate/translate_menu_basic";
import {_t} from "@web/core/l10n/translation";
import {useCommand} from "@web/core/commands/command_hook";
import {useEnvTranslateContext} from "./translate_context";
import {useService} from "@web/core/utils/hooks";
export class TranslateMenu extends TranslateMenuBasic {
setup() {
super.setup();
const translateContext = useEnvTranslateContext();
this.command = useService("command");
useCommand(
_t("Translate tools..."),
async () => {
const items = await translateContext.getItems(this.env);
let index = 0;
const defaultCategories = items.filter((item) => item.type === "separator").map(() => (index += 1));
const provider = {
async provide() {
const categories = [...defaultCategories];
let category = categories.shift();
const result = [];
items.forEach((item) => {
if (item.type === "item") {
result.push({
name: item.description.toString(),
action: item.callback,
category,
});
} else if (item.type === "separator") {
category = categories.shift();
}
});
return result;
},
};
const configByNamespace = {
default: {
categories: defaultCategories,
emptyMessage: _t("No translate command found"),
placeholder: _t("Choose a translate command..."),
},
};
const commandPaletteConfig = {
configByNamespace,
providers: [provider],
};
return commandPaletteConfig;
},
{
category: "translate",
}
);
}
}
TranslateMenu.components = {Dropdown, DropdownItem};
TranslateMenu.props = {};

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.TranslateMenu">
<Dropdown
class="'o_translate_manager'"
beforeOpen="getElements"
position="'bottom-end'"
menuClass="env.inDialog ? 'btn btn-link' : ''"
>
<button type="button" class="o-dropdown--narrow btn btn-link">
<i class="fa fa-language" role="img" aria-label="Open translate tools" />
</button>
<t t-set-slot="content">
<t t-foreach="elements" t-as="element" t-key="element_index">
<DropdownItem t-if="element.type == 'item'" onSelected="element.callback" t-att-attrs="element.href ? {href: element.href} : undefined">
<t t-esc="element.description" />
</DropdownItem>
<div t-if="element.type == 'separator'" role="separator" class="dropdown-divider" />
<t t-if="element.type == 'component'" t-component="element.Component" t-props="element.props" />
</t>
</t>
</Dropdown>
</t>
</templates>

View File

@@ -0,0 +1,21 @@
/** @odoo-module **/
import {Component} from "@odoo/owl";
import {Dropdown} from "@web/core/dropdown/dropdown";
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
import {useEnvTranslateContext} from "./translate_context";
export class TranslateMenuBasic extends Component {
setup() {
const translateContext = useEnvTranslateContext();
// Needs to be bound to this for use in template
this.getElements = async () => {
this.elements = await translateContext.getItems(this.env);
};
}
}
TranslateMenuBasic.components = {
Dropdown,
DropdownItem,
};
TranslateMenuBasic.template = "web.TranslateMenu";

View File

@@ -0,0 +1,21 @@
/** @odoo-module **/
import {_t} from "@web/core/l10n/translation";
import {browser} from "@web/core/browser/browser";
import {registry} from "@web/core/registry";
import {routeToUrl} from "@web/core/browser/router";
function leaveTranslateMode({env}) {
return {
type: "item",
description: _t("Leave the Translate Tools"),
callback: () => {
const route = env.services.router.current;
route.search.translate = "";
browser.location.href = browser.location.origin + routeToUrl(route);
},
sequence: 450,
};
}
registry.category("translate").category("default").add("leaveTranslateMode", leaveTranslateMode);

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.TranslateMenu.SetDefaultDialog">
<Dialog title="title">
<table style="width: 100%">
<tr>
<td>
<label for="formview_default_fields" class="oe_label oe_align_right">
Default:
</label>
</td>
<td class="oe_form_required">
<select id="formview_default_fields" class="o_input" t-model="state.fieldToSet">
<option value="" />
<option t-foreach="defaultFields" t-as="field" t-att-value="field.name" t-key="field.name">
<t t-esc="field.string" /> = <t t-esc="field.displayed" />
</option>
</select>
</td>
</tr>
<tr t-if="conditions.length">
<td>
<label for="formview_default_conditions" class="oe_label oe_align_right">
Condition:
</label>
</td>
<td>
<select id="formview_default_conditions" class="o_input" t-model="state.condition">
<option value="" />
<option t-foreach="conditions" t-as="cond" t-att-value="cond.name + '=' + cond.value" t-key="cond.name">
<t t-esc="cond.string" />=<t t-esc="cond.displayed" />
</option>
</select>
</td>
</tr>
<tr>
<td colspan="2">
<input type="radio" id="formview_default_self" value="self" name="scope" t-model="state.scope" />
<label for="formview_default_self" class="oe_label" style="display: inline;">
Only you
</label>
<br />
<input type="radio" id="formview_default_all" value="all" name="scope" t-model="state.scope" />
<label for="formview_default_all" class="oe_label" style="display: inline;">
All users
</label>
</td>
</tr>
</table>
<t t-set-slot="footer">
<button class="btn btn-secondary" t-on-click="props.close">Close</button>
<button class="btn btn-secondary" t-on-click="saveDefault">Save default</button>
</t>
</Dialog>
</t>
<t t-name="web.TranslateMenu.GetMetadataDialog">
<Dialog title="title">
<table class="table table-sm table-striped">
<tr>
<th>ID:</th>
<td><t t-esc="state.id" /></td>
</tr>
<tr>
<th>XML ID:</th>
<td>
<t t-if='state.xmlids.length > 1'>
<t t-foreach="state.xmlids" t-as="imd" t-key="imd['xmlid']">
<div
t-att-class='"p-0 " + (imd["xmlid"] === state.xmlid ? "fw-bolder " : "") + (imd["noupdate"] === true ? "fst-italic " : "")'
t-esc="imd['xmlid']"
/>
</t>
</t>
<t t-elif="state.xmlid" t-esc="state.xmlid" />
<t t-else="">
/ <a t-on-click="onClickCreateXmlid"> (create)</a>
</t>
</td>
</tr>
<tr>
<th>No Update:</th>
<td>
<t t-esc="state.noupdate" />
<t t-if="state.xmlid">
<a t-on-click="toggleNoupdate"> (change)</a>
</t>
</td>
</tr>
<tr>
<th>Creation User:</th>
<td><t t-esc="state.creator" /></td>
</tr>
<tr>
<th>Creation Date:</th>
<td><t t-esc="state.createDate" /></td>
</tr>
<tr>
<th>Latest Modification by:</th>
<td><t t-esc="state.lastModifiedBy" /></td>
</tr>
<tr>
<th>Latest Modification Date:</th>
<td><t t-esc="state.writeDate" /></td>
</tr>
</table>
</Dialog>
</t>
<t t-name="web.TranslateMenu.GetViewDialog">
<Dialog title="title">
<pre t-esc="props.arch.outerHTML" />
<t t-set-slot="footer">
<button class="btn btn-primary o-default-button" t-on-click="() => props.close()">Close</button>
</t>
</Dialog>
</t>
</templates>

View File

@@ -0,0 +1,21 @@
/** @odoo-module **/
import {TranslationDialog} from "@web/views/fields/translation_dialog";
export function editModelTranslate(env, title, model, id, reloadType) {
env.services.dialog.add(TranslationDialog, {
fieldName: "arch_db",
resId: id,
resModel: model,
userLanguageValue: env.services.user.lang,
userUserCurrentLang: true,
isComingFromTranslationAlert: false,
onSave: async () => {
env.bus.trigger("CLEAR-CACHES");
await env.services.action.doAction({
type: "ir.actions.client",
tag: reloadType,
});
},
});
}

View File

@@ -0,0 +1,23 @@
/** @odoo-module **/
import {TranslateMenu} from "@translation_helper/translate/translate_menu";
import {WebClient} from "@web/webclient/webclient";
import {patch} from "@web/core/utils/patch";
import {registry} from "@web/core/registry";
import {useOwnTranslateContext} from "@translation_helper/translate/translate_context";
patch(WebClient.prototype, {
setup() {
super.setup();
useOwnTranslateContext({categories: ["default"]});
if (this.env.translate) {
registry.category("systray").add(
"web.translate_mode_menu",
{
Component: TranslateMenu,
},
{sequence: 200}
);
}
},
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,682 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 18:36+0000\n"
"PO-Revision-Date: 2025-12-03 08:31+0000\n"
"Last-Translator: \"Anastasiia Koroleva (koan)\" <koan@odoo.com>\n"
"Language-Team: Russian <https://translate.odoo.com/projects/odoo-19/base_setup/ru/>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
"X-Generator: Weblate 5.14.3\n"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "<span class=\"fa fa-lg fa-users\" aria-label=\"Number of active users\"/>"
msgstr "<span class=\"fa fa-lg fa-users\" aria-label=\"Number of active users\"/>"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<span class=\"o_form_label\" invisible=\"active_user_count &gt; 1\">\n"
" Active User\n"
" </span>\n"
" <span class=\"o_form_label\" invisible=\"active_user_count &lt;= 1\">\n"
" Active Users\n"
" </span>"
msgstr ""
"<span class=\"o_form_label\" invisible=\"active_user_count &gt; 1\">\n"
" Активный пользователь\n"
" </span>\n"
" <span class=\"o_form_label\" invisible=\"active_user_count &lt;= 1\">\n"
" Активные пользователи\n"
" </span>"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<span class=\"o_form_label\" invisible=\"company_count &gt; 1\">\n"
" Company\n"
" </span>\n"
" <span class=\"o_form_label\" invisible=\"company_count &lt;= 1\">\n"
" Companies\n"
" </span>\n"
" <br/>"
msgstr ""
"<span class=\"o_form_label\" invisible=\"company_count &gt; 1\">\n"
" Компания\n"
" </span>\n"
" <span class=\"o_form_label\" invisible=\"company_count &lt;= 1\">\n"
" Компании\n"
" </span>\n"
" <br/>"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<span class=\"o_form_label\" invisible=\"language_count &gt; 1\">\n"
" Language\n"
" </span>\n"
" <span class=\"o_form_label\" invisible=\"language_count &lt;= 1\">\n"
" Languages\n"
" </span>"
msgstr ""
"<span class=\"o_form_label\" invisible=\"language_count &gt; 1\">\n"
" Язык\n"
" </span>\n"
" <span class=\"o_form_label\" invisible=\"language_count &lt;= 1\">\n"
" Языки\n"
" </span>"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<strong>Save</strong> this page and come back here to choose your Geo "
"Provider."
msgstr ""
"<strong>Сохраните</strong> эту страницу и вернитесь сюда, чтобы выбрать Geo "
"Provider."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<strong>Save</strong> this page and come back here to set up Cloudflare "
"turnstile."
msgstr ""
"<strong>Сохраните</strong> эту страницу и вернитесь сюда, чтобы настроить "
"Cloudflare Turnstile."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<strong>Save</strong> this page and come back here to set up Google Places "
"API key"
msgstr ""
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<strong>Save</strong> this page and come back here to set up reCaptcha."
msgstr ""
"<strong>Сохраните</strong> эту страницу и вернитесь сюда, чтобы настроить "
"reCaptcha."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"<strong>Save</strong> this page and come back here to set up the feature."
msgstr ""
"<strong>Сохраните</strong> эту страницу и вернитесь сюда, чтобы настроить "
"функцию."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "API Keys"
msgstr "Ключи API"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"API Keys allow your users to access Odoo with external tools when multi-"
"factor authentication is enabled."
msgstr ""
"Ключи API позволяют вашим пользователям получать доступ к Odoo с помощью "
"внешних инструментов, если включена многофакторная аутентификация."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "About"
msgstr "О программе"
#. module: base_setup
#. odoo-python
#: code:addons/base_setup/controllers/main.py:0
msgid "Access Denied"
msgstr "Доступ запрещен"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Add Languages"
msgstr "Добавить языки"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Add fun feedback and motivate your employees"
msgstr "Добавьте интересную обратную связь и мотивируйте своих сотрудников"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_mail_plugin
msgid "Allow integration with the mail plugins"
msgstr "Разрешить интеграцию с почтовыми плагинами"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_google_calendar
msgid "Allow the users to synchronize their calendar with Google Calendar"
msgstr ""
"Разрешить пользователям синхронизировать свой календарь с Google Calendar"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_microsoft_calendar
msgid "Allow the users to synchronize their calendar with Outlook Calendar"
msgstr ""
"Разрешить пользователям синхронизировать свой календарь с Outlook Calendar"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_base_import
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Allow users to import data from CSV/XLS/XLSX/ODS files"
msgstr ""
"Разрешить пользователям импортировать данные из файлов CSV/XLS/XLSX/ODS"
#. module: base_setup
#: model:ir.model.fields,help:base_setup.field_res_config_settings__group_multi_currency
msgid "Allows to work in a multi currency environment"
msgstr "Позволяет работать в мультивалютной среде"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Autocomplete partner addresses with Google Places"
msgstr ""
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Automatically enrich your contact base with company data"
msgstr "Автоматически дополняйте контактную базу данными о компаниях"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"By default, new users get highest access rights for all installed apps. If "
"unchecked, new users will only have basic employee access."
msgstr ""
"По умолчанию новые пользователи получают максимальные права доступа ко всем "
"установленным приложениям. Если флажок снят, новые пользователи будут иметь "
"только базовые права сотрудника."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Choose the layout of your documents"
msgstr "Выберите макет ваших документов"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_website_cf_turnstile
msgid "Cloudflare Turnstile"
msgstr "Cloudflare Turnstile (капча)"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Companies"
msgstr "Компании"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__company_id
msgid "Company"
msgstr "Компания"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__company_country_code
msgid "Company Country Code"
msgstr "Код страны компании"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__company_informations
msgid "Company Informations"
msgstr "Сведения о компании"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__company_name
msgid "Company Name"
msgstr "Название компании"
#. module: base_setup
#: model:ir.model,name:base_setup.model_res_config_settings
msgid "Config Settings"
msgstr "Настройки конфигурации"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Configure Document Layout"
msgstr "Настроить макет документа"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"Configure company rules to automatically create SO/PO when one of your "
"company sells/buys to another of your company."
msgstr ""
"Настройте правила компании для автоматического создания заказов на "
"продажу/покупку, когда одна из ваших компаний продает/покупает у другой "
"вашей компании."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Contacts"
msgstr "Контакты"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__company_country_group_codes
msgid "Country Group Codes"
msgstr ""
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"Create corresponding in/out orders and bills when you sell/buy between your "
"companies"
msgstr ""
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__report_footer
msgid "Custom Report Footer"
msgstr "Пользовательский нижний колонтитул отчета"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Default Access Rights"
msgstr "Права доступа по умолчанию"
#. module: base_setup
#. odoo-python
#: code:addons/base_setup/models/res_config_settings.py:0
msgid "Default access for new users"
msgstr ""
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_ir_http__display_name
#: model:ir.model.fields,field_description:base_setup.field_kpi_provider__display_name
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__display_name
#: model:ir.model.fields,field_description:base_setup.field_res_users__display_name
msgid "Display Name"
msgstr "Display Name"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Document Layout"
msgstr "Макет документа"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__external_report_layout_id
msgid "Document Template"
msgstr "Шаблон документа"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Edit Layout"
msgstr "Изменить макет"
#. module: base_setup
#. odoo-python
#: code:addons/base_setup/models/res_config_settings.py:0
msgid "Edit new user default group"
msgstr ""
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"Enable the profiling tool. Profiling may impact performance while being "
"active."
msgstr ""
"Включите инструмент профилирования. Профилирование может повлиять на "
"производительность во время его активности."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Find free high-resolution images from Unsplash"
msgstr "Найдите бесплатные изображения высокого разрешения на Unsplash"
#. module: base_setup
#: model:ir.model.fields,help:base_setup.field_res_config_settings__report_footer
msgid "Footer text displayed at the bottom of all reports."
msgstr "Текст нижнего колонтитула, отображаемый внизу всех отчетов."
#. module: base_setup
#: model:ir.ui.menu,name:base_setup.menu_config
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "General Settings"
msgstr "Общие настройки системы"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_base_geolocalize
msgid "GeoLocalize"
msgstr "Геолокация"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Geolocate your partners"
msgstr "Определите местоположение ваших партнеров"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Geolocation"
msgstr "Геолокация"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_google_address_autocomplete
msgid "Google Address Autocomplete"
msgstr ""
#. module: base_setup
#: model:ir.model,name:base_setup.model_ir_http
msgid "HTTP Routing"
msgstr "Маршрутизация HTTP"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_ir_http__id
#: model:ir.model.fields,field_description:base_setup.field_kpi_provider__id
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__id
#: model:ir.model.fields,field_description:base_setup.field_res_users__id
msgid "ID"
msgstr "ID"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Import & Export"
msgstr "Импорт и экспорт"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Integrate with mail client plugins"
msgstr "Интеграция с плагинами почтовых клиентов"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Integrations"
msgstr "Интеграции"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Inter-Company Transactions"
msgstr "Межфирменные операции"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__is_root_company
msgid "Is Root Company"
msgstr "Является корневой компанией"
#. module: base_setup
#: model:ir.model,name:base_setup.model_kpi_provider
msgid "KPI Provider"
msgstr "Поставщик KPI"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_auth_ldap
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "LDAP Authentication"
msgstr "Аутентификация LDAP"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Languages"
msgstr "Языки"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Layout"
msgstr "Макет"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Mail Plugin"
msgstr "Почтовый плагин"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Manage API Keys"
msgstr "Управление ключами API"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Manage Companies"
msgstr "Управление компаниями"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_account_inter_company_rules
msgid "Manage Inter Company"
msgstr "Управление межкомпанийными процессами"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Manage Languages"
msgstr "Управление языками"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Manage Users"
msgstr "Управление пользователями"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__group_multi_currency
msgid "Multi-Currencies"
msgstr "Мультивалютность"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__active_user_count
msgid "Number of Active Users"
msgstr "Количество активных пользователей"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__company_count
msgid "Number of Companies"
msgstr "Количество компаний"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__language_count
msgid "Number of Languages"
msgstr "Количество языков"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "OAuth Authentication"
msgstr "Аутентификация OAuth"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Odoo"
msgstr "Odoo"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_partner_autocomplete
msgid "Partner Autocomplete"
msgstr "Автозаполнение данных партнера"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Performance"
msgstr "Производительность"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Permissions"
msgstr "Разрешения"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_voip
msgid "Phone"
msgstr "Телефон"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Preview Document"
msgstr "Предпросмотр документа"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__profiling_enabled_until
msgid "Profiling enabled until"
msgstr "Профилирование включено до"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Progressive Web App"
msgstr "Прогрессивное веб-приложение"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Protect your forms from spam and abuse."
msgstr "Защитите свои формы от спама и злоупотреблений."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Protect your forms with CF Turnstile."
msgstr "Защитите свои формы с помощью турникета Cloudflare."
#. module: base_setup
#. odoo-javascript
#: code:addons/base_setup/static/src/views/module_views.xml:0
msgid "Reset Updating States"
msgstr ""
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_sms
msgid "SMS"
msgstr "СМС"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Send SMS"
msgstr "Отправить СМС"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Send texts to your contacts"
msgstr "Отправляйте сообщения своим контактам"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Set custom access rights for new users"
msgstr "Установите пользовательские права доступа для новых пользователей"
#. module: base_setup
#: model:ir.actions.act_window,name:base_setup.action_general_configuration
msgid "Settings"
msgstr "Настройки"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__show_effect
msgid "Show Effect"
msgstr "Показать эффект"
#. module: base_setup
#: model:ir.model.fields,help:base_setup.field_res_config_settings__company_country_code
msgid ""
"The ISO country code in two chars. \n"
"You can use this field for quick search."
msgstr ""
"Код страны ISO из двух символов.\n"
"Вы можете использовать это поле для быстрого поиска."
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"This name will be used for the application when Odoo is installed through "
"the browser."
msgstr ""
"Это имя будет использоваться для приложения при установке Odoo через "
"браузер."
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_web_unsplash
msgid "Unsplash Image Library"
msgstr "Библиотека изображений Unsplash"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Update Info"
msgstr "Обновить информацию"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Use LDAP credentials to log in"
msgstr "Используйте учетные данные LDAP для входа"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Use external accounts to log in (Google, Facebook, etc.)"
msgstr ""
"Используйте внешние учетные записи для входа (Google, Facebook и т. д.)"
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_auth_oauth
msgid "Use external authentication providers (OAuth)"
msgstr "Использовать внешние провайдеры аутентификации (OAuth)"
#. module: base_setup
#: model:ir.model,name:base_setup.model_res_users
msgid "User"
msgstr "Пользователь"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid "Users"
msgstr "Пользователи"
#. module: base_setup
#. odoo-python
#: code:addons/base_setup/models/res_config_settings.py:0
msgid "VAT"
msgstr "НДС"
#. module: base_setup
#: model_terms:ir.ui.view,arch_db:base_setup.res_config_settings_view_form
msgid ""
"When populating your address book, Odoo provides a list of matching "
"companies. When selecting one item, the company data and logo are auto-"
"filled."
msgstr ""
"При заполнении адресной книги Odoo предоставляет список подходящих компаний."
" При выборе одного элемента данные компании и логотип заполняются "
"автоматически."
#. module: base_setup
#. odoo-python
#: code:addons/base_setup/models/res_users.py:0
msgid "You have to install the Discuss application to use this feature."
msgstr ""
"Для использования этой функции необходимо установить модуль Обсуждения."
#. module: base_setup
#: model:ir.model.fields,field_description:base_setup.field_res_config_settings__module_google_recaptcha
msgid "reCAPTCHA"
msgstr "reCAPTCHA"
#. module: base_setup
msgid ""
"Automatically generate counterpart documents for orders/invoices between "
"companies"
msgstr ""
"Автоматически генерировать контрагентские документы для заказов/инвойсов "
"между компаниями"
#. module: base_setup
msgid ""
"By default, new users get highest access rights for all installed apps."
msgstr ""
"По умолчанию новые пользователи получают высокие права доступа для всех "
"установленных приложений."
#. module: base_setup
msgid "Default User Template not found."
msgstr "Шаблон пользователя по умолчанию не найден."
#. module: base_setup
msgid "Documentation"
msgstr "Документация"
#. module: base_setup
msgid "Geo Localization"
msgstr "Геолокация"
#. module: base_setup
msgid "GeoLocalize your partners"
msgstr "Геолокализация ваших партнеров"
#. module: base_setup
msgid "Get product pictures using barcode"
msgstr "Получение изображений товаров с помощью штрихкода"
#. module: base_setup
msgid "VoIP"
msgstr "VoIP"

View File

@@ -0,0 +1,109 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-25 18:36+0000\n"
"PO-Revision-Date: 2025-11-16 15:08+0000\n"
"Last-Translator: Weblate <noreply-mt-weblate@weblate.org>\n"
"Language-Team: Russian <https://translate.odoo.com/projects/odoo-19/contacts/ru/>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
"X-Generator: Weblate 5.12.2\n"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_action_res_partner_bank_form
#: model:ir.ui.menu,name:contacts.menu_config_bank_accounts
msgid "Bank Accounts"
msgstr "Банковские счета"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_action_res_bank_form
msgid "Banks"
msgstr "Банки"
#. module: contacts
#: model:ir.ui.menu,name:contacts.res_partner_menu_config
msgid "Configuration"
msgstr "Конфигурация"
#. module: contacts
#: model:ir.model,name:contacts.model_res_partner
msgid "Contact"
msgstr "Контакты"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_partner_category_form
msgid "Contact Tags"
msgstr "Теги контакта"
#. module: contacts
#: model:ir.actions.act_window,name:contacts.action_contacts
#: model:ir.ui.menu,name:contacts.menu_contacts
#: model:ir.ui.menu,name:contacts.res_partner_menu_contacts
msgid "Contacts"
msgstr "Контакт"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_country_partner
msgid "Countries"
msgstr "Страны"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_country_group
msgid "Country Group"
msgstr "Группы стран"
#. module: contacts
#: model_terms:ir.actions.act_window,help:contacts.action_contacts
msgid "Create a Contact in your address book"
msgstr "Создать контакт в адресной книге"
#. module: contacts
#: model:ir.model,website_form_label:contacts.model_res_partner
msgid "Create a Customer"
msgstr "Создать клиента"
#. module: contacts
#: model:ir.model.fields,field_description:contacts.field_res_partner__display_name
#: model:ir.model.fields,field_description:contacts.field_res_users__display_name
msgid "Display Name"
msgstr "Display Name"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_country_state_partner
msgid "Fed. States"
msgstr "Регионы"
#. module: contacts
#: model:ir.model.fields,field_description:contacts.field_res_partner__id
#: model:ir.model.fields,field_description:contacts.field_res_users__id
msgid "ID"
msgstr "ID"
#. module: contacts
#: model:ir.ui.menu,name:contacts.res_partner_industry_menu
msgid "Industries"
msgstr "Профессия"
#. module: contacts
#: model:ir.ui.menu,name:contacts.menu_localisation
msgid "Localization"
msgstr "Локализация"
#. module: contacts
#: model_terms:ir.actions.act_window,help:contacts.action_contacts
msgid "Odoo helps you track all activities related to your contacts."
msgstr "Помогает отслеживать все действия, связанные с вашими контактами."
#. module: contacts
#: model:ir.model,name:contacts.model_res_users
msgid "User"
msgstr "Пользователь"
#. module: contacts
msgid "Contact Titles"
msgstr "Названия контактов"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
Purchase Order Заказ поставщику.
Stock Picking Перемещение товаров
Bill Входящая счёт-фактура
Sell Order Коммерческое преложение
Invoice Исходящая счёт-фактура
PoS Order Кассовый чек

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,547 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * uom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-21 19:52+0000\n"
"PO-Revision-Date: 2024-04-21 19:52+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.product_uom_form_view
msgid ""
"<span class=\"oe_grey oe_inline\">\n"
" e.g: 1*(reference unit)=ratio*(this unit)\n"
" </span>"
msgstr ""
"<span class=\"oe_grey oe_inline\">\n"
" например: 1*(базовая единица)=коэффициент*(данная единица)\n"
" </span>"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.product_uom_form_view
msgid ""
"<span class=\"oe_grey oe_inline\">\n"
" e.g: 1*(this unit)=ratio*(reference unit)\n"
" </span>"
msgstr ""
"<span class=\"oe_grey oe_inline\">\n"
" например: 1*(текущая единица измерения)=коэффициент*(базовая единица измерения)\n"
" </span>"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__active
msgid "Active"
msgstr "Активный"
#. module: uom
#: model_terms:ir.actions.act_window,help:uom.product_uom_form_action
msgid "Add a new unit of measure"
msgstr "Добавить новую единицу измерения"
#. module: uom
#: model_terms:ir.actions.act_window,help:uom.product_uom_categ_form_action
msgid "Add a new unit of measure category"
msgstr "Добавьте новую категорию единиц измерения"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.uom_uom_view_search
msgid "Archived"
msgstr "Архивировано"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__factor_inv
msgid "Bigger Ratio"
msgstr "Коэффициент Больше"
#. module: uom
#: model:ir.model.fields.selection,name:uom.selection__uom_uom__uom_type__bigger
msgid "Bigger than the reference Unit of Measure"
msgstr "Больше, чем базовая Единица Измерения"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__category_id
#: model_terms:ir.ui.view,arch_db:uom.uom_uom_view_search
msgid "Category"
msgstr "Категория"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__color
msgid "Color"
msgstr "Цвет"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__ratio
msgid "Combined Ratio"
msgstr "Комбинированный коэффициент"
#. module: uom
#: model:ir.model.fields,help:uom.field_uom_uom__category_id
msgid ""
"Conversion between Units of Measure can only occur if they belong to the "
"same category. The conversion will be made based on the ratios."
msgstr ""
"Пересчет между единицами измерения может произойти только в том случае, если"
" они принадлежат к одной и той же категории. Пересчет будет осуществляться "
"на основе коэффициентов."
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__create_uid
#: model:ir.model.fields,field_description:uom.field_uom_uom__create_uid
msgid "Created by"
msgstr "Создано"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__create_date
#: model:ir.model.fields,field_description:uom.field_uom_uom__create_date
msgid "Created on"
msgstr "Создано"
#. module: uom
#: model:uom.uom,name:uom.product_uom_day
msgid "Days"
msgstr "Дней"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__display_name
#: model:ir.model.fields,field_description:uom.field_uom_uom__display_name
msgid "Display Name"
msgstr "Отображаемое имя"
#. module: uom
#: model:uom.uom,name:uom.product_uom_dozen
msgid "Dozens"
msgstr "Дюжины"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.uom_uom_view_search
msgid "Group By"
msgstr "Группировать по"
#. module: uom
#: model:uom.uom,name:uom.product_uom_hour
msgid "Hours"
msgstr "Часов"
#. module: uom
#: model:ir.model.fields,help:uom.field_uom_uom__factor_inv
msgid ""
"How many times this Unit of Measure is bigger than the reference Unit of "
"Measure in this category: 1 * (this unit) = ratio * (reference unit)"
msgstr ""
"Во сколько раз эта единица измерения больше, чем базовая единица измерения в"
" этой категории: 1 * (текущая единица) = коэффициент * (базовая единица)"
#. module: uom
#: model:ir.model.fields,help:uom.field_uom_uom__factor
msgid ""
"How much bigger or smaller this unit is compared to the reference Unit of "
"Measure for this category: 1 * (reference unit) = ratio * (this unit)"
msgstr ""
"Насколько больше или меньше эта единица сравнивается с базовой единицей "
"измерения для этой категории: 1 * (базовая единица) = коэффициент * (текущая"
" единица)"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__id
#: model:ir.model.fields,field_description:uom.field_uom_uom__id
msgid "ID"
msgstr ""
#. module: uom
#: model:uom.uom,name:uom.product_uom_litre
msgid "L"
msgstr "литр"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__write_uid
#: model:ir.model.fields,field_description:uom.field_uom_uom__write_uid
msgid "Last Updated by"
msgstr "Последнее обновление"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__write_date
#: model:ir.model.fields,field_description:uom.field_uom_uom__write_date
msgid "Last Updated on"
msgstr "Последнее обновление"
#. module: uom
#: model:uom.category,name:uom.uom_categ_length
msgid "Length / Distance"
msgstr "Длина / Расстояние"
#. module: uom
#: model:res.groups,name:uom.group_uom
msgid "Manage Multiple Units of Measure"
msgstr "Управление несколькими единицами измерения"
#. module: uom
#: model:ir.model,name:uom.model_uom_uom
msgid "Product Unit of Measure"
msgstr "Единица Измерения продукта"
#. module: uom
#: model:ir.model,name:uom.model_uom_category
msgid "Product UoM Categories"
msgstr "Категории Единиц Измерения продукта"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__factor
#: model_terms:ir.ui.view,arch_db:uom.product_uom_categ_form_view
msgid "Ratio"
msgstr "Коэффициент"
#. module: uom
#: model:ir.model.fields.selection,name:uom.selection__uom_uom__uom_type__reference
msgid "Reference Unit of Measure for this category"
msgstr "Базовая единица измерения для данной категории"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__reference_uom_id
msgid "Reference UoM"
msgstr "Базовая Единица Измерения"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__rounding
msgid "Rounding Precision"
msgstr "Точность Округления"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.uom_uom_view_search
msgid "Search UOM"
msgstr "Поиск Единицы Измерения"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.uom_categ_view_search
msgid "Search UoM Category"
msgstr "Поиск категории Единиц Измерения"
#. module: uom
#: model:ir.model.fields.selection,name:uom.selection__uom_uom__uom_type__smaller
msgid "Smaller than the reference Unit of Measure"
msgstr "Меньше, чем базовая Единица Измерения"
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid ""
"Some critical fields have been modified on %s.\n"
"Note that existing data WON'T be updated by this change.\n"
"\n"
"As units of measure impact the whole system, this may cause critical issues.\n"
"E.g. modifying the rounding could disturb your inventory balance.\n"
"\n"
"Therefore, changing core units of measure in a running database is not recommended."
msgstr ""
"Некоторые критически важные поля были изменены на %s.\n"
"Обратите внимание, что ранее созданные данные не будут обновлены этим изменением.\n"
"\n"
"Поскольку единицы измерения влияют на всю систему, это может привести к критическим проблемам.\n"
"Например, изменение округления может нарушить баланс складских запасов.\n"
"\n"
"Поэтому не рекомендуется изменять основные единицы измерения в работающей базе данных."
#. module: uom
#: model:uom.category,name:uom.uom_categ_surface
msgid "Surface"
msgstr "Площадь"
#. module: uom
#: model:ir.model.fields,help:uom.field_uom_uom__rounding
msgid ""
"The computed quantity will be a multiple of this value. Use 1.0 for a Unit "
"of Measure that cannot be further split, such as a piece."
msgstr ""
"Вычисленное количество будет кратно этому значению. Используйте 1,0 для "
"единиц измерения, которые нельзя разделить на части, например, для таких как"
" штука."
#. module: uom
#: model:ir.model.constraint,message:uom.constraint_uom_uom_factor_gt_zero
msgid "The conversion ratio for a unit of measure cannot be 0!"
msgstr "Коэффициент пересчета для единицы измерения не может быть равен 0!"
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid ""
"The following units of measure are used by the system and cannot be deleted: %s\n"
"You can archive them instead."
msgstr ""
"Следующие единицы измерения используются системой и не могут быть удалены: %s\n"
"Вместо этого их можно заархивировать."
#. module: uom
#: model:ir.model.constraint,message:uom.constraint_uom_uom_factor_reference_is_one
msgid "The reference unit must have a conversion factor equal to 1."
msgstr "Базовая единица должна иметь коэффициент пересчета, равный 1."
#. module: uom
#: model:ir.model.constraint,message:uom.constraint_uom_uom_rounding_gt_zero
msgid "The rounding precision must be strictly positive."
msgstr "Точность округления должна быть строго положительной."
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid ""
"The unit of measure %s defined on the order line doesn't belong to the same "
"category as the unit of measure %s defined on the product. Please correct "
"the unit of measure defined on the order line or on the product, they should"
" belong to the same category."
msgstr ""
"Единица измерения %s, определенная в строке заказа, не принадлежит к той же "
"категории, что и единица измерения %s, определенная в товаре. Пожалуйста, "
"исправьте единицу измерения, определенную в строке заказа или в товаре, они "
"должны относиться к одной и той же категории."
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid "The value of ratio could not be Zero"
msgstr "Значение коэффициента не может равно нулю"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__uom_type
msgid "Type"
msgstr "Тип"
#. module: uom
#: model:ir.model.fields,help:uom.field_uom_uom__active
msgid ""
"Uncheck the active field to disable a unit of measure without deleting it."
msgstr ""
"Снимите флажок поля 'Активно', чтобы отключить единицу измерения, не удаляя "
"ее."
#. module: uom
#: model:uom.category,name:uom.product_uom_categ_unit
msgid "Unit"
msgstr "Штука"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_uom__name
msgid "Unit of Measure"
msgstr "Единица Измерения"
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__name
msgid "Unit of Measure Category"
msgstr "Категория Единиц Измерения"
#. module: uom
#: model:uom.uom,name:uom.product_uom_unit
msgid "Units"
msgstr "шт."
#. module: uom
#: model:ir.actions.act_window,name:uom.product_uom_form_action
#: model_terms:ir.ui.view,arch_db:uom.product_uom_categ_form_view
#: model_terms:ir.ui.view,arch_db:uom.product_uom_form_view
#: model_terms:ir.ui.view,arch_db:uom.product_uom_tree_view
msgid "Units of Measure"
msgstr "Единицы Измерения"
#. module: uom
#: model:ir.actions.act_window,name:uom.product_uom_categ_form_action
msgid "Units of Measure Categories"
msgstr "Категории Единиц Измерения"
#. module: uom
#: model_terms:ir.ui.view,arch_db:uom.product_uom_categ_form_view
#: model_terms:ir.ui.view,arch_db:uom.product_uom_categ_tree_view
msgid "Units of Measure categories"
msgstr "Категории единиц измерения"
#. module: uom
#: model_terms:ir.actions.act_window,help:uom.product_uom_categ_form_action
msgid ""
"Units of measure belonging to the same category can be\n"
" converted between each others. For example, in the category\n"
" <i>'Time'</i>, you will have the following units of measure:\n"
" Hours, Days."
msgstr ""
"Единицы измерения, относящиеся к одной и той же категории, могут\n"
"             конвертироваться друг в друга. Например, в категории\n"
"             <i>'Рабочее Время' i&gt;, у вас будут следующие единицы измерения:\n"
"             Часы, Дни.\n"
" </i>"
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid "UoM category %s must have at least one reference unit of measure."
msgstr ""
"Категория Единицы Измерения %s должна иметь хотя бы одну базовую единицу "
"измерения."
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid "UoM category %s should have a reference unit of measure."
msgstr ""
"Категория Единицы Измерения %s должна иметь базовую единицу измерения."
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid "UoM category %s should only have one reference unit of measure."
msgstr ""
"Категория Единицы Измерения %s может иметь только одну базовую единицу "
"измерения."
#. module: uom
#: model:ir.model.fields,field_description:uom.field_uom_category__uom_ids
msgid "Uom"
msgstr "Ед. Изм."
#. module: uom
#: model:uom.category,name:uom.product_uom_categ_vol
msgid "Volume"
msgstr "Объем"
#. module: uom
#. odoo-python
#: code:addons/uom/models/uom_uom.py:0
#, python-format
msgid "Warning for %s"
msgstr "Предупреждение для %s"
#. module: uom
#: model:uom.category,name:uom.product_uom_categ_kgm
msgid "Weight"
msgstr "Вес"
#. module: uom
#: model:uom.category,name:uom.uom_categ_wtime
msgid "Working Time"
msgstr "Рабочее время"
#. module: uom
#: model_terms:ir.actions.act_window,help:uom.product_uom_form_action
msgid ""
"You must define a conversion rate between several Units of\n"
" Measure within the same category."
msgstr ""
"Вы должны определить коэффициент конверсии между несколькими Единицами\n"
"             Измерения в той же категории."
#. module: uom
#: model:uom.uom,name:uom.product_uom_cm
msgid "cm"
msgstr "см"
#. module: uom
#: model:uom.uom,name:uom.product_uom_floz
msgid "fl oz (US)"
msgstr "жидкая унция (США)"
#. module: uom
#: model:uom.uom,name:uom.product_uom_foot
msgid "ft"
msgstr "фут"
#. module: uom
#: model:uom.uom,name:uom.uom_square_foot
msgid "ft²"
msgstr "фт²"
#. module: uom
#: model:uom.uom,name:uom.product_uom_cubic_foot
msgid "ft³"
msgstr "фут³"
#. module: uom
#: model:uom.uom,name:uom.product_uom_gram
msgid "g"
msgstr ""
#. module: uom
#: model:uom.uom,name:uom.product_uom_gal
msgid "gal (US)"
msgstr "галлон (США)"
#. module: uom
#: model:uom.uom,name:uom.product_uom_inch
msgid "in"
msgstr "дюйм"
#. module: uom
#: model:uom.uom,name:uom.product_uom_cubic_inch
msgid "in³"
msgstr "дюйм³"
#. module: uom
#: model:uom.uom,name:uom.product_uom_kgm
msgid "kg"
msgstr "кг"
#. module: uom
#: model:uom.uom,name:uom.product_uom_km
msgid "km"
msgstr "км"
#. module: uom
#: model:uom.uom,name:uom.product_uom_lb
msgid "lb"
msgstr "фунт"
#. module: uom
#: model:uom.uom,name:uom.product_uom_meter
msgid "m"
msgstr "м"
#. module: uom
#: model:uom.uom,name:uom.product_uom_mile
msgid "mi"
msgstr "миля"
#. module: uom
#: model:uom.uom,name:uom.product_uom_millimeter
msgid "mm"
msgstr "мм"
#. module: uom
#: model:uom.uom,name:uom.uom_square_meter
msgid "m²"
msgstr "м²"
#. module: uom
#: model:uom.uom,name:uom.product_uom_cubic_meter
msgid "m³"
msgstr "м³"
#. module: uom
#: model:uom.uom,name:uom.product_uom_oz
msgid "oz"
msgstr "унция (oz)"
#. module: uom
#: model:uom.uom,name:uom.product_uom_qt
msgid "qt (US)"
msgstr "кварта (США)"
#. module: uom
#: model:uom.uom,name:uom.product_uom_ton
msgid "t"
msgstr ""
#. module: uom
#: model:uom.uom,name:uom.product_uom_yard
msgid "yd"
msgstr "ярд"

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Action to open the wizard (must be declared before the view that references it) -->
<record id="action_local_translation_sync_wizard" model="ir.actions.act_window">
<field name="name">Загрузка перевода из локального хранилища</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">translation.local.sync.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="help">Загрузить переводы модулей из локальных PO-файлов модуля</field>
</record>
<!-- Form view for Local Translation Sync Wizard -->
<record id="local_translation_sync_wizard_form" model="ir.ui.view">
<field name="name">translation.local.sync.wizard.form</field>
<field name="model">translation.local.sync.wizard</field>
<field name="arch" type="xml">
<form>
<field name="state" invisible="1"/>
<!-- FORM STATE: list of found local PO files -->
<div invisible="state != 'form'">
<div class="alert alert-info" role="alert">
<p>Найденные локальные PO-файлы перечислены ниже.
Удалите крестиком те, что загружать не нужно, затем
нажмите «Загрузить». Удаление строки не удаляет файл с диска.</p>
</div>
<group>
<field name="language" required="1"/>
<field name="overwrite"/>
</group>
<field name="line_ids" nolabel="1">
<list editable="bottom" create="false">
<field name="module_name"/>
<field name="file_date"/>
<field name="file_path" optional="hide"/>
</list>
</field>
</div>
<!-- DONE STATE: results -->
<div invisible="state != 'done'">
<div class="alert alert-success" role="status">
<p>Загрузка завершена!</p>
</div>
<field name="result_message" nolabel="1" readonly="1"
widget="text"/>
</div>
<footer>
<!-- form state -->
<button name="action_sync" type="object"
string="Загрузить" class="btn btn-success"
invisible="state != 'form'"/>
<button name="action_rescan" type="object"
string="Обновить список" class="btn btn-secondary"
invisible="state != 'form'"/>
<!-- done state -->
<button string="Закрыть" class="btn btn-primary"
special="cancel" invisible="state != 'done'"/>
<!-- cancel for form -->
<button string="Отмена" class="btn btn-secondary"
special="cancel" invisible="state == 'done'"/>
</footer>
</form>
</field>
</record>
<!-- Menu item under Settings > Translations > Import / Export -->
<menuitem id="menu_local_translation_sync"
name="Загрузка перевода из локального хранилища"
action="action_local_translation_sync_wizard"
parent="base.menu_translation_export"
sequence="31"
groups="base.group_system"/>
</data>
</odoo>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Action to open the wizard (must be declared before the view that references it) -->
<record id="action_remote_translation_sync_wizard" model="ir.actions.act_window">
<field name="name">Загрузка перевода из удаленной базы</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">translation.remote.sync.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="help">Загрузить переводы модулей с удалённого сервера Odoo</field>
</record>
<!-- Form view for Remote Translation Sync Wizard -->
<record id="remote_translation_sync_wizard_form" model="ir.ui.view">
<field name="name">translation.remote.sync.wizard.form</field>
<field name="model">translation.remote.sync.wizard</field>
<field name="arch" type="xml">
<form>
<field name="state" invisible="1"/>
<!-- FORM STATE: Initial form to enter connection parameters -->
<div invisible="state != 'form'">
<div class="alert alert-info" role="alert">
<p>Укажите параметры подключения к удалённому Odoo серверу,
выберите модули и язык для синхронизации переводов.</p>
</div>
<group>
<group string="Подключение к удалённому серверу">
<field name="remote_url" required="1"
placeholder="http://example.com:8069"/>
<field name="remote_db" required="1"/>
<field name="remote_user" required="1"/>
<field name="remote_password" required="1" password="True"/>
</group>
<group string="Что синхронизировать">
<field name="modules" widget="many2many_tags" required="1"/>
<field name="language" required="1"/>
</group>
<group string="Опции">
<field name="overwrite"/>
<field name="save_credentials"/>
</group>
</group>
</div>
<!-- PREVIEW STATE: Show what will be imported -->
<div invisible="state != 'preview'">
<div class="alert alert-info" role="status">
<p>Просмотр переводов, которые будут загружены:</p>
</div>
<field name="preview_lines" nolabel="1" readonly="1"
widget="text"/>
</div>
<!-- DONE STATE: Show results -->
<div invisible="state != 'done'">
<div class="alert alert-success" role="status">
<p>Синхронизация завершена!</p>
</div>
<field name="result_message" nolabel="1" readonly="1"
widget="text"/>
</div>
<footer>
<!-- form state -->
<button name="action_preview" type="object"
string="Просмотр" class="btn-primary"
invisible="state != 'form'"/>
<button name="action_sync" type="object"
string="Загрузить" class="btn btn-success"
invisible="state not in ('form', 'preview')"/>
<button name="action_open_local_sync" type="object"
string="Загрузить из локального хранилища"
class="btn btn-secondary"
invisible="state != 'form'"/>
<!-- done state -->
<button string="Закрыть" class="btn btn-primary"
special="cancel" invisible="state != 'done'"/>
<!-- cancel for form + preview -->
<button string="Отмена" class="btn btn-secondary"
special="cancel" invisible="state == 'done'"/>
</footer>
</form>
</field>
</record>
<!-- Menu item under Settings > Translations > Import / Export -->
<menuitem id="menu_remote_translation_sync"
name="Загрузка перевода из удаленной базы"
action="action_remote_translation_sync_wizard"
parent="base.menu_translation_export"
sequence="30"
groups="base.group_system"/>
</data>
</odoo>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form_budget" model="ir.ui.view">
<field name="name">res.config.settings.view.form.budget</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="25" />
<field name="inherit_id" ref="base_setup.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="Переводы" groups="base.group_system" name="translation_helper" img="/translation_helper/static/description/icon.png">
<block title="Translation Helper" name="translation_helper_setting_container">
<setting id="translation_weblate_server_protocol">
<field name="translation_weblate_server_protocol" />
<div class="text-muted">
Протокол вашего Weblate сервера
</div>
</setting>
<setting id="translation_weblate_server_address">
<field name="translation_weblate_server_address" />
<div class="text-muted">
Адрес вашего weblate сервера
</div>
</setting>
<setting id="translation_weblate_project_alias">
<field name="translation_weblate_project_alias" />
<div class="text-muted">
Алиас вашего Weblate проекта
</div>
</setting>
<setting id="translation_weblate_project_language">
<field name="translation_weblate_project_language" />
<div class="text-muted">
Язык для вашего Weblate проекта
</div>
</setting>
</block>
</app>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,9 @@
<odoo>
<template id="translation_helper.layout_with_translate" name="Web layout with translate" inherit_id="web.layout">
<xpath expr="//head/script[@id='web.layout.odooscript']" position="after">
<script id="translation_helper.layout.odooscript.add.translate" type="text/javascript">
odoo.translate = "<t t-esc="translate" />"
</script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,3 @@
from . import translation_helper_wizard
from . import remote_translation_sync_wizard
from . import local_translation_sync_wizard

View 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",
}

View File

@@ -0,0 +1,215 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
Wizard: Загрузка перевода из удалённой базы.
Connects to a remote Odoo 19 via XML-RPC, runs the standard PO export on
the remote side, downloads the resulting PO, and imports it locally using
the standard TranslationImporter pipeline.
"""
import base64
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class RemoteTranslationSyncWizard(models.TransientModel):
_name = "translation.remote.sync.wizard"
_description = "Загрузка перевода из удалённой базы"
# ---- Connection ----
remote_url = fields.Char(
string="URL удалённого Odoo",
required=True,
help="Адрес удалённого сервера (например, http://192.168.1.10:8069)",
)
remote_db = fields.Char(
string="База данных",
required=True,
help="Имя базы на удалённом сервере",
)
remote_user = fields.Char(
string="Логин",
required=True,
default="admin",
)
remote_password = fields.Char(
string="Пароль",
required=True,
)
# ---- What to sync ----
modules = fields.Many2many(
"ir.module.module",
string="Модули",
required=True,
domain=[("state", "=", "installed")],
help="Модули, переводы которых нужно загрузить",
)
language = fields.Many2one(
"res.lang",
string="Язык",
required=True,
default=lambda self: self.env["res.lang"].search(
[("code", "=", "ru_RU")], limit=1
),
)
# ---- Options ----
overwrite = fields.Boolean(
string="Перезаписать существующие переводы",
default=True,
help="Заменять уже имеющиеся переводы значениями из удалённой базы",
)
save_credentials = fields.Boolean(
string="Запомнить учётные данные",
default=True,
help="Сохранить URL, базу, логин и пароль для повторного использования",
)
# ---- State / results ----
state = fields.Selection(
[("form", "Форма"), ("preview", "Просмотр"), ("done", "Готово")],
default="form",
readonly=True,
)
preview_lines = fields.Text(
string="Просмотр",
readonly=True,
)
result_message = fields.Text(
string="Результат",
readonly=True,
)
# hidden binary to cache PO between preview ↔ sync
_po_cache = fields.Binary(attachment=False)
# ------------------------------------------------------------------
# Defaults from saved credentials
# ------------------------------------------------------------------
@api.model
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
ICP = self.env["ir.config_parameter"].sudo()
defaults.setdefault("remote_url", ICP.get_param("translation_helper.remote_url", ""))
defaults.setdefault("remote_db", ICP.get_param("translation_helper.remote_db", ""))
defaults.setdefault("remote_user", ICP.get_param("translation_helper.remote_user", "admin"))
defaults.setdefault("remote_password", ICP.get_param("translation_helper.remote_password", ""))
return defaults
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_preview(self):
"""Fetch PO from remote, show preview, cache for later import."""
self.ensure_one()
service = self.env["translation.remote.service"]
po_bytes, _filename = service.fetch_po_from_remote(
remote_url=self.remote_url,
remote_db=self.remote_db,
remote_user=self.remote_user,
remote_password=self.remote_password,
module_names=self.modules.mapped("name"),
lang_code=self.language.code,
)
preview_text = service.preview_po(po_bytes)
self.write({
"preview_lines": preview_text,
"state": "preview",
"_po_cache": base64.b64encode(po_bytes),
})
return self._reopen()
def action_sync(self):
"""Import PO into local database."""
self.ensure_one()
service = self.env["translation.remote.service"]
# Use cached PO if available (from preview), otherwise re-fetch
if self._po_cache:
po_bytes = base64.b64decode(self._po_cache)
else:
po_bytes, _filename = service.fetch_po_from_remote(
remote_url=self.remote_url,
remote_db=self.remote_db,
remote_user=self.remote_user,
remote_password=self.remote_password,
module_names=self.modules.mapped("name"),
lang_code=self.language.code,
)
# Apply
service.apply_po(po_bytes, self.language.code, overwrite=self.overwrite)
# Count translated entries for the message
import polib
po = polib.pofile(po_bytes.decode("utf-8", errors="replace"))
translated_count = sum(1 for e in po if e.msgstr and e.msgid)
msg = _(
"Загрузка завершена!\n\n"
"Модули: %(modules)s\n"
"Язык: %(lang)s\n"
"Переводов в файле: %(total)d\n"
"Из них переведено: %(translated)d\n"
"Перезапись: %(overwrite)s",
modules=", ".join(self.modules.mapped("name")),
lang=self.language.name,
total=len(po),
translated=translated_count,
overwrite=_("да") if self.overwrite else _("нет"),
)
# Save credentials
if self.save_credentials:
self._save_credentials()
msg += "\n\n" + _("Учётные данные сохранены.")
self.write({
"result_message": msg,
"state": "done",
"_po_cache": False,
})
_logger.info("Remote translation sync completed for %s", self.modules.mapped("name"))
return self._reopen()
def action_open_local_sync(self):
"""Open the local-storage translation wizard."""
self.ensure_one()
return {
"name": _("Загрузка перевода из локального хранилища"),
"type": "ir.actions.act_window",
"res_model": "translation.local.sync.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_language": self.language.id},
}
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _save_credentials(self):
ICP = self.env["ir.config_parameter"].sudo()
ICP.set_param("translation_helper.remote_url", self.remote_url or "")
ICP.set_param("translation_helper.remote_db", self.remote_db or "")
ICP.set_param("translation_helper.remote_user", self.remote_user or "admin")
ICP.set_param("translation_helper.remote_password", self.remote_password or "")
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",
}

View File

@@ -0,0 +1,139 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import os
import polib
from odoo import fields, models
from odoo.modules.module import get_module_path
_logger = logging.getLogger(__name__)
dir_path = os.path.dirname(os.path.realpath(__file__))
dir_path = os.path.join(dir_path, "..")
class TranslationHelperWizard(models.TransientModel):
_name = "translation.helper.wizard"
_description = "Translation Helper Wizard"
weblate_link = fields.Char(
string="Weblate Link",
help="Link to current term on Weblate server",
)
term_value = fields.Char(
string="Term",
help="Term value to translate",
)
translation_value = fields.Char(
string="Translation",
help="Translation for term value",
)
language_id = fields.Many2one(
string="Language",
help="The language for which the translation will be applied",
comodel_name="res.lang",
)
modules = fields.Char(
string="In Apps",
help="List of modules in which the translation term is defined",
)
metadata = fields.Json(
string="Metadata",
help="Metadata",
)
def save_translation(self):
for module_name in self.modules.split(", "):
self.update_term_translation_in_module(
module_name=module_name,
term_to_translate=self.term_value,
translation_value=self.translation_value,
lang=self.language_id.iso_code,
)
model_name = self.metadata.get("resModel")
field_name = self.metadata.get("field", {}).get("name")
translation_field_data = self.metadata.get("translation_field_data", {})
translation_field_data[self.language_id.code] = self.translation_value
query = """
UPDATE ir_model_fields SET field_description = %s
WHERE model = %s AND name = %s
"""
self.env.cr.execute(
query, [json.dumps(translation_field_data), model_name, field_name]
)
return {
"type": "ir.actions.client",
"tag": "translation_helper.response_from_wizard",
"name": "Translate info",
"params": {
"model_name": model_name,
"field_name": field_name,
"translation_field_data": translation_field_data,
},
}
def update_term_translation_in_module(
self, module_name, term_to_translate, translation_value, lang
):
# Always write to local translations/{lang}/ directory inside translation_helper module
local_po_dir = os.path.join(dir_path, "translations", lang.strip())
local_po_file = os.path.join(local_po_dir, f"{module_name}.po")
if os.path.exists(local_po_file):
# Local override already exists — update it
source_pofile = polib.pofile(local_po_file)
else:
# Try to find the system .po file to use as a base
source_pofile = None
module_path = get_module_path(module_name, display_warning=False)
if module_path:
for subdir in ("i18n", "i18n_extra"):
candidate = os.path.join(module_path, subdir, f"{lang}.po")
if os.path.exists(candidate):
source_pofile = polib.pofile(candidate)
break
if source_pofile is None:
_logger.warning(
"translation_helper: no .po file found for module %r, lang %r",
module_name,
lang,
)
return
# Ensure local directory exists
os.makedirs(local_po_dir, exist_ok=True)
# Build new pofile with updated translation
new_pofile = polib.POFile()
new_pofile.metadata = source_pofile.metadata
module_prefix = f"module: {module_name}"
for old_entry in source_pofile:
msgstr = old_entry.msgstr
if old_entry.msgid == term_to_translate:
msgstr = translation_value
# Odoo 19 translate.py requires "module: {name}" in every entry's comment
comment = old_entry.comment
if comment and "module:" not in comment:
comment = f"module: {module_name}\n{comment}"
elif not comment:
comment = f"module: {module_name}"
new_entry = polib.POEntry(
comment=comment,
occurrences=old_entry.occurrences,
flags=old_entry.flags,
msgid=old_entry.msgid,
msgstr=msgstr,
)
new_pofile.append(new_entry)
# Always save to local path (never to system directories)
new_pofile.save(local_po_file)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="translation_helper_wizard_form" model="ir.ui.view">
<field name="name">translation.helper.wizard.form</field>
<field name="model">translation.helper.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="weblate_link" readonly="1" widget="url" options="{'website_path': True}" />
<field name="term_value" readonly="1" />
<field name="translation_value" />
<field name="language_id" readonly="1" />
</group>
</sheet>
<footer>
<button name="save_translation" string="Apply" class="oe_highlight" type="object" data-hotkey="q" />
<button special="cancel" data-hotkey="x" string="Cancel" />
</footer>
</form>
</field>
</record>
</odoo>