История версий

Файл создан скриптом docs/build/generate/Changelog.php из CHANGELOG.md в корне модуля. Править надо там.

7.6.2 — 2026-09-20

Витрина снова показывает разметку, а не теги текстом

Срочное исправление регресса 7.5.0.

В 7.5.0 подстановка перевода в поля элемента и раздела стала следовать договору ядра: в ключ без тильды — значение, подготовленное к выводу. Формально верно. Практически — на витрине вместо оформленного текста появились видимые <p>, <br> и прочие теги.

Причина: к моменту подстановки компонент уже выбрал вид значения. У HTML-поля в DETAIL_TEXT лежит готовая разметка, и шаблон печатает её как есть. Подменив её экранированной, модуль показал посетителю исходный код.

Подстановка вернулась к прежнему поведению: отдаём значение в том виде, в каком оно лежит, и не решаем за компонент.

Цена возврата известна и принята: значение с разметкой, записанное контент-менеджером, попадает в страницу разметкой. По рубрике Маркетплейса это stored-xss-trusted-editor-content — low, выкладку не блокирует. Сломанная витрина блокирует всё.

Экранирование, добавленное в 7.5.0 в других местах, остаётся: хлебные крошки, подготовленные ключи выдачи поиска, подписи меню админки и названия способов доставки. Там значение печатается без выбора со стороны компонента, и договор однозначен.

7.6.1 — 2026-09-20

Фразы установщика были пустыми

На странице удаления вместо предупреждения показывалась пустая рамка, а галочка «сохранить настройки» шла без подписи. Кнопка при этом подписывалась правильно — её текст берётся из словаря ядра.

Причина: словарь подключался по пути к самому файлу словаря (../lang/<язык>/install/index.php), да ещё через ... Ядро ждёт путь к исходному файлу и находит словарь само. Ошибка была и в странице после установки — то есть сообщение «Модуль успешно установлен» тоже не показывалось, и до 7.6.0 в том числе.

Правило закреплено офлайн-сценарием: увидеть такое можно только глазами на живом сайте, а это плохая проверка.

7.6.0 — 2026-09-20

Установка и удаление приведены к тому же виду, что у соседних модулей.

Удаление спрашивает подтверждение

Раньше нажатие «Удалить» сразу снимало обработчики событий, фоновое задание, права и служебные файлы. Ни вопроса, ни возможности передумать.

Теперь это мастер из двух шагов. На первом сказано, что именно будет снято, и стоит галочка «сохранить настройки модуля». На втором — подтверждение, что всё прошло.

Переводы не удаляются ни при какой настройке — ни словарь в highload-блоке, ни таблица состояния очереди. Это контент, а не служебные данные. Так было и раньше, но человеку об этом не говорили; теперь сказано прямо на первом шаге.

Удаление проверяет подлинность запроса

Проверки сессии при удалении не было вовсе. Теперь она стоит до первого разрушающего действия, а не после: проверка, стоящая после, подавила бы сообщение об успехе, но не сами разрушения.

Из консоли проверка пропускается — сессии там нет, и требовать её значило бы сделать удаление скриптом выкладки невозможным.

Установка отвечает, а не молчит

7.5.3 — 2026-09-19

Сбой перевода sprint-блока попадает в журнал событий

Lang::translateTexts() при ошибке провайдера пишет TRANSLATE_ERROR, а соседний Lang::translateSprintJson() — не писал: ловил исключение, возвращал его текст вызывающему и всё.

Из-за этого разбор боевого случая упирался в пустоту. На экране — «Элемент 10, свойство SERVICES_CONTENT_ZH: не удалось перевести sprint-блок — Ошибка внешнего провайдера перевода. Повторите позже.», в журнале событий — ничего. Показать ошибку человеку и оставить след — разные задачи, и вторая нужна как раз тогда, когда окно уже закрыли.

Теперь в записи виден формат, исходный и целевой язык, движок и текст ошибки от API целиком.

Сама причина того сбоя — на стороне сервиса перевода, и она исправлена отдельно: отказ модели на sprint-блоке больше не выдаётся за временную ошибку провайдера, и сбой провайдера пишется в журнал сервиса всегда, а не только когда обращение уже оплачено.

7.5.2 — 2026-09-19

Исправление сбоя, который принесла 7.5.1.

Пересчёт элементов падал с ошибкой типа

TypeError: BatchAccess::withPermission(): Argument #2 ($permission)
must be of type array, null given

Правило прав для скана очереди завели в одном методе, а читали в другом — туда переменная не доходит, и PHP отдал null. Правило передаётся параметром.

Затронуты установки, где успели поставить 7.5.1 и нажать пересчёт. Данные при этом не пострадали: ошибка происходила до первой записи.

Почему это не поймали

Ни php -l, ни офлайн-сценарии этого не видели: синтаксис верный, а сценарий смотрел на текст файла, а не на область видимости.

Добавлен разборщик областей видимости и сценарий на нём: ни одна функция модуля не читает имени, которого в её области никто не завёл. Ловит именно этот класс ошибок — второй за месяц после потерянного $docRoot в 7.4.6.

7.5.1 — 2026-09-18

Разбор внешней проверки 7.5.0. Правил перевода и хеширования выпуск не меняет, миграции нет.

Хлебные крошки: убрано двойное экранирование

В 7.5.0 две правки одной группы столкнулись: декоратор стал класть в NAME значение, подготовленное к выводу, а Breadcrumbs начал экранировать имя элемента, полученное снаружи. Руководство разработчика при этом учит звать fixBreadcrumbs() ровно тем значением, которое декоратор только что подготовил, — то есть «Кофе & чай» в цепочке выходило по документированному сценарию, а не в экзотическом случае.

Договор теперь такой же, как у ядра в AddChainItem, и записан в руководстве: имя элемента передаётся подготовленным. Названия разделов метод по-прежнему читает и готовит сам — там в выборке лежит сырое UF_NAME_<LANG>.

Если ваш шаблон обходил прежнее поведение своим htmlspecialcharsbx() перед вызовом — его нужно убрать.

Фильтр словаря опознаёт схему в любой записи

cleanTrusted() пропускал href="javascript&colon;alert(1)". Числовые сущности libxml разворачивает сам, а &colon; — нет: это HTML5, а парсер libxml знает HTML4. Браузер HTML5 знает, поэтому ссылка срабатывала.

Схема теперь сравнивается с тем, что из атрибута получит браузер. Возврат строки байт в байт, когда чистить нечего, сохранён.

Права на сущности: закрыты ещё два входа

Прочее

Непригодное значение saas_api_base_url больше не отбраковывается молча: модуль работает с адресом по умолчанию и пишет об этом в журнал событий под TRANSLATE_API_BASE_URL_REJECTED.

