Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC

This commit is contained in:
CI Publish Bot
2026-05-31 21:19:21 +00:00
commit aa4214c195
1213 changed files with 183945 additions and 0 deletions

View File

@ -0,0 +1,59 @@
# Модуль переводов в реальном времени
⚠️ **ВНИМАНИЕ:** модуль находится на стадии бета-тестирования. Использование в промышленной эксплуатации **не рекомендуется**.
Модуль реализует **прямой перевод терминов в режиме реального времени** прямо в пользовательском интерфейсе Odoo.
---
## Краткое руководство пользователя
1. Установите модуль.
2. Перейдите в настройки и активируйте **«Режим переводчика»** — он работает аналогично режиму разработчика.
3. В правом верхнем углу экрана появится дополнительная иконка. Нажмите на неё, чтобы открыть меню доступных действий.
4. У каждого пункта меню появится иконка для перевода. При изменении перевода он будет применён, и страница автоматически перезагрузится.
5. У каждого поля появится кнопка для его перевода. Вы можете перевести:
* Название поля
* Подсказку (tooltip), которая отображается при наведении на знак вопроса
Перевод применяется **немедленно**.
6. Все переводы сохраняются:
* В базу данных
* В `.po`-файлы выбранного языка, с именем, соответствующим модулю, в котором был объявлен термин
Например, если вы перевели термин из модуля `crm`, будет создан файл:
```
translation_helper/translations/{код_языка}/crm.po
```
7. В этот файл выгружаются все переведённые термины.
---
## Применение переводов на новой базе
Если вы создадите новую базу с установленным модулем, то при выборе, например, русского языка, система будет **на лету** подменять обращения к файлу:
```
odoo/addons/crm/i18n/ru.po
```
на ваш собственный файл:
```
translation_helper/translations/ru/crm.po
```
---
## Преимущества
* Вы можете **накапливать собственные переводы**, которые не зависят от внешних переводов.
* Вы **полностью контролируете** систему локализации.
* Перенос переводов между инстансами Odoo выполняется **простым копированием модуля**.

View File