7.5.0 — 2026-09-18

Подготовка к Маркетплейсу по правилам marketplace-security-skills. Правил перевода и хеширования выпуск не меняет, миграции состояния в нём нет. Функционал не сокращён ни в одном пункте.

Консольные скрипты больше не открываются из браузера

cli/_bootstrap.php объявлял NOT_CHECK_PERMISSIONS и BX_PUBLIC_MODE без проверки, что мы в консоли. По HTTP это снимало файловый ACL ядра на путь скрипта, а собственной проверки прав четыре скрипта из десяти не имели вовсе: гость мог выполнить разведку базы, ремонт хешей и обращение к платному API. Проверка баланса при этом успевала уйти в сервис и только потом падала — расход был тихим.

Гейт по SAPI стоит первой исполняемой строкой заготовки, до объявления констант и до пролога. Веб-запрос получает 404 и больше ничего.

Вместе с ним снят HTTP-режим, который гейт сделал мёртвым: приём пути для CSV из адреса, запись брака в таблицу состояния по обычной ссылке, отчёт разведки под /upload/, HTML-вывод. search_recon и vocab_bench переведены на ключи командной строки — раньше они жили только на адресе и из консоли не работали вовсе.

Что это значит на практике: адреса вида /local/modules/mrlexndr.translate/cli/… больше не работают. Все скрипты запускаются из консоли, из каталога модуля.

Служебные каталоги не едут в поставку

cli/, tests/ и docs/ помечены export-ignore. Рядом лежат .htaccess и .access.php — на случай, если каталог всё-таки оказался на сервере. tests/run_all.php получил тот же гейт по SAPI.

Подстановка уважает договор ядра

Ядро кладёт в ключ без тильды значение, подготовленное к выводу, а в ~KEY — сырое, и шаблоны печатают ключ без тильды не экранируя. Модуль клал сырое в оба, и содержимое поля попадало в страницу как разметка.

Исправлены декоратор витрины (элемент и раздел), элемент в хлебных крошках и подготовленные ключи выдачи поиска.

Это заметно на сайте. Правка 7.3.2 читала сырое значение ради симптома «&amp; в названии» на одном проекте — там шаблон экранирует NAME сам и потому удваивает экранирование. После этого выпуска симптом там вернётся, и чинить его нужно в шаблоне: убрать htmlspecialcharsbx() вокруг NAME, потому что ядро отдаёт это поле уже подготовленным.

Пакетный перевод считает права на каждый элемент

Одиночный перевод проверял право записи на конкретный элемент, пакетный — только на инфоблок. На инфоблоке с расширенными правами человек переводил и перезаписывал элементы, которых ему не видно.

У очереди правило осталось прежним, и это решение: её шаг выполняется на чужом хите, и счёт прав текущего посетителя терял бы переводы.

Словарь: второй режим фильтра

Ответ API для словаря шёл мимо всякой проверки. Белый список туда вешать нельзя — это пробовали в 6.10.0 и откатили. Поэтому режим чёрный: убираются script, обработчики событий, javascript:-адреса и комментарии, а картинки, таблицы и разделители проходят. Если убирать нечего, строка возвращается байт в байт.

Прочее

7.4.6 — 2026-09-18

Консольные скрипты поднимают ядро одинаково и работают из консоли без ручных правок. Правил перевода и хеширования выпуск не меняет.

Что было не так

Из консоли $_SERVER['DOCUMENT_ROOT'] не задан: его заполняет веб-сервер. Ядро читает эту переменную изнутри prolog_before.php, а скрипты вычисляли корень в свою переменную и передавали его в require — ядру от этого не легче. Часть скриптов и вовсе читала $_SERVER напрямую и падала сразу.

Заказчик всё это время дописывал строку с корнем руками перед каждым запуском.

Что сделано

Одна заготовка cli/_bootstrap.php, все десять скриптов начинаются с её подключения. Корень ищется по порядку: уже заданный → переменная окружения BITRIX_DOCUMENT_ROOT → вычисление от места файла.

Путь конкретного сервера не зашит: модуль стоит не на одном сайте, и зашитый путь превратил бы правку обратно в ручную — только на другом проекте. Для нестандартной раскладки есть переменная окружения.

Правило записано в соглашения проекта и прибито офлайн-сценарием: он следит, что запись идёт именно в $_SERVER, что она стоит до пролога, что путь не зашит и что ни один скрипт не поднимает ядро сам.

7.4.5 — 2026-09-18

Два инструмента разбора. Правил перевода и хеширования выпуск не меняет, поэтому повторный ремонт после него не нужен.

Почему поля ждут перевода

php cli/hash_repair.php --why разбирает ожидание по причинам:

Ремонт отвечает на один вопрос: «разошёлся ли хеш». Когда он говорит «ноль», а очередь не пуста, нужен второй вопрос — а почему тогда. Первая причина лечится переводом, вторая ремонтом, третья пересчётом, четвёртая разбором.

Отчёт ремонта перестал складывать разное

Раньше всё починенное шло одним числом «мнимое расхождение». Это было нечестно: там две разные вещи.

Теперь в отчёте две строки вместо одной.

7.4.4 — 2026-09-18

Правило вместо списка: запись знака сущностью теперь равна самому знаку.

Что было не так

До этого выпуска перечислялись три написания неразрывного пробела, и этого хватало ровно до первой встречи с &thinsp;: сам тонкий пробел схлопывался, а его запись сущностью меняла хеш. Непоследовательность, которая повторилась бы с любым другим написанием — а модуль правки текста, как выяснилось на боевом стенде, пишет и знаками, и сущностями.

Что сделано

Сущность разворачивается, и решение принимается по тому, что получилось: пробельное становится пробелом, невидимое исчезает, остальное остаётся сущностью нетронутым. &amp; и &lt; — это текст.

Теперь закрыты все написания разом: &thinsp;, &ensp;, &emsp;, &#8201;, &#8239;, &#x2009;, &shy;, &#173; и любые другие, известные HTML5.

Пробельное становится пробелом, а не пустотой. Неразрывный пробел разделяет слова: выбрось его — и «NNN BC» станет «NNNBC», а разные тексты начнут давать один хеш.

Соединители нулевой ширины и направляющие метки не трогаются. Они невидимы сами по себе, но меняют то, что видит читатель: управляют слитностью букв в арабском, склеивают «👨‍👩‍👧» из трёх знаков. Объявить такие тексты одинаковыми — та же ошибка, только с другого конца.

Что надо сделать после обновления

Это четвёртая смена правила подсчёта, поэтому ремонт надо прогнать снова: php cli/hash_repair.php, затем --apply. Прежнее правило добавлено в список известных, так что ремонт узнает старые хеши.

7.4.3 — 2026-09-18

Ремонт хешей перестал угадывать прежний текст и начал его читать.

Что выяснилось

Исходники на боевом стенде правил не человек и не модуль перевода, а модуль правки текста — 1 сентября, сразу после перевода. Правки косметические, но хеш меняли честно:

было:   ...для&nbsp;туристических лыж: NNN&nbsp;BC, Xplore, NN&nbsp;75
стало:  ...для·туристических лыж: NNN BC, Xplore, NN 75

Одна сущность превратилась в знак, две исчезли. Из нынешнего текста прежний не выводится: подставить обратно можно один пробел, а их было три, и какие именно — в тексте не записано.

Что сделано

Ремонт читает точные снимки оригиналов из таблицы правщика (mrlexndr_corrector_backup) и проверяет сохранённый хеш против них. Это не догадка, а запись: совпало — расхождение объяснено целиком.

Читается прямым запросом, без загрузки того модуля: таблица снимков переживает его удаление, и требовать его наличия значило бы отказываться чинить ровно в том случае, ради которого всё затевалось. Нет таблицы — ремонт просто работает как раньше.

Режим --explain тоже сверяется со снимками и показывает найденный оригинал.

Чего выпуск не делает

Не переводит заново ничего и не тратит денег.

7.4.2 — 2026-09-18

Продолжение 7.4.1: ремонт не узнавал главную разновидность мнимого расхождения и починил на боевом стенде 10 полей вместо полутора тысяч.

Что было не так

В 7.4.1 я решил, что старый хеш посчитан над значением, которое ядро прогнало через htmlspecialcharsEx(). Оказалось иначе: htmlspecialchars неразрывный пробел не трогает вовсе. Настоящая разница была в том, как знак записан в самой базе — то сам знак U+00A0, то последовательность &nbsp;. Зависит от того, чем текст правили в последний раз: визуальный редактор, импорт и типографские модули пишут по-разному.

Читателю разницы нет никакой — обе записи дают один и тот же пробел. А хеш менялся, и перевод уходил в платный повтор.

Что сделано

Чего выпуск не делает

Не переводит заново ничего и не тратит денег.

7.4.1 — 2026-09-18

Починка последствий правки 6.13.0: весь каталог заказчика числился ждущим перевода, хотя ни один текст не менялся. Новых возможностей нет.

Что случилось

Правка 6.13.0 переключила чтение значения из Битрикса с экранированного на сырое. Правка верная — без неё разметка в исходнике портила сверку переводов. Но тем же чтением кормится подсчёт хеша исходника, то есть памяти модуля о том, под какой текст сделан перевод, а миграции сохранённых хешей выпуск не содержал.

Битрикс при чтении без тильды прогоняет значение через htmlspecialcharsEx(), и неразрывный пробел там превращается в &nbsp;. В русской типографике этот знак стоит после коротких предлогов и в конструкциях вроде «Часть 2» — то есть почти в каждом заголовке и анонсе. Все хеши, записанные до 6.13.0, перестали совпадать навсегда.

Очередь честно докладывала «ждёт перевода» по всему каталогу. Заметить было некому: она выглядела работающей. На боевом стенде так набралось 1411 строк состояния из 1655, и каждая означала повторную оплату уже сделанного перевода.

Что сделано

Что это значит на практике

Типографский модуль, расставляющий неразрывные пробелы и мягкие переносы, теперь можно запускать по каталогу без опаски: очередь этого не заметит и на перевод ничего не отправит.

Чего выпуск не делает

Не переводит заново ничего и не тратит денег. Переводы годные: изменился не текст, а способ его хеширования.

7.4.0 — 2026-09-18

Страница «Проверка стенда» в меню модуля: паспорт проекта и автопроверки, которые ничего не пишут. Первый из двух заходов — глубокая проверка со своими сущностями и ручной чек-лист идут следующим выпуском.

Что появилось

Почему это сделано так

Чего в выпуске нет

Разбор перед выпуском

Независимый проход по коду нашёл одиннадцать дефектов, и главный рушил саму посылку страницы: проверка версии сравнивала файл сам с собой. ModuleManager::getVersion() в базу не ходит — он инклюдит тот же install/version.php, а версии модуля в базе нет вовсе. Теперь установка оставляет отметку сама, и сверяется она; именно так ловится случай «выложили файлы и не нажали Обновить».

Остальные десять — в истории коммитов группы 47. Четыре из них дали бы красное или жёлтое на исправном стенде, то есть отправили бы человека чинить работающее.

Долг, закрытый по дороге

Правка 7.3.2 про ссылку вида //чужой.сайт была сделана без офлайн-сценария, хотя отчёт его просил: мутация «разрешить два слеша» оставалась зелёной. Сценарий заведён, в нём 26 проверок и семь мутаций.

7.3.2 — 2026-09-18

Заказчик прошёл весь модуль шестью независимыми проходами по зонам. Новых возможностей нет: это то, что должно было работать и работало не так.

Что изменилось

Чего делать не стали и почему

7.3.1 — 2026-09-18

Заказчик сверил код с программой расширения охвата и нашёл то, что должно было работать в 7.0.0–7.3.0, а работало не так или не до конца. Новых возможностей здесь нет: это долг закрытой программы.

Что изменилось

Чего делать не стали и почему

7.3.0 — 2026-09-18

Завершение программы расширения охвата: отметка «оригинал изменился» для словаря (A6.4), магазин через словарь (Б6), названия инфоблоков (Б7). Постановка — translate-saas-plan-coverage.md; ход работ по группам — docs/agent/.

Что изменилось

Чего делать не стали и почему

7.2.0 — 2026-09-18

Продолжение программы расширения охвата: торговые предложения (Б4), UF-поля произвольных сущностей и строки highload-блоков (Б5). Постановка — translate-saas-plan-coverage.md; ход работ по группам — docs/agent/.

Что изменилось

Что нашла проверка перед выпуском

Независимый разбор нашёл три ошибки, из-за которых перевод справочников не работал вовсе, и несколько мест, где заявленное работало дороже обещанного. Всё закрыто до выпуска.

Чего делать не стали и почему

7.1.0 — 2026-09-18

Продолжение программы расширения охвата: конвенция служебных фраз (A6), названия свойств (Б2), разделы в очереди (Б3), поиск по языкам (Б8). Постановка — translate-saas-plan-coverage.md; ход работ по группам задач — docs/agent/.

Что изменилось

Что нашла проверка перед выпуском

Независимый разбор возможностей 7.1.0 нашёл одну ошибку, из-за которой часть заявленного не работала вовсе, и несколько мест, где заявленное работало дороже обещанного. Всё закрыто до выпуска.