@ -0,0 +1,134 @@
==========================
Помощник по переводам
==========================
.. |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|
Модуль предоставляет инструменты для ручного перевода полей, представлений и меню Odoo
с сохранением переводов в локальные PO-файлы модулей.
**Содержание**
.. contents::
:local:
Возможности
===========
* Ручной перевод полей через контекстное меню
* Перевод нескольких полей одновременно
* Поддержка переводимых полей типа char, text и html
* Поддержка глоссариев для согласованной терминологии
* Память переводов для повторного использования
* Интеграция с режимом разработчика
* Сохранение переводов в локальные PO-файлы модулей
* Перевод меню и представлений
Настройка
=========
Для настройки модуля необходимо:
#. Перейти в *Настройки > Общие настройки*
#. Найти раздел "Помощник по переводам"
#. Настроить параметры перевода:
* Выбрать исходный и целевые языки
* При необходимости загрузить файлы глоссариев
#. Сохранить настройки
Настройка глоссариев:
~~~~~~~~~~~~~~~~~~~~~
Файлы глоссариев размещаются в директории ``translations/{lang}/``:
* ``glossary.txt``: Пары терминов для согласованного перевода
* ``{module}.po``: Переводы для конкретного модуля
Использование
=============
Перевод полей:
~~~~~~~~~~~~~~
#. Включите режим разработчика
#. Откройте любую форму с переводимыми полями
#. Нажмите кнопку "Перевести" в меню отладки
#. Выберите поля для перевода
#. Введите переводы вручную
#. Нажмите "Сохранить"
Модуль выполнит следующие действия:
* Определит исходный язык из текущих значений полей
* Применит термины из глоссария для согласованности
* Использует память переводов для повторяющихся фраз
* Обновит переводы полей для всех настроенных языков
* Сохранит переводы в локальные PO-файлы модулей
Перевод меню:
~~~~~~~~~~~~~
#. Перейдите к любому пункту меню
#. Нажмите иконку перевода в меню
#. Выберите целевые языки
#. Введите переводы
#. Подтвердите сохранение
Перевод представлений:
~~~~~~~~~~~~~~~~~~~~~~
Модуль позволяет переводить:
* Метки полей
* Текст подсказок
* Текст-заполнитель
* Метки кнопок
* Заголовки групп
Известные проблемы / Планы развития
====================================
* Добавить поддержку дополнительных сервисов перевода
* Пакетный перевод для нескольких записей
* Оценка качества переводов
* Контекстно-зависимые переводы
* Рабочий процесс проверки переводов
Отслеживание ошибок
===================
Ошибки отслеживаются на `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
В случае проблем, пожалуйста, проверьте, не была ли ваша проблема уже зарегистрирована.
Авторы
======
Авторы
~~~~~~
* MK.Lab, RuOdoo
Участники
~~~~~~~~~
* MK.Lab, RuOdoo
Сопровождающие
~~~~~~~~~~~~~~
Модуль сопровождается 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,54 @@
{
"name": "Translation Helper",
"summary": "Помощник для ручного перевода полей и представлений Odoo",
"description": """
Помощник по переводам
=====================
Инструмент для ручного перевода полей, представлений и меню Odoo с поддержкой
глоссариев, памяти переводов и сохранением в локальные PO-файлы модулей.
Возможности:
------------
* Ручной перевод полей через контекстное меню
* Перевод нескольких полей одновременно
* Поддержка переводимых полей типа char, text и html
* Поддержка глоссариев для согласованной терминологии
* Память переводов для повторного использования
* Интеграция с режимом разработчика
* Настраиваемые параметры перевода
* Поддержка перевода меню и представлений
* Сохранение переводов в локальные PO-файлы модулей
Технические детали:
-------------------
* Расширяет модели ir.http, ir.qweb, base
* JavaScript-компоненты для интеграции с UI
* Слой абстракции сервисов перевода
* Парсинг PO-файлов и управление глоссариями
* Контекстно-зависимый перевод с метаданными полей
""",
"author": "MK.Lab, RuOdoo",
"website": "https://ruodoo.ru",
"category": "Productivity/Translations",
"version": "19.0.1.0.1",
"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",
"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,85 @@
Переведенные поля больше не используют модель `ir.translation`. Теперь они хранят все свои значения как `JSON` и сохраняют их в столбцах `JSONB` в
таблице соответствующей модели. Значение столбца поля либо `NULL`, либо `JSON dict`, где ключом является код языка, а его значение и есть термин. Ключ
`en_US` всегда ставится по умолчанию, в тех случаях когда нет других ключей Пустой текст допускается в значениях перевода, но не `NULL`.
Вот пример поля с параметром `translate=True`:
```json
NULL
{"en_US": "Foo"}
{"en_US": "Foo", "fr_FR": "Bar", "nl_NL": "Baz"}
{"en_US": "Foo", "fr_FR": "", "nl_NL": "Baz"}
```
Как и ранее, если полю установить значение `False` , то на уровне БД оно будет `NULL`, т. е. `False` для всех языков. Однако если установить значение
`""` (пустая строка) система делает его значение пустым на текущем языке, но не отменяет значения на других языках.
Вот пример поля с параметром `translate=xml_translate`:
```json
NULL
{"en_US": "<div>Foo<p>Bar</p></div>", "fr_FR": "<div>Fou<p>Barre</p></div>"}
```
Изменение для полей `callable(translate)`: теперь в таком поле можно записать любое значение на любом языке. Новое значение будет адаптировано для
всех языков на основе сопоставления терминов между языками в старых значениях. В основном структура значения должна оставаться одинаковой для всех
языков, как и раньше.
Чтение переведенного поля теперь проще и быстрее, чем в предыдущей реализации. Мы извлекаем значение поля на текущем языке, объединяя его значение со
значением поля `en_US`:
```sql
SELECT id, COALESCE(name->>'fr_FR', name->>'en_US') AS name ...
```
Необработанный кэш поля содержит в базе данных (за исключением отсутствующих языков) либо `None`, либо `dict`, который концептуально является
подмножеством значения `JSON` . Для простоты большинство операций кэширования работают с `dict` и возвращают текстовое значение на текущем языке.
Индексы триграмм адаптированы к новой стратегии хранения и должны позволять поиск на любом языке. До этого изменения индексировалось только исходное
значение поля `en_US`.
Хранимые вычисляемые переведенные поля не поддерживаются фреймворком из-за сложности самого вычисления: поле должно быть вычислено на всех активных
языках. Мы решили не предоставлять никаких хуков для вычисления поля на всех языках одновременно, и фреймворк всегда вызывает метод вычисления один
раз, чтобы пересчитать его.
Переводы кода больше не хранятся в базе данных. Они становятся статическими и извлекаются из файлов PO по мере необходимости. Worker просто использует
кэш с извлеченными переводами кода для производительности. Это разумно, так как переводы кода `fr_FR` для всех модулей занимают около 2 МБ памяти, и
кэш может быть общим для всех реестров в Worker. Изменение переводов кода требует обновления соответствующего файла PO и перезагрузки Worker.
Сводка по производительности: (+) чтение переведенных полей `model` происходит быстрее (+) чтение переведенных полей `model_terms` происходит намного
быстрее (нет необходимости вводить переводы в исходное значение) (+) поиск переведенных полей с оператором `ilike` происходит намного быстрее, если
поле индексировано с помощью `trigram` (+) обновление переведенных полей требует меньше очистки `ORM` (-) импорт переводов из файлов `PO` происходит в
2 раза медленнее
Несколько экстра исправлений: сделать поле `name` модели `ir.actions.actions` переведенным; из-за наследования `PG` это необходимо для того, чтобы
сделать определение столбца согласованным во всех моделях, которые наследуются от `ir.actions.actions`. добавить внутренний API для
веб-клиента/клиента веб-сайта для редактирования переводов переместить методы `get_field_string()` в модель `ir.model.fields` переместить
`_load_module_terms` в модель `ir.module.module` адаптировать тесты в `test_impex`, `test_new_api` поскольку `env.lang` внедряется в запросы `SQL`,
его возвращаемое значение теперь гарантированно соответствует допустимому активному языку или `None` удалить мастер для вставки отсутствующих
переводов (больше не имеет смысла)
task-id: 2081307
Резюме по исследованию формирования переводов для представлений: Переводы для имен полей формируются следующим образом:
1. Необходимо иметь перевод в PO файле, тогда при обновлении перевода как такового, применится новый перевод
2. Для перевода имени поля создается в модели `ir.model.fields` создано поле `field_description` которое и хранит его перевод
3. Тем не менее данный перевод будет применен только после перезагрузке системы
4. Это связано с тем, что при обновлении страницы вызывается метод `get_views` из модели `ir.ui.view` в ответе которого есть ключ `models`, и в нем
содержатся переводы для имен полей. Но происходит это не через переводы, а через сбор атрибутов поля Есть предположение что для конкретного языка в
памяти создается единожды класс для каждого поля с переведенными атрибутами и именно по этому помогает только перезагрузка.
Дополнительные сведения полученные экспериментальным путем
1. Если в каталоге i18n есть pot файл, то если в нем не указаны какие либо термины для перевода, то эти термины будут игнорироваться во всех po файлах
этих переводов. Т.е для того, чтобы переводы из po файла применились нужно указать термины в pot файле, либо удалить pot файл из каталог
2. Часть терминов берется напрямую из po файлов при обновлении страницы и на фронт загружаются все термины, которые помечены комментарием
`#. odoo-javascript`
3.
В системе есть как минимум 3 больших блока для которых необходимо сформировать инструменты для перевода:
1. Атрибуты полей
2. Термины для перводов во вьюхах в том числе и Search View
3. Термины для переводов в js коде
4. Меню?

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,7 @@
from . import base
from . import 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,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,2 @@
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
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

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

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,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 @@
from . import translation_helper_wizard

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>