7.0.0 — 2026-09-18

Программа расширения охвата, фаза A и блок SEO. Постановка — translate-saas-plan-coverage.md в корне рабочей папки; ход работ по группам задач — docs/agent/.

Перед выкладкой на боевой проект — чек-лист стенда: сначала копия базы, потом «Обновить» (меняется уникальный ключ таблицы состояния очереди), затем проверки по списку.

Что изменилось

Чего делать не стали и почему

6.15.1 — 2026-09-17

Зеркало языка блокирует SEO-вкладку.

Поля IPROPERTY_TEMPLATES[...] у ядра общие для всех языков, а форма постит их в базовые шаблоны. В режиме зеркала они оставались редактируемыми, и правка «на английском» уезжала в базовый шаблон — тихо, без предупреждения. Теперь эти поля в режиме зеркала только для чтения, с подсказкой, что SEO-шаблоны по языкам появятся в 7.0.0 (программа расширения охвата, блок Б1, входит в первый релиз вместе с фундаментом). Скрытые настройкой формы поля не трогаются: они уезжают как есть.

Блокировка идёт тем же механизмом и тем же журналом отката, что у полей без языкового двойника. Затронуты только install/js/iblock_edit_lang.js, lib/UI/IblockLangMirror.php и языковые файлы; схема базы не менялась.

6.15.0 — 2026-09-17

Правка перевода в штатной форме админки: зеркало языка.

Что появилось

Администратор переключает язык админки штатным переключателем в шапке. Если выбранный язык — один из целевых языков перевода, форма элемента показывает контент этого языка на местах базовых полей: «Название» в своей строке, «Анонс» и «Подробно» на своих вкладках, языковые свойства — на местах базовых. Сохранение пишет введённое в языковые свойства, базовые значения уезжают на сервер нетронутыми. Форма при этом остаётся штатной формой Битрикса: сервер (UI\IblockLangMirror) отдаёт скрипту таблицу пар, всю подмену делает клиент (iblock_edit_lang.js). То же для формы раздела с полями UF_NAME_<LANG> и UF_DESCRIPTION_<LANG>.

Функция по умолчанию выключена: она вмешивается в чужую форму на работающих проектах. Включается на вкладке «Инфоблоки» вместе со списком инфоблоков; пока список ни разу не сохраняли, действует список очереди.

Как устроена подмена

Две стратегии, и выбор между ними — по таблице в buildMirrorPlan(), которую проверяет отдельный сценарий. Для стандартных полей и HTML-свойств — подмена имени: видимый базовый контрол показывает перевод и постится под именем языкового свойства, базовое значение уезжает свежим скрытым полем. Для строк, списков и sprint_editor с отрисованной целью — перенос строки целевого свойства на место базовой: значения и имена не трогаются вовсе. Множественные свойства в этой версии не зеркалятся: кнопка «Добавить» у них строится скриптом ядра, и после переноса строки её работа не проверялась.

HTML-свойство отправляется только в каноническом виде PROP[id][vid][VALUE][TEXT] плюс [VALUE][TYPE], а все контролы цели — искажённая textarea, радиокнопки типа и поле-якорь — остаются без имени. Приёмный цикл ядра читает искажённые имена только через якорь, и оставить его значило бы получить один ключ VALUE скаляром и массивом одновременно.

Правило «всё или ничего»: сначала находятся и проверяются все узлы, и только потом форма мутируется одним синхронным проходом. Источник не найден в разметке, у формы есть черновик автосохранения, визуальный редактор запущен в неопознанном состоянии — отказ целиком с плашкой, форма остаётся базовой. Худший исход этой функции — текст одного языка в поле другого, и он возникает только при частично применённой подмене.

Защита ручной правки от очереди

Без неё функцию выпускать было нельзя. Сохранение формы — обычное сохранение элемента, и очередь видела его как любое другое: ставила «проверить», не находила хеша и отправляла свойство в платный машинный перевод, который перезаписывал только что написанное руками. Полный пересчёт со сверкой находил в ручной правке «порчу» — текст сокращён вдвое, убрана ссылка — и отправлял в повтор мимо хешей.

Новая колонка MANUAL_PROPS в состоянии очереди: «этот перевод сделан человеком и соответствует вот такому оригиналу». Обработчик Event\IblockLangMirrorHandler ставит метку при сохранении формы в режиме зеркала, а правило decidePending() проверяет её после выхода «только что перевели» и до претензий сверки: решение человека сильнее претензии. Метка истекает сама, как только оригинал меняется. На вкладке очереди — счётчик правленных руками и кнопка сброса меток.

Предупреждение, которое показывается всегда

Если язык админки — целевой, а зеркало не применилось (выключено, инфоблок не в списке, нет права, форма не опознана, внешний скрипт не загрузился), вверху формы жёлтая плашка: «интерфейс на одном языке, поля показывают другой». Она подключается инлайновым скриптом до проверки опции и прав и появится на всех проектах после обновления. Это сознательно: без неё человек на английской админке молча правит русский текст — так было и до этой версии.

Права

Зеркало и метка открыты на право чтения модуля, не записи. Зеркало ничего не тратит и записывает не оно, а штатная форма, права на которую проверяет сам Битрикс. Гейт на запись создал бы ровно ту ситуацию, ради которой всё затевалось: контент-менеджер без права на модуль правит не тот язык. Право записи остаётся у вкладки «Перевод», где тратятся деньги.

Чего делать не стали и почему

После двух независимых проверок кода

Попутно

6.14.0 — 2026-09-03

Второй движок перевода: Google Gemini рядом с Яндексом.

Выбор в настройках

На вкладке автоперевода появился селектор движка. Значения по умолчанию в коде перевода больше нет: захардкоженный движок означал бы, что настройка врёт, а разбираться пришлось бы по счетам.

Рядом с выбором — во сколько раз движок отличается по цене. Числа приходят с бэкенда вместе с балансом и здесь только запоминаются: цены провайдеров и курс живут там и меняются чаще, чем обновляется модуль. Показывать устаревшую цифру хуже, чем не показывать никакой.

Показываются оба края, а не одно среднее. На коротких полях Gemini примерно вдвое дешевле Яндекса, на длинных текстах — примерно вчетверо дороже: среднее число здесь было бы враньём.

Что осталось прежним

Запасной движок для обоих — машинный переводчик Яндекса, и отметка машинного перевода работает как работала.

Накопленные переводы остаются в силе. Ключ кеша на бэкенде получил движок, но для Яндекса формула прежняя байт в байт — обесценить кеш разом значило бы перевести всё заново за ваш счёт.

Историческое имя движка gpt понимается как Яндекс. Оно могло остаться в настройках от прежних версий и после обновления должно означать то же самое, а не молча стать чем-то другим.

Осторожность в мелочах

Неизвестное имя движка приводится к Яндексу, а не роняет перевод: значение в настройке могло быть поправлено руками или прийти от будущей версии модуля. А вот отправить бэкенду имя, которого он не знает, — значит уронить каждый запрос на проверке контракта.

6.13.0 — 2026-09-02

Инфоблок, который не переводился ни с какого раза.

Значения читались подготовленными к выводу, а не сырыми

CIBlockElement::GetList() и CIBlockSection::GetList() кладут каждое поле в результат дважды: KEY и ~KEY. Под тильдой — то, что лежит в базе; без тильды Битрикс прогоняет значение через htmlspecialcharsEx(), готовя его к выводу на страницу. Модуль читал ключи без тильды — все шесть мест, где он вообще берёт значения.

На простом тексте разницы нет, поэтому ошибка жила незаметно. Видно её только на значениях с разметкой:

в базе:      <p>9</p>
читалось:    &lt;p&gt;9&lt;/p&gt;

Дальше происходило следующее. На перевод уходил текст, который изображает разметку; модель разворачивала сущности обратно в теги — единственное разумное поведение; сверка тегов на бэкенде обнаруживала «в исходнике 0 тегов, в переводе 2» и объявляла перевод негодным. Включалась штатная машинерия: деление пачки пополам до одного текста, повтор, машинный переводчик. Инфоблок из шести таких значений стоил около 18 000 токенов за проход и не переводился никогда.

Записываются значения через CIBlockElement::Update() и SetPropertyValuesEx(), которые ждут сырое. То есть модуль читал экранированное, а писал сырое.

То же чтение кормило сверку переводов

Третье место оказалось дороже двух первых. TranslationQueue::extractValue() готовит пару «источник — перевод» для сверки. Перевод лежит в базе сырым, с настоящими тегами; источник читался экранированным. Сверка сравнивала одно с другим и честно докладывала о расхождении разметки там, где всё цело.

Само по себе это был бы ложный отчёт. Но при теге из списка тяжёлых — a, table, div, img — расхождение получает высокую важность, а высокая важность ставит свойству «ждёт перевода». Свойство уходило в повтор, снова переводилось, снова не сходилось при сверке — и так на каждом проходе. Абзацы <p> в этот список не входят, поэтому инфоблок из примера выше крутился без этой петли; элементу со ссылкой внутри повезло меньше.

Чтение вынесено в \mrLexndr\Translate\Iblock\RawValue — один класс на все три места: контракт, размазанный по трём файлам, однажды отстанет в одном из них.

Что это значит при обновлении. У значений с разметкой меняется источник, а с ним и его хеш, — такие свойства уйдут в повторный перевод и будут оплачены заново. Ровно они сейчас и сломаны. Простой текст экранирование не меняло: его хеш прежний, и повторно он не переводится.

Дело не в типе свойства, а в символах внутри значения: htmlspecialchars() трогает <, >, " и &. Значение без них в обоих видах совпадает побайтово.

Список повторных переводов можно получить заранее

cli/raw_value_probe.php — только чтение, денег не тратит. Считает набор тем же способом, каким очередь решает, что ждёт перевода: извлекает значение обоих видов и сравнивает хеши. Показывает, сколько значений изменится, каких полей они касаются, какие это элементы и сколько выйдет повторных переводов с учётом числа языков:

php cli/raw_value_probe.php
php cli/raw_value_probe.php --iblock=3
php cli/raw_value_probe.php --csv=/tmp/recount.csv

Пустой результат — тоже ответ: значит, экранирование в этих инфоблоках ничего не ломало и причина была другой.

6.12.0 — 2026-09-02

Две находки первого аудита, обе про то, что до боевых сайтов не доезжало.

Фатал шага очереди

dropUntranslatable() был объявлен с двумя параметрами, а вызывался с тремя: третьим передавался признак «осталось ещё» по ссылке. На PHP 8 это ArgumentCountError — фатальная ошибка прямо в шаге очереди.

Срабатывало на паре «инфоблок + язык», у которой нет карты свойств: агент ловил исключение и после пяти таких ошибок останавливал фоновое задание целиком. Метод теперь возвращает массив, ссылочных параметров нет, и рассогласовать нечего.

В том же месте нашлись ещё два дефекта, оба про тот же признак:

Обновление через Маркетплейс не запускало миграции

install/updater.php не существовало. При обновлении через сервис обновлений Битрикс не вызывает CModule::DoUpdate() — он выполняет updater<версия>.php из корня архива. Без этого файла подменялся только PHP модуля, а JS инлайн-редактора, стили, эндпоинт в /bitrix/tools/, заглушки в /bitrix/admin/ и колонки таблицы состояния оставались от прежней версии. Получалась худшая из пар — новый серверный код и старый клиентский.

Файл написан по образцу mrlexndr.contacts и учитывает две ловушки, на которых ломались соседние модули: скрипт исполняется из каталога распакованного архива, поэтому установщик ищется по трём путям (сперва local, затем bitrix, и только потом архив), и одного InstallDB() мало — без InstallFiles() клиентская часть не обновляется.

Требование к сборке пакета. Файл в install/ сам по себе не запускается никогда. Сборка обязана положить его копию в корень архива обновления под именем updater<VERSION>.php — например updater6.12.0.php. Скрипта сборки пока нет ни у одного модуля; это отдельная задача (T44 аудита).

Две проверки, которых не хватало

Обе ловят класс ошибок, невидимый для php -l.

tests/offline/static_call_arity.php разбирает токенизатором объявления и вызовы статических методов модуля и сверяет число аргументов. Проверено на возвращённом дефекте: краснеет с указанием файла и строки, при том что php -l тот же код принимает. Сейчас сверяется 278 вызовов.

tests/offline/updater_contract.php стережёт договор скрипта обновления: вызываемые методы существуют и публичны, класс установщика тот самый, пути ищутся в правильном порядке, обязательные вызовы на месте.

Оба сценария проверены мутациями — то есть их сначала заставили покраснеть на заведомо сломанном коде, и только потом сочли рабочими. Первая версия договора мутацию пережила: она искала имена методов по всему файлу и находила их в комментарии, поэтому удаление самого вызова не замечала.

6.11.0 — 2026-09-02

Появилась сверка переводов с оригиналами: инструмент, который находит уже записанный испорченный перевод, не обращаясь к API и не тратя денег.

Зачем. На стороне SaaS длинный текст резался на куски вслепую — через каждые N символов, без оглядки на разметку. Разрезы попадали в середину слова, внутрь HTML-сущности и внутрь тега; каждый кусок переводился отдельным запросом, ответы склеивались встык. На боевом сайте это дало статьи с незакрытой таблицей внутри ячейки, потерянными ссылками и склейками вида «long loadsFor batches». Нарезку починили на бэкенде, но уже испорченные переводы остались в базе, а проверить десятки тысяч элементов глазами нельзя.

Что проверяется

Service\TranslationIntegrity сравнивает оригинал с переводом. Четыре проверки, все с высокой уверенностью — ложное срабатывание здесь дороже пропуска: оно отправляет целый перевод в платный повтор.

Код Что значит
TAGS_MISMATCH состав тегов не совпал: потеряна ссылка, появилась лишняя таблица
TAGS_UNBALANCED в оригинале теги закрыты, в переводе нет
TOO_SHORT перевод меньше половины оригинала по видимому тексту
BROKEN_ENTITY обрубок сущности: «&nbsp2200» или «;2200»

Порядок тегов не сверяется: перенести закрывающий тег выделения на конец предложения модель вправе, в другом языке оно ложится иначе.

Для иероглифики порог длины отдельный — 0,2 вместо 0,5. Китайский записывает то же содержание втрое короче, и с общим порогом первый же прогон дал 63 выдуманные претензии из 128.

Проверяются все переводимые пары: стандартные поля (NAME, PREVIEW_TEXT, DETAIL_TEXT), строковые свойства, множественные и sprint-редактор. Списочные пропускаются — они копируются по XML_ID, модель их не касается.

Вес находок

Разбирать десятки претензий вперемешку невозможно, поэтому у каждой есть вес:

Где смотреть и как чинить

Отчёт — cli/integrity_scan.php, работает и из консоли, и из браузера с правом записи на модуль. Результат хранится в колонке BROKEN_PROPS таблицы состояния; очередь её не читает.

Кнопка «Пересчитать все элементы» теперь заодно сверяет переводы, и критичные находки уходят в очередь наравне с изменившимся исходником. Интерфейс при этом прежний.

Три ограничения, без которых так делать было нельзя:

Заодно

Что нужно сделать при обновлении

Колонка BROKEN_PROPS добавляется установщиком при нажатии «Обновить». Если кнопки нет, добавьте вручную:

ALTER TABLE b_mrlexndr_translate_state ADD COLUMN BROKEN_PROPS text NULL;

Прежде чем переводить найденное заново, убедитесь, что SaaS-бэкенд обновлён: на старом повторный перевод воспроизведёт ту же порчу.

6.10.0 — 2026-09-01

Типографика вынесена в модуль mrlexndr.corrector. Копия UniversalSpacer, жившая в переводе, удалена.

Зачем. Копий было две, и они разошлись. В корректоре типограф пережил два аудита и вырос до 896 строк; в переводе остался на 214, и общих строк между ними было 22 %. Правки в корректор доезжали, обратно — нет. Расхождение видел check-modules.php и фиксировал CONVENTIONS.md, но перенос откладывался дважды. Дальше откладывать нечего: копия не отставала, а портила текст.

Текст Копия в переводе Типограф корректора
<pre>в доме 10 кг</pre> <pre>в&nbsp;доме 10&nbsp;кг</pre> без изменений
год назад год&nbsp;назад без изменений
ООО НПО МЧС РФ работает ООО&nbsp;НПО&nbsp;МЧС&nbsp;РФ&nbsp;работает без изменений
В 2024 компания В&nbsp;2024&nbsp;компания В&nbsp;2024 компания
Он сказал "привет" ей Он&nbsp;сказал "привет" ей Он сказал «привет» ей
Москва - столица Москва&nbsp;- столица Москва&nbsp;— столица
80 000 тегов (1,2 млн символов) 114,5 с 0,25 с

Связывалось любое слово короче четырёх букв, а не служебное, поэтому аббревиатуры склеивались в цепочку, которая не переносится совсем. Число липло к любому следующему слову, а не к единице измерения. Содержимое pre и code не защищалось: неразрывные пробелы уезжали внутрь примеров кода. Кавычек и правил тире не было вовсе. Восстановление разметки шло str_replace с массивом, то есть квадратично — отсюда две минуты на большом тексте.

Что изменилось в коде. Появился фасад Service\Typography. Он зовёт публичный метод корректора, а при любом сбое возвращает исходный текст: терять оплаченный перевод из-за типографики нельзя. Lang::typo() и Lang::translateTexts() сохранили сигнатуры и работают через него же.

Корректор — зависимость необязательная. Модули продаются порознь, и жёсткое требование сломало бы тех, кто купил только перевод. Без корректора текст записывается как есть, в журнал уходит одна запись TYPO_CORRECTOR_MISSING за процесс, а на вкладке «Автоперевод» рядом с галочкой висит предупреждение. Молчаливый отказ — та же ловушка, из-за которой кнопки очереди раньше просто ничего не делали.

Старая сборка корректора отвергается явно. Версия корректора 1.0.12 существует с двумя несовместимыми сигнатурами фасада: третий аргумент — режим plain/HTML — добавили, не подняв номер версии. Лишний аргумент PHP у пользовательской функции отбрасывает молча, поэтому на старой сборке в поле NAME элемента снова уехала бы сущность &nbsp; — без ошибки и без записи в журнале. Теперь возраст фасада проверяется, старый корректор считается недоступным, а вкладка «Автоперевод» предлагает его обновить: молча выдавать неверный результат хуже, чем не выдавать никакого.

Заодно.

Чего эта версия НЕ делает: санитайзера на пакетном переводе словаря. Находка T3 первого аудита («пакетная запись словаря из SaaS без HtmlSanitizer») остаётся открытой сознательно. Белый список санитайзера рассчитан на разметку инлайн-редактора; всё прочее он разворачивает в текст, и тег без текстового содержимого превращается в пустоту: <img src="…">, <table>…</table>, <hr> дают пустую строку, а «Отдел R&D» — «Отдел R&D». В словаре такая разметка живёт штатно — её кладут туда и миграция lang-файлов, и правка HL-блока в админке ядра. Пустое значение не записывается, строка остаётся с пустым переводом, а выборка берёт именно такие строки: тот же текст уходил бы в платный API на каждом прогоне, бесконечно. При этом отказ от санитайзера здесь не расширяет доверие к API — ответ того же запроса уже пишется без всякой проверки в свойства элементов и поля разделов на тех же публичных страницах. Задача решается не белым списком инлайн-редактора и не в этой правке. Причина записана прямо в коде, рядом с местом записи, и закреплена сценарием tests/offline/dictionary_batch.php.

При обновлении. Ничего перенастраивать не нужно. Если типографика была включена, поставьте mrlexndr.corrector не ниже сборки с трёхаргументным Lang::typo() — иначе типографика перестанет применяться, о чём предупредит вкладка «Автоперевод». Значения auto_typo и все переводы сохраняются.

6.9.0 — 2026-08-30

Второй сплошной разбор кода, на этот раз чужими глазами: четыре независимых агента читали свои зоны, не видя выводов первого аудита. Найдено около шестидесяти дефектов, закрыто сорок девять. Подробности — AUDIT_ROUND2.md.

Три самых тяжёлых.

Деньги. Остановка по балансу на sprint-пути не доходила до списания — Яндексу заплачено, с клиента не взято. Запись о списании по ключу жила вечно при суточном кеше ответа: тот же ключ через сутки означал новую работу, которая уходила бесплатно. Частичное списание занимало ключ клиента, и повтор после пополнения счёта тоже был даром. Повторный частичный возврат банка не списывался. Баланс хранился с двумя знаками при учёте в шести — списание меньше половины копейки пропадало. Клиент, заведённый в админке, стартовал со ста рублей.

Данные. Перенесённый между инфоблоками элемент навсегда выпадал из очереди. Первая правка элемента без точки отсчёта растворялась — «работает через раз». Множественное свойство теряло значение и сдвигало остальные. Импорт CSV обходил проверку разметки и портил фразы апострофом. Язык без поля словаря ронял весь сайт фатальной ошибкой.

Интерфейс. Статистика по инфоблокам показывала нули всегда. Ошибка одного шага бросала всю пару и рапортовала «готово» при непустой очереди. Остановленное задание показывалось как «агент умер». Кнопки очереди при недостатке прав молча ничего не делали.

Обновление. DoUpdate не обновлял файлы вовсе: JS, CSS, заглушка и тема оставались от прошлой версии — новый серверный код работал со старым клиентским. Это же означает, что все правки прошлых версий в JS до боевых проектов не доезжали.

Три миграции на бэкенде: точность баланса, признак машинного перевода с отметкой использования, право на админку и домен «на подтверждение».

6.8.0 — 2026-08-29

Сплошной разбор обоих репозиториев вторым читателем и правки по всем находкам. Полный список — AUDIT.md, отчёт о работах — AUDIT_RESULT.md.

Деньги.

Безопасность.

Данные.

Скорость и устойчивость.

Интерфейс.

Типографика. UniversalSpacer вызывался только из Lang::translateTexts(), а её звал единственный файл — cli/smoke_translate.php. Весь боевой перевод шёл мимо, хотя руководство обещало обратное. Теперь типограф на боевом пути и понимает формат поля: в HTML связка идёт сущностью &nbsp;, в обычной строке — самим символом неразрывного пробела. Выключен по умолчанию: включённый, он поменял бы вывод на работающих проектах. По дороге нашлась давняя ошибка — класс символов валют разбирался как имя переменной, и ни рубль, ни доллар к числу не привязывались никогда.

Убрано. Очередь синхронизации инфоблоков (её никто не запускал) вместе с неймспейсом Agents, недостижимая ветка движка v2, колонка hits в кеше переводов, неиспользуемые метод и поле.

Обновление снимает старый агент LangIblockQueue и удаляет четыре осиротевшие настройки. На бэкенде три миграции: признак машинного перевода и отметка использования в кеше, право на админку, домен клиента «на подтверждение».

6.7.0 — 2026-08-24

От чего отказался GPT, переводит машинный движок. Модель регулярно отвечает «Я не могу обсуждать эту тему» вместо перевода. Строгая формулировка снимает часть отказов, но не все, и до сих пор оставшиеся заканчивались пустой строкой: свойство висело непереведённым, а причину приходилось искать в журнале событий.

Теперь после двух попыток моделью текст уходит в машинный переводчик Яндекса. Он не собеседник, и отказываться ему нечем.

Установка добавляет колонку MACHINE_PROPS в b_mrlexndr_translate_state отдельным шагом. Без обновления установки модуль работает как раньше: отметки не хранятся, машинный перевод остаётся только в журнале событий (TRANSLATE_MACHINE_FALLBACK).

6.6.0 — 2026-08-20

Три правки по итогам стороннего разбора кода. Подробности — раздел K2–K5 в BUGS_REGISTRY.md.

6.5.3 — 2026-08-20

Починка незакрытого тега больше не ломает валидную вложенность. При сохранении фразы через inline-редактор разметка вида <span>Цена: <span>1000</span> руб.</span> превращалась в <span>Цена: </span>1000</span> руб.</span>.

Текст перед вложенным тегом и отсутствие у того атрибутов — нормальная разметка, по виду неотличимая от опечатки. Отличает их только баланс: теперь правка применяется, лишь если у тега открытий больше, чем закрытий, и за проход исправляется одна опечатка.

6.5.2 — 2026-08-20

Модуль устанавливается из /local/modules/. Раньше установка оттуда падала на первом же автозагруженном классе с «Failed opening required /bitrix/modules/…», потому что ядро узнаёт расположение модуля только при includeModule, а на установке этого ещё не произошло. Теперь пути считаются от корня сайта и от расположения не зависят.

Работающую установку в /bitrix/modules/ правка не затрагивает.

6.5.1 — 2026-08-20

Непереведённое стало видно, и модуль сам пробует ещё раз.

Почему отказавшие тексты не остаются в самой очереди: иначе счётчик никогда не дойдёт до нуля и станет невозможно отличить «ещё предстоит перевести» от «не переведётся никогда». Они вынесены в отдельный блок над очередью — заметный и с объяснением.

6.5.0 — 2026-08-20

Отказ провайдера переводить конкретный текст.

Что было. Яндекс отказался переводить абзац про очереди фур на границе: «Я не могу обсуждать эту тему». Дальше по журналу — восемь одинаковых отказов подряд. Модуль получал общий код 502, считал это временным сбоем и повторял запрос четыре раза, а очередь отправляла тот же текст при каждом прогоне.

Что стало.

Требуется обновление модуля в админке: в таблицу состояния добавляется колонка отметок. Пока её нет, модуль работает как раньше.

6.4.0 — 2026-08-19

Итог сплошной перепроверки всего, что сделано за сессию. Подробности — раздел «Сплошная перепроверка сделанного» в BUGS_REGISTRY.md.

Объём одного шага теперь ограничен по-настоящему

Стоп-кран останавливает всё, что тратит деньги

Кнопка «Остановить перевод» теперь действует и на массовый перевод инфоблока, и на перевод словаря. Раньше — только на очередь, хотя именно с массового прогона и начался случай, ради которого стоп-кран делался.

Расширенный режим свойств

Если у инфоблока есть и TITLE, и TITLE_RU, побеждает явный источник с суффиксом. Раньше оба уходили на перевод и писали в один TITLE_EN: платили дважды, а результат зависел от порядка обхода свойств.

6.3.2 — 2026-08-19

Вторая партия по аудиту словаря и фронта, раздел H в BUGS_REGISTRY.md.

6.3.1 — 2026-08-19

Разбор словаря LANG_VAR и фронтовых точек входа — того, что не попало в предыдущий проход. Подробности в BUGS_REGISTRY.md, раздел H.

Известная и не исправленная проблема: вкладка «Перевод» в карточке элемента не работает на сайтах со строгим CSP (пункт H5 в реестре, там же описано, как чинить и чем пользоваться пока).

6.3.0 — 2026-08-19

Сплошной разбор кода перевода. Ниже — то, что реально ломало консистентность, а не косметика.

Очередь застревала и переводила одно и то же по кругу

Главная находка. После успешного перевода состояние пересчитывалось против старых хешей — тех самых, из-за расхождения с которыми свойство и попало в очередь. Свойство оставалось «ждущим» навсегда:

Теперь пересчёт сразу после записи считает ждущим только то, для чего перевода так и не появилось. Неудачные записи остаются в очереди и повторяются, удачные из неё уходят.

Туда же: в очередь попадали свойства, которые модуль в принципе не переводит (например, числовые пары). Переводом их никто не занимался, и строка висела вечно.

«Перевести всё» переводило не всё

Событие правки элемента ставит только отметку «надо проверить» — сравнивать значения на каждом сохранении слишком дорого. Фоновое задание собиралось из уже посчитанного и молча пропускало свежие правки. Теперь задание идёт двумя фазами: сначала проверка изменений, потом перевод найденного.

Один шаг мог идти минутами

Размер шага считался в элементах, а не в текстах: десять элементов по пять изменённых полей — это полсотни текстов, то есть семь запросов к API подряд внутри одного HTTP-запроса. Плюс потолок времени отсчитывался заново для каждой порции, так что три порции могли законно занять больше восьми минут.

Теперь шаг ограничен числом текстов, а потолок времени — общий на весь вызов.

Платили дважды за одну работу

Ключ идемпотентности генерировался случайным. Он защищал от повторов внутри одного вызова, но стоило шагу оборваться по таймауту и повториться позже — ключ был уже другой, сервер не узнавал работу и считал её заново за отдельные деньги. Теперь ключ считается из содержимого запроса.

Вечные ошибки вместо работы

Инфоблок без пар свойств висел в очереди вечной ошибкой: каждый шаг падал с «нет пар свойств», а фоновое задание после пяти таких ошибок останавливалось целиком. Теперь такие строки очищаются один раз с записью в журнал.

Молчаливая потеря переводов разделов

Результат CIBlockSection::Update не проверялся: неудачная запись считалась переводом, и раздел оставался без перевода при отчёте «успешно».

Нагрузка

Циклы в админке

Шаг, не сдвинувший курсор, останавливает цикл. С прежним next_cursor || 0 цикл уходил в начало списка и гнал платные шаги по кругу.

6.2.0 — 2026-08-19

Фоновый перевод без открытой вкладки

Прогон очереди больше не привязан к браузеру. Кнопка «Перевести всё в фоне» ставит задание, а шаги делает агент Битрикса: вкладку можно закрыть, перевод продолжится сам.

Задание живёт в настройках модуля, поэтому прогресс виден с любого компьютера, а не только в той вкладке, откуда запускали.

Требуется запуск агентов по cron. Без него агент выполняется внутри запроса случайного посетителя — шаг перевода будет ждать он, и сайт станет медленнее, а не быстрее. Пока cron не включён, кнопка не показывается и вместо неё висит объяснение. Как включить — в docs/USER_GUIDE.md.

Защиты внутри задания:

Стоп-кран

Кнопка «Остановить перевод» на вкладке очереди. Флаг проверяется на сервере перед каждым оплачиваемым шагом, поэтому закрытая вкладка или ошибка в админке больше не оставляют прогон работать сам по себе.

Сайт не должен ложиться во время перевода

Две причины нагрузки, обе в записи результата:

Запрос к API больше не бывает длинным

Пачка резалась на стороне сервера, а по HTTP уезжала целиком: 19 HTML-текстов считались больше минуты, модуль отваливался по таймауту и повторял запрос, а сервер продолжал считать и платить. Теперь один запрос — не больше 8 текстов и ~6000 символов, и разойтись админка с сервером могут максимум на одну порцию.

6.1.0 — 2026-08-18

Очередь перевода

Новая вкладка «Перевод → Очередь перевода»: модуль запоминает, что и когда было переведено, и показывает, где исходный текст с тех пор изменился.

Автоматического перевода нет и не планируется. События только копят информацию, деньги тратятся исключительно по кнопке. Это осознанный отказ: автоперевод на сохранение элемента означает, что обмен с 1С на 10 000 позиций молча превращается в счёт, который видишь постфактум.

Как устроено:

Настройки: список отслеживаемых инфоблоков и исходный язык. Пока инфоблоки не отмечены, события не делают ничего.

Прочее

6.0.0 — 2026-08-17

Ревизия модуля по итогам аудита. Полный разбор с ID пунктов, файлами и строками — в BUGS_REGISTRY.md; ниже только то, что меняет поведение.

Требует внимания при обновлении

  1. Выложить файлы и нажать «Обновить» в Marketplace → Установленные решения. Именно DoUpdate() дочиняет структуру: недостающие поля словаря и уникальный индекс по UF_CODE. Простой выкладки файлов недостаточно.
  2. Открыть «Инфоблоки → Языки», проверить отметки и сохранить список. До сохранения модуль работает по автоопределению и показывает предупреждение. Языки без сайта, попавшие в список случайно, снимите.
  3. Проверить «Служебные → Языки и свойства». Если прежние версии наплодили комплекты свойств для лишних языков, здесь их видно: сначала выгрузка в CSV, потом удаление пустых.
  4. Посмотреть на страницы модуля. Красный баннер о структуре БД означает, что не удалось создать уникальный индекс — в журнале будут конкретные дублирующиеся коды.

Языки

Синхронизация свойств

Перевод

Массовый прогон

Фронт и режим правки

Админка

Прочее

Бэкенд (отдельный репозиторий)

Правки живут в отдельном репозитории бэкенда и выкатываются отдельно от модуля.

Порядок выкладки бэкенда: git pull, php artisan migrate --force, сброс кешей конфига и маршрутов.

5.3.4 — 2026-06-03

Последняя версия до ревизии. История до неё не велась.