История версий
Файл создан скриптом 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-блоке, ни таблица состояния очереди. Это контент, а не служебные данные. Так было и раньше, но человеку об этом не говорили; теперь сказано прямо на первом шаге.
Удаление проверяет подлинность запроса
Проверки сессии при удалении не было вовсе. Теперь она стоит до первого разрушающего действия, а не после: проверка, стоящая после, подавила бы сообщение об успехе, но не сами разрушения.
Из консоли проверка пропускается — сессии там нет, и требовать её значило бы сделать удаление скриптом выкладки невозможным.
Установка отвечает, а не молчит
- версия PHP проверяется до первого изменения в базе: раньше модуль устанавливался на неподходящий PHP и падал позже, уже наполовину установленным;
- неудачное копирование файлов больше не проглатывается. Установка при этом не отменяется — без своих стилей модуль работает, — но в сообщении сказано, какие каталоги проверить;
DoInstall()иDoUninstall()возвращают результат ядру;- из консоли страницы админки не подключаются: раньше их HTML печатался прямо в вывод скрипта выкладки;
- на странице после установки появилась кнопка возврата в список.
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:alert(1)". Числовые
сущности libxml разворачивает сам, а : — нет: это HTML5, а
парсер libxml знает HTML4. Браузер HTML5 знает, поэтому ссылка
срабатывала.
Схема теперь сравнивается с тем, что из атрибута получит браузер. Возврат строки байт в байт, когда чистить нечего, сохранён.
Права на сущности: закрыты ещё два входа
- Скан очереди по запросу человека считает права на элементы и разделы. Оператор с правом на инфоблок, но без права на часть элементов, полным сканом клал в очередь всё подряд, а очередь потом это переводила. Скан фонового задания работает как раньше — потерь нет, пропущенное положит ближайший проход агента.
- SEO-пакет проверяет право на владельца строки шаблона.
b_iblock_ipropertyхранит шаблоны не только инфоблока, но и конкретных элементов и разделов.
Прочее
Непригодное значение 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 читала сырое значение ради
симптома «& в названии» на одном проекте — там шаблон экранирует
NAME сам и потому удваивает экранирование. После этого выпуска
симптом там вернётся, и чинить его нужно в шаблоне: убрать
htmlspecialcharsbx() вокруг NAME, потому что ядро отдаёт это поле
уже подготовленным.
Пакетный перевод считает права на каждый элемент
Одиночный перевод проверял право записи на конкретный элемент, пакетный — только на инфоблок. На инфоблоке с расширенными правами человек переводил и перезаписывал элементы, которых ему не видно.
У очереди правило осталось прежним, и это решение: её шаг выполняется на чужом хите, и счёт прав текущего посетителя терял бы переводы.
Словарь: второй режим фильтра
Ответ API для словаря шёл мимо всякой проверки. Белый список туда
вешать нельзя — это пробовали в 6.10.0 и откатили. Поэтому режим
чёрный: убираются script, обработчики событий, javascript:-адреса и
комментарии, а картинки, таблицы и разделители проходят. Если убирать
нечего, строка возвращается байт в байт.
Прочее
- адрес страницы экранируется в семи местах админки (
GetCurPage); saas_api_base_urlпринимается только поhttpsи с хостом;- токен режима разработки берётся из источника случайности, а не из зашитой в поставку соли;
- текст непредвиденного исключения уходит в журнал ядра, а не в ответ;
- адрес оплаты проверяется перед переходом;
- подмена подписи в меню админки и в способах доставки и оплаты экранируется;
- возвращён потерянный в 7.4.6 корень сайта в
cli/seo_recon.phpиcli/lang_mirror_recon.php: обе разведки считали пути от корня файловой системы.
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
Правило вместо списка: запись знака сущностью теперь равна самому знаку.
Что было не так
До этого выпуска перечислялись три написания неразрывного пробела, и
этого хватало ровно до первой встречи с  : сам тонкий пробел
схлопывался, а его запись сущностью меняла хеш. Непоследовательность,
которая повторилась бы с любым другим написанием — а модуль правки
текста, как выяснилось на боевом стенде, пишет и знаками, и сущностями.
Что сделано
Сущность разворачивается, и решение принимается по тому, что получилось:
пробельное становится пробелом, невидимое исчезает, остальное остаётся
сущностью нетронутым. & и < — это текст.
Теперь закрыты все написания разом:  ,  ,  ,
 ,  ,  , ­, ­ и любые другие,
известные HTML5.
Пробельное становится пробелом, а не пустотой. Неразрывный пробел разделяет слова: выбрось его — и «NNN BC» станет «NNNBC», а разные тексты начнут давать один хеш.
Соединители нулевой ширины и направляющие метки не трогаются. Они невидимы сами по себе, но меняют то, что видит читатель: управляют слитностью букв в арабском, склеивают «👨👩👧» из трёх знаков. Объявить такие тексты одинаковыми — та же ошибка, только с другого конца.
Что надо сделать после обновления
Это четвёртая смена правила подсчёта, поэтому ремонт надо прогнать
снова: php cli/hash_repair.php, затем --apply. Прежнее правило
добавлено в список известных, так что ремонт узнает старые хеши.
7.4.3 — 2026-09-18
Ремонт хешей перестал угадывать прежний текст и начал его читать.
Что выяснилось
Исходники на боевом стенде правил не человек и не модуль перевода, а модуль правки текста — 1 сентября, сразу после перевода. Правки косметические, но хеш меняли честно:
было: ...для туристических лыж: NNN BC, Xplore, NN 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, то последовательность . Зависит от того, чем текст
правили в последний раз: визуальный редактор, импорт и типографские
модули пишут по-разному.
Читателю разницы нет никакой — обе записи дают один и тот же пробел. А хеш менялся, и перевод уходил в платный повтор.
Что сделано
- Записи неразрывного пробела приравнены.
, и приводятся к обычному пробелу перед хешированием — там же, где схлопываются пробельные знаки Unicode. Сущности, которые пробелом не являются, не трогаются. - Ремонт восстанавливает прежнюю запись текста, а не только прежнее правило: к нынешнему значению примеряются и запись сущностью, и экранирование ядра, и все три правила подсчёта — все сочетания.
- Отчёт показывает примеры необъяснённого. Раньше печатались только починенные поля, и разбирать остаток приходилось вслепую. Теперь видно и то, что ремонт не узнал, — по живому примеру, а не по догадке.
Чего выпуск не делает
Не переводит заново ничего и не тратит денег.
7.4.1 — 2026-09-18
Починка последствий правки 6.13.0: весь каталог заказчика числился ждущим перевода, хотя ни один текст не менялся. Новых возможностей нет.
Что случилось
Правка 6.13.0 переключила чтение значения из Битрикса с экранированного на сырое. Правка верная — без неё разметка в исходнике портила сверку переводов. Но тем же чтением кормится подсчёт хеша исходника, то есть памяти модуля о том, под какой текст сделан перевод, а миграции сохранённых хешей выпуск не содержал.
Битрикс при чтении без тильды прогоняет значение через
htmlspecialcharsEx(), и неразрывный пробел там превращается в
. В русской типографике этот знак стоит после коротких предлогов
и в конструкциях вроде «Часть 2» — то есть почти в каждом заголовке и
анонсе. Все хеши, записанные до 6.13.0, перестали совпадать навсегда.
Очередь честно докладывала «ждёт перевода» по всему каталогу. Заметить было некому: она выглядела работающей. На боевом стенде так набралось 1411 строк состояния из 1655, и каждая означала повторную оплату уже сделанного перевода.
Что сделано
- Ремонт хешей.
php cli/hash_repair.php— разведка, показывает числа и ничего не пишет.--apply— записывает. Для каждой строки нынешний исходник пересчитывается всеми прежними правилами; если сохранённый хеш объясняется одним из них, расхождение мнимое и хеш переписывается на нынешний. Если не объясняется ничем — не трогается: там исходник правда менялся, и жалоба очереди законна. Перевод не запускается, деньги не тратятся. - Невидимые знаки больше не меняют хеш. Выбрасываются пробел нулевой ширины, мягкий перенос, склейка слов и метка порядка байтов. Читателю их не видно, значит перевод от них не устаревает. Пробельные знаки Unicode — неразрывный, узкий неразрывный, тонкий — схлопывались в обычный пробел и раньше.
- Правило записано в руководство разработчика (раздел 15): любая правка, меняющая получение хеша, обязана выпускаться вместе с миграцией — включая правку чтения значения, которая опасной не выглядит.
Что это значит на практике
Типографский модуль, расставляющий неразрывные пробелы и мягкие переносы, теперь можно запускать по каталогу без опаски: очередь этого не заметит и на перевод ничего не отправит.
Чего выпуск не делает
Не переводит заново ничего и не тратит денег. Переводы годные: изменился не текст, а способ его хеширования.
7.4.0 — 2026-09-18
Страница «Проверка стенда» в меню модуля: паспорт проекта и автопроверки, которые ничего не пишут. Первый из двух заходов — глубокая проверка со своими сущностями и ручной чек-лист идут следующим выпуском.
Что появилось
- Паспорт стенда. Список того, что модуль видит на этом проекте: версия в файлах против версии в базе, ядро и PHP, установленные модули, выложенные клиентские файлы, языки сайтов против языков модуля, колонки словаря и SEO-блока, отслеживаемые инфоблоки с номерами и названиями, зеркало, расширенный режим, дополнительные UF разделов, справочники с полями, счётчики очереди по типам, схема состояния, предупреждения установки. Читается при открытии и ничего не меняет, поэтому смотреть его можно на любом стенде, включая боевой.
- Обзор: двадцать одна автопроверка по вашим настоящим данным, тоже без единой записи. Установка и схема, согласованность настроек, чтение по живым данным. Идёт порциями по одной группе.
- Баннер на страницах модуля, когда версия в файлах разошлась с версией в базе или установка оставила предупреждение по схеме.
Почему это сделано так
- Обзор отделён от того, что создаёт сущности. Проверки, которые только читают, заперли бы за подтверждением «только копия базы», хотя запускать их можно где угодно. Всё, что пишет, ушло во второй заход.
- Проверки объясняют, а не отчитываются. Сюда приходят затем, чтобы не идти в код, и «О1.4 отказ» этой цели не служит. Где лечение известно — оно названо: «Сохранить» на вкладке языков, «Обновить» в списке модулей, «Сохранить и создать языковые поля».
- Жёлтый и красный разведены осознанно. Инфоблок предложений можно снять из списка намеренно, и модуль это бережёт, — такая строка жёлтая, а не красная. Языковые свойства вне поиска модуль не чинит по решению 7.3.2 — тоже жёлтая. Каждая лишняя красная строка это человек, который полдня чинит работающее.
- Отказ группы не роняет зависимые, а помечает их «не проверялось». Иначе одна непочиненная схема даёт полтора десятка красных строк, и человек ищет полтора десятка проблем вместо одной.
- Наличие строк состояния, а не число ожидающих. Ноль ожидающих значит «всё переведено», и принять это за «пересчёт не запускали» было бы враньём.
- Выкладка файлов сверяется по содержимому. Версия у клиентских файлов идёт параметром в адресе, а не частью имени: старая копия лежит под тем же именем и отдаётся браузеру как новая.
- Баннер считает три чтения, а не обзор: полный обзор на каждой вкладке был бы схемой базы на каждом хите.
Чего в выпуске нет
- Глубокой проверки со своими сущностями, снимка настроек, ручного чек-листа с подстановкой ваших данных и кнопки «собрать отчёт» целиком — всё это второй заход.
- Проверки снятия модуля: удаление модуля удаляет саму страницу.
- Проверки поискового индекса со страницы: нужен элемент, про который заранее известно, что он переведён, а выбрать его может только человек. Проверка честно отсылает к готовой разведке.
Разбор перед выпуском
Независимый проход по коду нашёл одиннадцать дефектов, и главный рушил
саму посылку страницы: проверка версии сравнивала файл сам с собой.
ModuleManager::getVersion() в базу не ходит — он инклюдит тот же
install/version.php, а версии модуля в базе нет вовсе. Теперь
установка оставляет отметку сама, и сверяется она; именно так ловится
случай «выложили файлы и не нажали Обновить».
Остальные десять — в истории коммитов группы 47. Четыре из них дали бы красное или жёлтое на исправном стенде, то есть отправили бы человека чинить работающее.
Долг, закрытый по дороге
Правка 7.3.2 про ссылку вида //чужой.сайт была сделана без
офлайн-сценария, хотя отчёт его просил: мутация «разрешить два слеша»
оставалась зелёной. Сценарий заведён, в нём 26 проверок и семь мутаций.
7.3.2 — 2026-09-18
Заказчик прошёл весь модуль шестью независимыми проходами по зонам. Новых возможностей нет: это то, что должно было работать и работало не так.
Что изменилось
- Перенос значений списка больше не может подставить чужой вариант. Карта вариантов собиралась отбором по коду свойства, а такой отбор у ядра работает как поиск по образцу: подчёркивание значит «любой один знак». Коды модуля всегда с подчёркиванием, и в карту попадали варианты чужих свойств того же инфоблока. Достаточно было совпавших внешних ключей — а у «да/нет» они совпадают сплошь и рядом.
- Дополнительные UF раздела видны на витрине. Переводились они с
7.2.0, а хелпер подставлял только название и описание: перевод лежал
в
UF_SEO_TEXT_EN, шаблон читалUF_SEO_TEXTи показывал русский текст, хотя очередь докладывала «готово». - Поиск по переводу заработал. Языковые свойства стандартных полей создавались без признака участия в поиске, а умолчание ядра здесь «нет». Перевод названия в индекс не попадал вовсе: английский запрос элемент не находил. Новые свойства создаются с признаком; существующим он не дописывается, но синхронизация их называет. Заодно сниппет из HTML-свойства перестал быть словом «Array».
- Название на витрине не экранируется дважды. Декоратор подставлял подготовленное к выводу значение вместо сырого, и «Том & Джерри» показывался с сущностью.
- Зеркало языка закрыло два пути к порче. Единственный объект автосохранения на странице больше не считается своим: взяв чужой, зеркало думало, что разобралось, а настоящее автосохранение писало черновик по старым именам. И у HTML-поля, которое зеркалится без живого редактора, выключен переключатель режима: включи человек визуальный редактор после подмены, ядро подняло бы его с базовым текстом и записало базовый текст в поле перевода.
- Предложения дописываются во все четыре списка. Правило было подключено к очереди и зеркалу, а дырявы оказались соседние два на той же вкладке: режим «все свойства» и список синхронизации.
- Импорт не затирает переводы пустыми ячейками. Повторный импорт собственной выгрузки, файл из Excel, «поправить одну колонку» — каждый из этих сценариев обнулял переводы, а отчёт показывал «обновлено».
- Отметка «оригинал изменился» встаёт с первого прохода. При смене текста рядом кладётся хеш прежнего. Раньше хеш не трогался вовсе, и для фразы без него выходило обратное задуманному: она не попадала в список, а следующий сбор помечал её свежей. Устаревший перевод прятался навсегда. То же теперь делает импорт, меняя базовую колонку.
- Мелочи. Обработчики строк справочника снимаются и при пустом
списке полей, и при удалении модуля из
/local/modules/. Ссылка вида//чужой.сайтбольше не считается путём от корня. Флаги кодировщика для данных внутри тега скрипта переехали в класс модуля: три вставки в публичных классах до общего набора не дотягивались. Кеш свойств инфоблока сбрасывается вместе с картой пар.
Чего делать не стали и почему
- Закрывающий тег в
include.phpне убран. Общий стиль PHP велит его опускать, и для обычных файлов это верно. Здесь правило противоположное и записано вCONVENTIONS.mdс внешней причиной: сборщик Маркетплейса ломается наinclude.phpбез закрывающего тега. - Признак участия в поиске существующим свойствам не дописывается. Это настройки ваших свойств, и такое же решение модуль уже принял в 7.1.0 для языковых UF разделов. Отступить от него ради двух кликов значит завести второе правило на тот же вопрос. Свойства без признака синхронизация называет — включите и переиндексируйте.
- Зеркало не отказывается от HTML-полей целиком. Отчёт предлагал отказывать всякий раз, когда живого редактора нет. На загрузке страницы его нет никогда, так что это выключило бы зеркало для анонса и подробного описания — двух самых ценных полей формы. Закрыт сам переход, а не возможность.
7.3.1 — 2026-09-18
Заказчик сверил код с программой расширения охвата и нашёл то, что должно было работать в 7.0.0–7.3.0, а работало не так или не до конца. Новых возможностей здесь нет: это долг закрытой программы.
Что изменилось
- Название инфоблока больше не может сломать админку. У этого имени два пути на страницу, и в 7.0.0 закрыли только один. Данные для скрипта кодировались внутрь тега скрипта, а там разметка не экранируется ничем: название вида «закрыть скрипт» закрывало тег посреди значения. Теперь оба помощника берут один набор флагов кодировщика — тот же, что стоял у зеркала языка. Названия с кириллицей, кавычками и амперсандами остаются читаемыми.
{=iblock.name}в SEO-шаблонах на языке. Макрос отдавал базовое название инфоблока, хотя перевод в словаре лежит с 7.3.0. Обещание обратного стояло в трёх местах документации, а в код не доехало. Теперь шаблон «{=this.name} — купить в {=iblock.name}» собирается на языке целиком.{=sections.*}остаётся базовым — это сознательный отказ 7.0.0, и теперь причина записана рядом.- Пункты меню инфоблока переводятся своими подписями. Раньше все переводились названием инфоблока: на английской админке «Товары» и «Разделы» стали бы третьим и четвёртым пунктом «Catalog» подряд. Теперь страница пункта решает, какой подписью он переводится.
- Префикс служебных фраз перестал быть зарезервированным на словах. Проверка существовала в классе и в руководстве, но не звалась нигде: словарь заводил служебный код при первом показе, с текстом-заглушкой, и сборщик потом считал такую фразу своей. Автосоздание и импорт CSV новый код с этим префиксом больше не создают; отказ виден в журнале и в отчёте импорта. Перевод существующей служебной фразы импортом по-прежнему разрешён — так их и переводят.
- Мелочи. Шаг очереди по справочникам отказывал текстом про права на инфоблок, которого у справочника нет. Обработчики правки строк справочника теперь приводятся к настройке на обновлении модуля: до этого перенос сайта файлами или откат базы оставляли настройку без обработчиков, и правки молча переставали попадать в очередь. Корзина берёт код целевого свойства из карты пар, а не собирает его на месте.
Чего делать не стали и почему
- Международное обозначение единицы измерения не переводится. Оно приходит из классификатора и одинаково на всех языках: переводить «PCE» не на что. Переводится русское обозначение, оно и собиралось. Какое из двух рисует витрина, решает шаблон каталога — это пункт стендовой проверки.
- Статус заказа убран из списка служебных сущностей. Константа висела там с 7.1.0 без источника, сборщика и потребителя. У ядра названия статусов уже лежат по языкам отдельной таблицей, и заводить им фразы словаря значит поставить второй источник правды рядом с работающим первым.
- Подмена результатов поиска (7.1.0) собирает код целевого свойства так же, как это делала корзина до правки. Ответ она даёт верный, у неё свой сценарий, и переписывать выпущенное в заплатке без нужды — плохой размен. Известное расхождение, не ошибка.
7.3.0 — 2026-09-18
Завершение программы расширения охвата: отметка «оригинал изменился» для
словаря (A6.4), магазин через словарь (Б6), названия инфоблоков (Б7).
Постановка — translate-saas-plan-coverage.md; ход работ по группам —
docs/agent/.
Что изменилось
-
Словарь научился говорить, что оригинал изменился (A6.4). Очередь про словарь не знает и знать не будет: фразы правят и люди, и сборщик, и импорт. Раньше переименованная запись оставалась со старым переводом молча — перевод не пустой, ошибки нет, просто текст не тот. Теперь рядом с фразой лежит хеш текста, под который сделан перевод, и расхождение видно. На странице обслуживания появилась вкладка «Оригинал изменился»: список, кнопка «перевод проверен» (снимает отметку, сам перевод не трогает) и дорога на автоперевод.
Синхронизация свойств теперь обновляет текст переименованного свойства в словаре, а не оставляет старый: переводить с несуществующего оригинала бессмысленно. Хеш при этом не трогается — по нему фраза и попадает в список.
Фразам, заведённым до 7.3.0, отметка проставляется при первом же проходе сборщика — но только тем, у кого текст совпал с источником. Наплыва отметок это не даёт по построению: текст тот же, значит перевод сделан именно под него. Без этого весь словарь, собранный раньше, остался бы вне механизма навсегда.
Отметка одна на фразу, а не на язык. Перевели фразу на один язык — отметка снимается целиком, и остальные языки надо просмотреть самим. Перевод, залитый импортом, отметку тоже не снимает. Колонка на язык добавила бы по колонке в чужую таблицу на каждый язык проекта, и для двух-трёх языков это дороже пользы. Сказано в подсказке на вкладке.
-
Магазин переводится через словарь (Б6). Платёжные системы, службы доставки, свойства заказа, типы плательщиков и единицы измерения ядро хранит одним текстом на все языки. Теперь их названия заводятся в словарь служебными фразами по кнопке «Собрать названия магазина» на странице обслуживания и переводятся тем же пайплайном, что всё остальное. У конкурента то же самое переводится языковыми файлами по идентификатору, причём самих файлов в поставке нет.
Страница оформления заказа переводится сама — через два штатных события компонента. Компонент при этом не форкается: копия чужого компонента в поставке означает подписку на его поддержку на годы вперёд, а оформление заказа — самая дорогая страница магазина.
Модули «Каталог» и «Интернет-магазин» остаются необязательными: чего нет, то пропускается молча.
-
Инфоблок стал объектом перевода (Б7). Название, описание и подписи содержимого — «Товары», «Разделы», «Добавить товар» — ядро хранит одним текстом на все языки. Теперь они собираются в словарь вместе с названиями свойств, при той же синхронизации: их состав меняется в один и тот же момент. В меню админки названия инфоблоков подставляются сами, на витрине их отдают
IblockHelper::iblockName()иiblockLabel().Ценность здесь умеренная, и об этом сказано прямо: название инфоблока посетитель видит редко, подписи админские. Поэтому берутся только отслеживаемые инфоблоки — каждая лишняя фраза это строка в словаре, который читается на каждом запросе страницы.
Чего делать не стали и почему
- URL-шаблоны инфоблока по языкам (Б7.3) не делаются. Они осмысленны только вместе с языком в адресе страницы, а этот блок в программу не входит: на мультисайте язык уже задаёт сайт, и второй механизм на ту же задачу спорил бы с первым.
- Компонент корзины не форкнут. Событийной точки у него нет, и
вместо копии компонента сделан хелпер для
result_modifier.php:IblockHelper::substituteBasket(). Он подменяет названия товаров переводом самого элемента и единицу измерения переводом из словаря. Одна строка в шаблоне вместо чужого кода в поставке.
7.2.0 — 2026-09-18
Продолжение программы расширения охвата: торговые предложения (Б4),
UF-поля произвольных сущностей и строки highload-блоков (Б5).
Постановка — translate-saas-plan-coverage.md; ход работ по группам —
docs/agent/.
Что изменилось
-
Торговые предложения перестали теряться (Б4). Отметили товарный инфоблок — отметился и инфоблок предложений, и в списке очереди, и в списке зеркала. Раньше про него надо было догадаться: человек отмечал «Каталог», переводил и видел на витрине русские названия цветов и размеров, потому что они лежат в соседнем инфоблоке. Модуль говорит, что именно дописал; снятую отметку не возвращает — правило считает разницу списков, а не их состав, иначе оно спорило бы с человеком о его же настройке. Зеркало языка теперь работает и на отдельной форме предложения: она устроена как форма элемента, и раньше зеркало на ней рисовалось, а сохранение уходило в базовые поля.
-
Справочники переводятся (Б5). Цвета, размеры и бренды живут в highload-блоках, и подпись у строки одна на все языки. Теперь перевод кладётся в ту же строку рядом с базовым значением:
UF_NAMEиUF_NAME_EN— соседние колонки одной таблицы. Ни теневых таблиц, ни сгенерированных на лету классов для этого не нужно. Переводимые поля задаются явным списком на блок, по умолчанию пустым: среди полей справочника есть служебные, файлы и поля чужих модулей. Правка строки попадает в очередь и переводится шагом и фоновым заданием, как элемент или раздел. На витрине подпись подставляют хелперыIblockHelper::directoryName()иdirectoryNames(): свойство типа «справочник» хранит у элемента идентификатор строки, а не текст, поэтому сама собой подпись не переведётся.Обработчики правки строк регистрируются живьём, при сохранении списка полей: имя события у highload-блока складывается из его названия, а названия модулю на установке не известны — справочники заводят потом. Снимает их сам обработчик, по списку, который он же и вёл.
Что нашла проверка перед выпуском
Независимый разбор нашёл три ошибки, из-за которых перевод справочников не работал вовсе, и несколько мест, где заявленное работало дороже обещанного. Всё закрыто до выпуска.
- Правка строки справочника не попадала в очередь никогда. Отметка ставится только за отслеживаемой сущностью, а этот вопрос сводился к списку отмеченных инфоблоков. У строки справочника владелец — номер справочника, и список отвечал «не следим» на любой строке, кроме случая, когда номер справочника случайно совпал с номером отмеченного инфоблока. Событие при этом честно отрабатывало и отметку просило.
- Список справочников выходил пустым, если его спросить раньше списка полей. В админке это было замаскировано порядком вызовов, но из фонового задания или консоли обработчики снялись бы со всех справочников и больше не поставились.
- Существующие строки справочника в очередь не попадали ничем. Справочник цветов заполняют один раз при запуске каталога, событий правки по нему больше не будет. Добавлена фаза справочников в обход «Пересчитать всё».
- Подписи справочника читались целиком, без предела. Для цветов и размеров это дешевле любых выборок, а для классификатора на двадцать тысяч строк — мегабайты на каждый запрос витрины. Теперь маленький справочник читается разом, большой — только по спрошенным значениям.
- В сводке очереди группа справочника подписывалась названием инфоблока с тем же номером.
- Мелкое: номер справочника уезжал в параметр «инфоблок» у перевода текстов; отказы записи справочника были нелокализованы; «переводить нечего» приезжало отказом и ставило отметки на всю порцию; имя инфоблока в уведомлении о предложениях не экранировалось.
Чего делать не стали и почему
- Зеркало во вложенной сетке предложений (Б4.3) не сделано и
перенесено в 7.3.0. Строки сетки порождает скрипт ядра, имена полей у
них свои, и устройство сетки ни разу не снято на живой форме. Подмена
полей по догадке ломает карточку товара у всех, у кого сетка устроена
иначе. Что снять, чтобы это закрыть, написано в шапке зонда
cli/lang_mirror_dom_probe.js; до отчёта со стенда оценка не ставится. Предложения при этом переводятся: очередью, массовым прогоном и на своей отдельной форме. - Отдельной вкладки «UF-поля произвольной сущности» (Б5.1 в первоначальной формулировке) не появилось, и это осознанно. Общий синхронизатор языковых полей действительно принимает любую сущность, но интерфейс «выберите сущность, выберите поля» открывал бы дорогу к заведению колонок у пользователей, заказов и чужих модулей, где переводить их некому: очереди для них нет, писать перевод нечем, показывать негде. Сделана та сущность, которая нужна и замкнута целиком, — строка highload-блока. Остальные появятся, когда под них будет очередь и подстановка, а не раньше.
- Множественные поля справочника пар не образуют: сборщик порции одиночный. Пара просто не строится — это честнее, чем каждый проход докладывать «пропущено».
7.1.0 — 2026-09-18
Продолжение программы расширения охвата: конвенция служебных фраз (A6),
названия свойств (Б2), разделы в очереди (Б3), поиск по языкам (Б8).
Постановка — translate-saas-plan-coverage.md; ход работ по группам задач —
docs/agent/.
Что изменилось
- Служебные фразы словаря живут по конвенции (A6.1–A6.3). Названия
чужих сущностей получают коды вида
SYS_IBPROP_123_NAME(Service\SystemPhrase), и по коду теперь видно, чьё это имя. ПрефиксSYS_зарезервирован: «Мусорщик» такие фразы не показывает и удалить не предлагает — иначе он предложил бы снести названия всех свойств разом, потому что в файлах проекта их нет и счётчик использований у них нулевой. Заводит их сборщикService\SystemPhraseHarvesterзаранее, а не первый показ страницы: иначе пришлось бы оборачивать в вызов словаря каждый вывод названия, а первый посетитель видел бы заглушку. Источники подключаются черезService\SystemPhraseSource, по одному на сущность. Цену роста словаря показываетcli/vocab_bench.php. - Названия и подсказки свойств переводятся (Б2). У свойства инфоблока
одно поле названия на все языки, поэтому перевод живёт в словаре, а
модуль подставляет его там, где название видно: в режиме зеркала языка
подписи полей формы перестали быть русскими на английской админке, а на
витрине названия отдают хелперы
IblockHelper::propertyName(),propertyHint()иpropertyFilterHint(). У подсказки фильтра два уровня: своя подсказка раздела сильнее общей подсказки свойства, как и настройки умного фильтра в ядре. Названия и общие подсказки заводит сборщик сразу после синхронизации свойств: именно там состав свойств меняется. На базовом языке хелперы словарь не спрашивают вовсе. Варианты списков переводились и раньше, теперь это описано: подписи вариантов переводятся при первом переводе элементов инфоблока на язык, а значения элементов копируются поXML_IDварианта. - Разделы в очереди (Б3). Правка названия или описания раздела попадает в «что ждёт перевода» и переводится шагом очереди и фоновым заданием, как элементы. Переводятся только разошедшиеся поля: без уточнения правка одного названия оплачивала бы перевод всего раздела заново. Дополнительные UF-поля раздела переводятся по явному списку на вкладке «Инфоблоки» (коды через запятую, по умолчанию пусто): молча размножать все UF сущности нельзя, там служебные поля и поля чужих модулей. Клонируются только строковые поля и поля блочного редактора — у списка свои варианты, у файла переводить нечего. Значение блочного редактора у раздела переводится своим путём, как у элементов: это JSON, и обычный путь порции сломал бы разметку блоков. В отчётах вкладки очереди у разделов теперь видны имена.
- Поиск по языкам (Б8). Выдачу штатного поиска переводит хелпер
IblockHelper::substituteSearchResults(): индексная запись у элемента одна и содержит все языки, поэтому по переводу он находится, а вот заголовок и сниппет собраны из базовых полей и без подмены оставались русскими. Языковые UF-поля разделов теперь создаются участвующими в поиске: раньше стояло «нет», и перевод названия раздела в индекс не попадал вовсе. Проверить состав индекса помогаетcli/search_recon.php. Морфология остаётся одна на запись, по языку сайта инфоблока: это ограничение ядра, и оно названо прямо.
Что нашла проверка перед выпуском
Независимый разбор возможностей 7.1.0 нашёл одну ошибку, из-за которой часть заявленного не работала вовсе, и несколько мест, где заявленное работало дороже обещанного. Всё закрыто до выпуска.
- Дополнительные UF раздела не попадали в очередь. У раздела всё лежит полями строки, а общий читатель значений искал такое поле под ключом свойства, которого у раздела не бывает. Источник выходил пустым, пара молча выпадала из очереди как «переводить не с чего», и правку такого поля модуль не замечал. То же касалось перевода не с базового языка. Массовый перевод при этом работал, поэтому со стороны это выглядело как «очередь игнорирует поле».
- Полный пересчёт не обходил разделы. Они попадали в очередь только по событию сохранения, а на готовом каталоге таких событий нет: перевод разделов включился бы только после того, как каждый раздел пересохранят руками. Добавлена фаза разделов в обход «Пересчитать всё».
- Значок запасного движка у разделов не показывался. Отметка приезжала с кодом целевого поля, а сводится она по кодам источников — и терялась целиком.
- Шаг очереди по разделам не считал записанное сборщиком напрямую (поля блочного редактора), не глушил теговый кеш на время записи и докладывал об ошибке, если в отметках оставался код переименованного свойства. Всё три поправлено по образцу пути элементов.
- Названия свойств стоили запроса на свойство. На странице каталога с полусотней свойств это полсотни лишних запросов; теперь исходники читаются целым инфоблоком на первом промахе.
- «Мусорщик» прятал лишнее. Отбор по коду фразы работал как поиск вхождения, поэтому из списка неиспользуемых пропадали и обычные фразы проекта, в коде которых встречались те же буквы. Служебные фразы отсеиваются разбором кода, а не отбором базы.
- Поле дополнительных UF рисовалось для каждого инфоблока сайта. На проекте со ста инфоблоками вкладка превращалась в сотню полей ввода, а сохранение — в сотню записей настройки. Показываются отслеживаемые, сохранение идёт одной записью.
7.0.0 — 2026-09-18
Программа расширения охвата, фаза A и блок SEO. Постановка —
translate-saas-plan-coverage.md в корне рабочей папки; ход работ по группам
задач — docs/agent/.
Перед выкладкой на боевой проект — чек-лист стенда: сначала копия базы, потом «Обновить» (меняется уникальный ключ таблицы состояния очереди), затем проверки по списку.
Что изменилось
-
Очередь знает тип сущности (A2). В таблице состояния появилась колонка
ENTITY_TYPE, уникальный ключ стал(ENTITY_TYPE, IBLOCK_ID, ELEMENT_ID, LANG)— владелец в ключе нужен строкам HL-блоков, у которых идентификаторы считаются в каждой таблице отдельно. МетодыTranslationQueueпринимают ссылкуService\EntityRef {type, ownerId, id}вместо пары «инфоблок + элемент»;forgetElement()сталforget(EntityRef). Сводка, фоновое задание и вкладка очереди работают с ключом «тип|инфоблок|язык», шаг перевода берёт строки только своего типа и ветвится до сборщика. Для элементов внешнее поведение не изменилось. -
Миграция схемы без права на половину. Правило миграции вынесено в чистый
Service\StateSchema: колонка (всегда), проверка дублей по новому ключу, одинALTERсDROP INDEXиADD UNIQUE INDEX. При дублях смена ключа откладывается, колонка со старым ключом остаётся рабочей; отказ уходит в свою опциюstate_schema_warning, обновление через Маркетплейс сообщает о нём вerrorMessage, вкладка очереди показывает предупреждение. Элементы при этом переводятся как раньше, новые типы не пишутся, пока опцияstate_schemaне равна 2. Немигрированную таблицу (файлы выложены без «Обновить») очередь переживает: сводка пуста, вкладка открывается с предупреждением, отметки правок уходят в журнал. -
Одна карта пар для элементов и разделов (A1). Карта разделов строится в формате карты элементов (
TARGET_CODE,TYPE,USER_TYPE,MULTIPLE,SOURCE_IS_FIELD), приведение в зеркале удалено. Схему полей отдают поставщикиIblock\Schema\*: свойства инфоблока и UF-поля любой сущности (только строковые — у списков UF варианты переносить нечем). КромеUF_NAME/UF_DESCRIPTIONразделу можно назначить дополнительные UF явным списком (опцияsection_extra_uf, интерфейс — в 7.1.0); базовое поле для них — UF без суффикса. Кеш карт на запрос различает тип сущности. Вопрос «считать ли код без языкового суффикса источником» тоже задаётся по паре «тип сущности + владелец»: у элемента ответ по настройке инфоблока, у раздела всегда «да», у строк HL-блока «нет» до 7.2.0. -
Обратная схема пар удалена (Р2). Пара «свойство базового языка → стандартное поле» (
NAME_RU → NAME) не учитывала язык: перевод на EN, затем на DE оставлял в поле только DE. Теперь она не строится, ключаTARGET_IS_FIELDв карте нет, отказ зеркалаreverse_schemeушёл. Синхронизация показывает предупреждение, если уNAME_RUнетNAME_<ЦЕЛЬ>: она заведёт языковые свойства, и перевод пойдёт в них. На transgid.com и pavelrudenko.ru такой схемы нет (docs/SEO_FACTS.md). -
Языковые UF-поля создаёт одно место (A3).
Service\UfLangFieldSynchronizer: чистый план «какие поля<БАЗА>_<LANG>создать» и исполнитель. Поля раздела и колонки словаряUF_LANG_<LID>заводятся через него с прежними типами, настройками и подписями; языковые колонки SEO-блока и UF произвольных сущностей будут заводиться им же. Клонируются только строковые UF — тот же список, что считает переводимым сопоставитель. -
Writer'ы и общая механика порции (A4). Общий контракт
Service\ValueWriterInterface::writeValue()(null — записано, строка — ошибка); у разделов появилсяIblockSectionValueWriter: все поля раздела пишутся одним обновлением вместо одного на поле, результат проверяется, переиндексацию поиска решает настройка очереди — в обоих путях пайплайна, где раньше стоял прямойCIBlockSection::Update. Пределы порции (тексты, знаки, sprint-вызовы) и выбор свойств вынесены вSliceBudgetиPropSelection, общие для сборщиков элементов и разделов; инфоблочное (варианты списков,buildSelect) осталось в сборщиках. Мёртвые после A1 ветки «цель — стандартное поле» убраны из пайплайна и writer'а элементов. -
Scope описываются одним местом (A5).
Service\ScopeRegistryдержит вид, режимы, обязательные поля, правило «языки различаются», дополнительные проверки и обработчик каждого scope единой точки входа. Запрос (TranslateRequestDTO), порция (ParsedBatchDTO) и эндпоинт спрашивают реестр, а не держат свои списки; новый scope — одна запись в реестре плюс метод пайплайна, сторонний код может добавить свой черезregister(). Размер шага по умолчанию у очереди тоже в описании scope.TranslationTaskDTOзнает типыHLROWиSEOдля будущих сборщиков. -
Зеркало языка на новом фундаменте (A8). Карта разделов читается в общем формате без приведения, отказа
reverse_schemeбольше нет, метка ручной правки ставится черезEntityRef. Пары раздела с UF-источником (дополнительные UF) зеркало до Б3.3 не строит: их поля попадают в «остальные». Клиентский скрипт менялся только удалением мёртвой ветки; прогон на HTML-макете формы — в чек-листе стенда 7.0.0. -
T10 закрыт. Имя инфоблока в строке прогресса автоперевода экранируется: раньше оно уходило в
innerHTMLкак есть, и инфоблок с именем вида<img src=x onerror=…>выполнял скрипт в сессии администратора. -
Хранилище переводов SEO-шаблонов (Б1.1). HL-блок
MrlexndrTranslateSeo(b_mrlexndr_translate_seo): строка — «уровень + сущность + код шаблона» (UF_CODEвидаE.12.ELEMENT_META_TITLE, уровниE/S/Bкак в ядре), по колонкеUF_LANG_<XX>на каждый целевой язык; базовый шаблон остаётся в ядре. Создаётся при установке и «Обновить» тем же кодом, что словарь, колонки — общим синхронизатором A3 и при сохранении списка языков; индексы по сущности и по инфоблоку.Seo\TemplateStoreчитает собственные переводы сущности (managed-кеш на сущность; сбой чтения не кешируется и уходит в журнал), пишет, заводит и удаляет строки; каждая запись сбрасывает кеш сущности и тег кеша инфоблока, в порции — один раз в конце. Блок переживает удаление модуля, как словарь. -
Чтение SEO с наследованием (Б1.2).
Seo\TemplateRepository::resolve()по правилу «уровень с собственным базовым шаблоном побеждает»: уровень берётся изfindTemplates()ядра, перевод — только его, без перевода — его базовый шаблон, а не перевод родителя. Кешируетсяown(), неresolve(). -
SEO в очереди (Б1.7). Три типа
seo_element/seo_section/seo_iblockзаполняются: карта пар — по одной на код собственного шаблона (Seo\PairMap), унаследованное не переводится на уровне наследника. Строка состояния появляется только когда в событии сохранения есть непустой собственный шаблон (IPROPERTY_TEMPLATESиз$arFields); сохранение без шаблонов её не создаёт (TranslationQueue::touch()только помечает существующую); строку без единого источника пересчёт удаляет. Прямая записьBaseTemplate::set()мимо формы ловится ORM-событиями\Bitrix\Iblock\InheritedProperty. Зарегистрированы события разделовOnAfterIBlockSectionAdd/Update/Delete(в 7.0.0 — только SEO),OnAfterIBlockUpdate,OnIBlockDelete; удаление элемента, раздела и инфоблока чистит строки HL и состояние. Полный пересчёт («Пересчитать всё») идёт тремя фазами одним циклом шагов — элементы, сущности с собственными шаблонами изb_iblock_iproperty, строкиseo_*, — фаза закодирована в курсоре (Seo\ScanCursor). -
Перевод SEO-шаблонов (Б1.6). Порция
Seo\SliceBuilderберёт только собственные шаблоны сущности (по строкам очереди — с уточнением по ожидающим кодам; по курсору инфоблока — строкиb_iblock_ipropertyвсех трёх уровней),Seo\TemplateWriterпишет в HL-блок, тег кеша инфоблока сбрасывается один раз на порцию. Макросы{=this.name}обязаны вернуться на месте: перевод с потерянным, лишним или изменённым макросом не пишется (Seo\Macros), это пропуск с записьюTRANSLATE_SEO_MACROSв журнал, а не ошибка шага; код остаётся ждущим и получает отметку «пробовали, не получилось» до правки исходника — даже если в HL лежит старый перевод. Форматplain, без типографики. Шаг очереди для строкseo_*— тот же курсор и те же отметки, что у элементов; фоновый агент берёт их сам. Scopeseoединой точки входа; инфоблок без собственных шаблонов — успешный шаг с пометкой в журнале, прогон по остальным продолжается.ParsedBatchDTOпроверяет scope вида «инфоблок» по реестру. -
Где запускается (Б1.10). Вкладка «Автоперевод»: объект «SEO-шаблоны инфоблока» с тем же выбором инфоблоков и языков. Карточка элемента и раздела: галочка «включая SEO-шаблоны» (по умолчанию включена) — один клик переводит поля и собственные шаблоны сущности. Вкладка «Очередь перевода»: ссылки в отчётах об отказах и машинном переводе ведут на форму нужного уровня. Вкладка «Инфоблоки»: ссылка на штатный список HL-блока «SEO-шаблоны по языкам». Новых опций модуля нет.
-
SEO на публичной части (Б1.4, Б1.5, Б1.8).
IblockHelper::substituteSeo($arResult)иsubstituteSectionSeo()переписывают оба ключа —IPROPERTY_VALUESиMETA_TAGS(TITLE,BROWSER_TITLE,KEYWORDS,DESCRIPTION,ELEMENT_CHAIN), — а также alt/title картинокPREVIEW_PICTURE/DETAIL_PICTURE: компонент собираетMETA_TAGSдо шаблона и ставит<title>после него уже изMETA_TAGS, оба в ключах кеша, поэтому подмена доезжает до кешированного хита. Значения считаются штатным движком ядра по шаблону на языке (уровень с собственным базовым шаблоном побеждает) с сущностями-наследниками:{=this.name},{=this.property.CODE},{=parent.name}подставляют переведённые значения по тем же картам пар, что уDecorator(включая фолбэкNAME_<ЯЗЫК>без пары в карте);{=iblock.*}— базовый до 7.3.0,{=sections.*}— базовый. Наследники перенаправляют чтение поля (getField()), а не строят сущность заново: не зависят от структуры, которую ждёт ядро, и не форкают его. На базовом языке хелпер ничего не делает. Отдельного перевода картинок не нужно. Вызовы вписывает разработчик сайта (чек-лист —docs/SEO_FACTS.md, раздел 9). -
SEO-вкладка правится на языке (Б1.3). Блокировка 6.15.1 снята: в режиме зеркала языка поля собственных шаблонов сущности показывают перевод и уезжают под именем
mrtrlm_seo[<КОД>], базовый шаблон уходит скрытым носителем под исходным именем — ядро пишет его нетронутым, а перевод забирает обработчик зеркала и кладёт в HL-блок, пометив как правку человека. Унаследованный шаблон остаётся заблокированным: переводится он там, где задан. Уровень инфоблока правится там же, где и в ядре — на SEO-вкладке настроек инфоблока; зеркало получило третью сущность. Предпросмотр значений подписан как базовый: переведённый предпросмотр в 7.0.0 не делается. Переводы шаблонов, чьей сущности больше нет (событие удаления не сработало), показывает и удаляет вкладка «Мусорщик». Успешная запись переводов из формы оставляет след в журнале (TRANSLATE_LANG_MIRROR_SEO_SAVED): правку через карточку делает не только администратор.
Чего делать не стали и почему
- Режим «все UF» для разделов — утащил бы служебные UF, привязки и поля чужих модулей; только явный список на инфоблок.
- Списки UF в схеме полей — у них свои варианты (
CUserFieldEnum), и переносить их по XML_ID, как варианты свойств, нечем; пара не строится, а не переводится как текст. - Отдельную таблицу состояния для разделов — решение Р3 плана: правило
decidePending()одно, копия на вторую таблицу разошлась бы, а сводки считались бы объединением. - Переименование
IBLOCK_IDвOWNER_ID—RENAME COLUMNна живой таблице ради косметики; смысл «владелец» записан в руководстве и вEntityRef::fields(). - Совместимые обёртки старых сигнатур (
markDirty(int, int)и прочие) — методы очереди не публичный API шаблонов сайта; все вызовы внутри модуля переведены, рассогласование ловитstatic_call_arity.php. - Хранить вычисленные SEO-значения по языкам — они производные от шаблона; конкурент завёл под них три теневые таблицы, и это лишнее. Считает их хелпер публичной части (Б1.4, следующая группа).
- Переводить унаследованные SEO-шаблоны на уровне наследника — шаблон инфоблока уехал бы в перевод по разу на каждый элемент и язык: тысячи одинаковых платных переводов и HL-блок размером с каталог.
- Заводить строку
seo_*на каждое сохранение — у каталога тысячи элементов без собственных шаблонов; строка появляется только от непустого собственного шаблона в событии или от ORM-события ядра. - Строку состояния SEO с нулём — сущность без собственных шаблонов строки не имеет вовсе: пересчёт удаляет, а не хранит.
- Колонку базового языка в SEO-блоке — базовый шаблон живёт в ядре и читается оттуда как источник; копия разошлась бы с ним.
- Обработчик
main:OnEpilog, правящий заголовок и мета-теги без хелпера — в эпилоге нечем узнать, какая сущность выведена на странице; правка чужого вывода наугад ломала бы страницы. Путь один — хелпер вresult_modifier.php. - Строить сущности шаблона через штатную установку полей — её структуру со стенда не сняли (второй заход разведки не запускался); перенаправление чтения поля не зависит от неё и не форкает ядро.
- Типографику для SEO-шаблонов — неразрывный пробел или кавычка внутри
макроса
{=this.name}сломали бы его.
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() проверяет её после выхода «только что
перевели» и до претензий сверки: решение человека сильнее претензии.
Метка истекает сама, как только оригинал меняется. На вкладке очереди —
счётчик правленных руками и кнопка сброса меток.
Предупреждение, которое показывается всегда
Если язык админки — целевой, а зеркало не применилось (выключено, инфоблок не в списке, нет права, форма не опознана, внешний скрипт не загрузился), вверху формы жёлтая плашка: «интерфейс на одном языке, поля показывают другой». Она подключается инлайновым скриптом до проверки опции и прав и появится на всех проектах после обновления. Это сознательно: без неё человек на английской админке молча правит русский текст — так было и до этой версии.
Права
Зеркало и метка открыты на право чтения модуля, не записи. Зеркало ничего не тратит и записывает не оно, а штатная форма, права на которую проверяет сам Битрикс. Гейт на запись создал бы ровно ту ситуацию, ради которой всё затевалось: контент-менеджер без права на модуль правит не тот язык. Право записи остаётся у вкладки «Перевод», где тратятся деньги.
Чего делать не стали и почему
- Подписка на событие инициализации визуального редактора. Была в
плане, убрана: человек может править перевод в режиме «Текст» и только
потом включить редактор, повторная запись значения стёрла бы его правки.
Живой редактор опознаётся по
iframeв строке поля, содержимое ставится черезSetContent. - Подавление предложения восстановить черновик. Черновик — несохранённая работа человека; вместо тихого подавления зеркало не применяется, а плашка просит восстановить или отклонить его.
- Множественные свойства, торговые предложения, список элементов на
альтернативном языке, SEO-вкладка, обратная схема пар — записано в
docs/BACKLOG.md.
После двух независимых проверок кода
- Мутация клиента ведёт журнал отката. Каждый шаг записывает, как его
отменить; исключение на любом шаге откатывает всё сделанное и
заканчивается отказом с плашкой. Отказ
SetContentживого редактора — тоже отказ, а не тихий пропуск: иначе редактор при отправке вернул бы в textarea базовый текст под именем цели. - Пустой пересчёт не стирает метки ручной правки. Элемент переехал в другой инфоблок или карта временно пуста — метки остаются; снимаются только истёкшие и те, что переводили заново. Перевод кнопкой «Перевести» с перезаписью снимает метки элемента явно.
- Обе новые точки входа гасят исключения и пишут их в журнал, как остальные обработчики модуля: карточка и сохранение элемента важнее зеркала.
- Автосохранение: объект ищется по значению поля
autosave_id; егоInitна стенде ленивый и при загрузке не вызывается, поэтому в режиме зеркала он глушится вместе с записью и таймерами. Нет объекта при наличии поля — отказ. - Тип текста без радиокнопок (смена типа запрещена настройками инфоблока) уезжает отдельным скрытым полем.
- Описание значения свойства (
[DESCRIPTION]) у цели сохраняет имя. - Предупреждение показывается и на форме создания элемента.
Попутно
T26закрыт: сохранение режима свойств без права записи показывает сообщение, а не молчит.- В адрес скрипта и стилей добавлена версия модуля — у зеркала и у трёх прежних ассетов (режим правки, inline-редактор словаря, список элементов): старый скрипт из кеша браузера на новой разметке — это подмена имён по устаревшей таблице.
- Предупреждение вкладки «Перевод» дополнено строкой про режим зеркала: после перевода страница перезагружается, несохранённые правки формы будут потеряны.
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>
читалось: <p>9</p>
Дальше происходило следующее. На перевод уходил текст, который изображает разметку; модель разворачивала сущности обратно в теги — единственное разумное поведение; сверка тегов на бэкенде обнаруживала «в исходнике 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 — фатальная ошибка прямо в шаге очереди.
Срабатывало на паре «инфоблок + язык», у которой нет карты свойств: агент ловил исключение и после пяти таких ошибок останавливал фоновое задание целиком. Метод теперь возвращает массив, ссылочных параметров нет, и рассогласовать нечего.
В том же месте нашлись ещё два дефекта, оба про тот же признак:
- предел выборки стоял числом
500рядом с константой в 500. Правка одного из двух мест ничем бы себя не проявила, кроме неверного ответа «готово»; - признак считался по успешно очищенным строкам, а не по выбранным. Часть обновлений не прошла — очищенных меньше порции — и выходило «больше нет», хотя строки на месте.
Обновление через Маркетплейс не запускало миграции
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 |
обрубок сущности: « 2200» или «;2200» |
Порядок тегов не сверяется: перенести закрывающий тег выделения на конец предложения модель вправе, в другом языке оно ложится иначе.
Для иероглифики порог длины отдельный — 0,2 вместо 0,5. Китайский записывает то же содержание втрое короче, и с общим порогом первый же прогон дал 63 выдуманные претензии из 128.
Проверяются все переводимые пары: стандартные поля (NAME, PREVIEW_TEXT,
DETAIL_TEXT), строковые свойства, множественные и sprint-редактор. Списочные
пропускаются — они копируются по XML_ID, модель их не касается.
Вес находок
Разбирать десятки претензий вперемешку невозможно, поэтому у каждой есть вес:
- критично —
a,tableи семейство,img,nav,div, а также любойTOO_SHORT: потерянный адрес человек уже не восстановит, незакрытую таблицу браузер не закроет; - заметно —
ul,ol,li, заголовки: список, ставший переносами строк, читается хуже, но страница цела; - косметика —
p,br,span: незакрытый абзац браузер закрывает сам, в вёрстке этого не видно.
Где смотреть и как чинить
Отчёт — cli/integrity_scan.php, работает и из консоли, и из браузера с
правом записи на модуль. Результат хранится в колонке BROKEN_PROPS таблицы
состояния; очередь её не читает.
Кнопка «Пересчитать все элементы» теперь заодно сверяет переводы, и критичные находки уходят в очередь наравне с изменившимся исходником. Интерфейс при этом прежний.
Три ограничения, без которых так делать было нельзя:
- сверка встроена только в полный пересчёт. Обычный проход разбирает отметки правок и ходит из фонового агента — там решение «перевести заново» принималось бы без человека и тратило деньги само по себе;
- в повтор уходит только критичное. Платить за исправление невидимого незачем;
- пересчёт сразу после перевода порчу не подхватывает, иначе выходит петля с оплатой на каждом круге.
Заодно
- Порча ловится и у элементов, переведённых мимо очереди. Проверка сначала стояла в конце правила «что ждёт перевода», а выше есть выход «сохранённого хеша нет — принимаем за точку отсчёта». Хеша нет ровно у тех элементов, что переводили массовым переводом из админки, то есть у самых испорченных.
- Сверка идёт по всем инфоблокам с языковыми свойствами, а не по списку отслеживаемых. Тот список отвечает на вопрос «что продолжать переводить», а здесь вопрос другой. Пока охват был чужой, отсутствие инфоблока в отчёте читалось как «там всё хорошо».
- Тестов стало пять сценариев и семьдесят одна проверка. Среди них девять
на
decidePending— самое дорогое правило модуля, чей дефект когда-то заставлял платить за один перевод многократно.
Что нужно сделать при обновлении
Колонка 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>в доме 10 кг</pre> |
без изменений |
год назад |
год назад |
без изменений |
ООО НПО МЧС РФ работает |
ООО НПО МЧС РФ работает |
без изменений |
В 2024 компания |
В 2024 компания |
В 2024 компания |
Он сказал "привет" ей |
Он сказал "привет" ей |
Он сказал «привет» ей |
Москва - столица |
Москва - столица |
Москва — столица |
| 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 элемента снова уехала бы сущность — без ошибки и без записи в
журнале. Теперь возраст фасада проверяется, старый корректор считается
недоступным, а вкладка «Автоперевод» предлагает его обновить: молча выдавать
неверный результат хуже, чем не выдавать никакого.
Заодно.
- Пакетный перевод словаря шёл мимо всей постобработки — элементы и разделы получали типографику, словарь не получал ничего. Теперь на этом пути чинятся незакрытые теги и работает типографика.
Lang::translateTexts()ставила сущность в простые строки. Типограф звался двумя аргументами, третий оставался со значением по умолчанию. В пайплайне это чинили в 6.9, сюда правка не доехала.- Незакрытые теги в переводах больше не остаются.
HtmlTypoFixerстоял на единственном пути — ручном сохранении строки словаря, — и переводы свойств инфоблока чинить было некому. Теперь он на всех путях, где значение может содержать разметку, и не зависит от галочки «Типографика»:<span>текст<span>разъезжает вёрстку всей страницы, это сломанная разметка, а не выбор стиля. - Появился каталог
tests/— его отсутствие фиксировалcheck-modules.php. Три сценария, 42 проверки, ядро Битрикса не нужно:php tests/run_all.php. Каждая строка таблицы выше проверяется на каждом прогоне.
Чего эта версия НЕ делает: санитайзера на пакетном переводе словаря.
Находка 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.
Три самых тяжёлых.
- Провалившийся шаг очереди помечал непереведённое переведённым. Шаг всегда сообщал «перевод состоялся», а признак «после перевода» считает свойство неждущим, как только в цели есть любое значение, и переписывает хеш исходника. Для свойства со старым переводом это приговор: кончились деньги или отказал провайдер — и в базе стоит «исходник такой и переведён». Правка контент-менеджера терялась навсегда и молча. Тот же корень был у кнопки «Перевести» без галочки «перезаписать».
- Отметка «пробовали, не получилось» снималась переводом соседнего свойства. Условие опровергало само себя: отмеченное свойство исключается из очереди именно из-за отметки, поэтому в списке ждущих его нет никогда. Отказной текст снова уезжал в платный API при каждой правке соседа.
- Отметки машинного перевода не сохранялись ни разу. Ключи собирались по целевому коду свойства, а состояние очереди говорит на языке исходных: список «переведено машинным движком» не показывался с самого своего появления.
Деньги. Остановка по балансу на sprint-пути не доходила до списания — Яндексу заплачено, с клиента не взято. Запись о списании по ключу жила вечно при суточном кеше ответа: тот же ключ через сутки означал новую работу, которая уходила бесплатно. Частичное списание занимало ключ клиента, и повтор после пополнения счёта тоже был даром. Повторный частичный возврат банка не списывался. Баланс хранился с двумя знаками при учёте в шести — списание меньше половины копейки пропадало. Клиент, заведённый в админке, стартовал со ста рублей.
Данные. Перенесённый между инфоблоками элемент навсегда выпадал из очереди. Первая правка элемента без точки отсчёта растворялась — «работает через раз». Множественное свойство теряло значение и сдвигало остальные. Импорт CSV обходил проверку разметки и портил фразы апострофом. Язык без поля словаря ронял весь сайт фатальной ошибкой.
Интерфейс. Статистика по инфоблокам показывала нули всегда. Ошибка одного шага бросала всю пару и рапортовала «готово» при непустой очереди. Остановленное задание показывалось как «агент умер». Кнопки очереди при недостатке прав молча ничего не делали.
Обновление. DoUpdate не обновлял файлы вовсе: JS, CSS, заглушка и тема оставались от прошлой версии — новый серверный код работал со старым клиентским. Это же означает, что все правки прошлых версий в JS до боевых проектов не доезжали.
Три миграции на бэкенде: точность баланса, признак машинного перевода с отметкой использования, право на админку и домен «на подтверждение».
6.8.0 — 2026-08-29
Сплошной разбор обоих репозиториев вторым читателем и правки по всем находкам. Полный список — AUDIT.md, отчёт о работах — AUDIT_RESULT.md.
Деньги.
- Возврат платежа теперь снимает деньги с баланса. Статусы
REVERSEDиREFUNDEDлежали в списке «платёж не состоялся» и уходили в обработчик, который первым делом выходит на подтверждённом платеже, — то есть ровно в том единственном случае, когда возврат и бывает. - Повтор запроса с тем же ключом идемпотентности больше не упирается в проверку баланса. Запрос мог списать остаток в ноль, а ответ не доехать; повтор не доходил до готового оплаченного ответа, и переводы терялись.
- Содержимое sprint-блоков наконец кешируется: признак «кеш включён» считался по формату запроса, а для конструктора страниц он равен
sprint.
Безопасность.
- Разметка фразы проверяется на сервере, а не только в браузере. Сохранение делало ровно одно — чинило незакрытые теги; право записи на модуль превращалось в возможность положить
<script>на публичный сайт. - Политика прав описана один раз. Копий было две, и они разошлись: ужесточение внесли в
Access, а на административных действиях стояла старая проверка — «есть право на админку, значит можно тратить деньги». - Право на админку бэкенда задано явно.
Данные.
- Импорт словаря не режет фразы длиннее десяти килобайт. Остаток уезжал отдельной записью с пустым кодом и молча попадал в «пропущено».
- Сбой записи файла словаря больше не оставляет сайт вообще без переводов: словарь собирается в память и отдаётся, файл обновится при следующем удачном запросе.
Скорость и устойчивость.
- Синхронизация структуры перестала спрашивать базу на каждую пару «свойство × язык» и переписывать каждый вариант списка. Она же теперь укладывается в отведённое время и честно говорит, что работа не закончена, — повторный запуск продолжает с того же места.
- Ожидание чужого запроса на стороне API сокращено со ста двадцати секунд до двадцати, ответ «уже выполняется» стал повторяемым (429 с
Retry-Afterвместо 409). - У запроса на перевод появились границы размера, у вебхука платежей — счётчик обращений, у логов — ротация, у кеша переводов — чистка.
Интерфейс.
- Зависшее фоновое задание видно сразу: пятнадцать минут без шагов дают красное предупреждение с кнопкой сброса.
- Домен, с которого клиент стучится вопреки записанному, показывается в админке бэкенда и принимается кнопкой.
- Появился переключатель типографики (по умолчанию выключен, см. ниже).
Типографика. UniversalSpacer вызывался только из Lang::translateTexts(), а её звал единственный файл — cli/smoke_translate.php. Весь боевой перевод шёл мимо, хотя руководство обещало обратное. Теперь типограф на боевом пути и понимает формат поля: в HTML связка идёт сущностью , в обычной строке — самим символом неразрывного пробела. Выключен по умолчанию: включённый, он поменял бы вывод на работающих проектах. По дороге нашлась давняя ошибка — класс символов валют разбирался как имя переменной, и ни рубль, ни доллар к числу не привязывались никогда.
Убрано. Очередь синхронизации инфоблоков (её никто не запускал) вместе с неймспейсом Agents, недостижимая ветка движка v2, колонка hits в кеше переводов, неиспользуемые метод и поле.
Обновление снимает старый агент LangIblockQueue и удаляет четыре осиротевшие настройки. На бэкенде три миграции: признак машинного перевода и отметка использования в кеше, право на админку, домен клиента «на подтверждение».
6.7.0 — 2026-08-24
От чего отказался GPT, переводит машинный движок. Модель регулярно отвечает «Я не могу обсуждать эту тему» вместо перевода. Строгая формулировка снимает часть отказов, но не все, и до сих пор оставшиеся заканчивались пустой строкой: свойство висело непереведённым, а причину приходилось искать в журнале событий.
Теперь после двух попыток моделью текст уходит в машинный переводчик Яндекса. Он не собеседник, и отказываться ему нечем.
- Порядок попыток: обычная формулировка, строгая, машинный переводчик, и только потом пустая строка.
- Машинный перевод считается и оплачивается наравне с GPT, по своему прайсу: символы исходника вместо токенов, наценка та же. Отказ модели при этом приходит с нулём токенов, то есть неудавшаяся платная попытка ничего не стоит. В
metaответа появилисьmachine_charsиmachine_price_per_1k— по ним счёт сходится без догадок. - Такие поля помечаются и показываются на вкладке «Очередь перевода» отдельным списком со ссылками на элементы: глоссарий и контекст проекта к ним не применялись, и их стоит просмотреть. Отметка снимается сама, когда исходный текст правят, — заново отправленный текст модель может и перевести.
- Выбор движка в интерфейсе по-прежнему не предлагается: в настройках указан GPT, машинный перевод остаётся запасным вариантом внутри него.
Установка добавляет колонку MACHINE_PROPS в b_mrlexndr_translate_state отдельным шагом. Без обновления установки модуль работает как раньше: отметки не хранятся, машинный перевод остаётся только в журнале событий (TRANSLATE_MACHINE_FALLBACK).
6.6.0 — 2026-08-20
Три правки по итогам стороннего разбора кода. Подробности — раздел K2–K5 в BUGS_REGISTRY.md.
- Новый вариант списка переводится сразу. Раньше пометка «схема списков переведена» жила пять минут и переживала добавление варианта: перенос значения падал на каждом элементе, а фоновое задание останавливалось на пятой ошибке. Теперь пометку снимает правка свойства, а если целевого варианта всё же не нашлось — она снимается на месте, и следующий шаг заводит вариант.
- «Мусорщик» честно говорит о границах поиска. Ноль использований означает «не найдено явных вызовов в .php и .phtml», а не «фраза не нужна»: код, собранный из частей, не находится в принципе. Предупреждение с этим текстом теперь висит прямо на вкладке.
- Режим правки не портит ajax и JSON. HTML-обёртка фразы больше не выводится, если ответ заведомо не страница.
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
Непереведённое стало видно, и модуль сам пробует ещё раз.
- Вторая попытка с другой формулировкой. Модель отказывается переводить, приняв текст за приглашение обсудить тему — новости про границы, происшествия, политику. Задача при этом чисто механическая, и если проговорить это прямо, часть отказов снимается. Одна дополнительная попытка на отказ, не больше.
- Список прямо на вкладке очереди. Раньше была только сводка «N свойств у M элементов», а что именно не прошло — приходилось искать в журнале событий. Теперь таблица: элемент со ссылкой на карточку, какие свойства не перевелись, язык.
Почему отказавшие тексты не остаются в самой очереди: иначе счётчик никогда не дойдёт до нуля и станет невозможно отличить «ещё предстоит перевести» от «не переведётся никогда». Они вынесены в отдельный блок над очередью — заметный и с объяснением.
6.5.0 — 2026-08-20
Отказ провайдера переводить конкретный текст.
Что было. Яндекс отказался переводить абзац про очереди фур на границе: «Я не могу обсуждать эту тему». Дальше по журналу — восемь одинаковых отказов подряд. Модуль получал общий код 502, считал это временным сбоем и повторял запрос четыре раза, а очередь отправляла тот же текст при каждом прогоне.
Что стало.
- Отказ не роняет всю пачку. Деление доводит до одного виноватого текста, и он возвращается пустой строкой. Остальные тексты сохраняются — раньше они терялись вместе с запросом, уже переведённые и оплаченные.
- Отказ не повторяют. Сервер отвечает кодом 422 вместо 502; такой ответ модуль не считает временным сбоем.
- Очередь запоминает отказ. Свойство помечается «пробовали с этим исходником, не получилось» и уходит из очереди. Отметка снимается сама, когда исходный текст изменят. При ошибке шага — нет денег, недоступен провайдер — отметки не ставятся: временный сбой не должен вычёркивать свойство навсегда.
- Отказы видно. На вкладке очереди появилась сводка «провайдер отказался перевести N свойств у M элементов» и кнопка «Попробовать снова» — она нужна, если поменяли глоссарий или контекст.
Требуется обновление модуля в админке: в таблицу состояния добавляется колонка отметок. Пока её нет, модуль работает как раньше.
6.4.0 — 2026-08-19
Итог сплошной перепроверки всего, что сделано за сессию. Подробности — раздел «Сплошная перепроверка сделанного» в BUGS_REGISTRY.md.
Объём одного шага теперь ограничен по-настоящему
- Порция массового перевода обрезается по объёму — не больше шестнадцати текстов. Раньше шаг задавался числом элементов, а во сколько текстов он развернётся, было неизвестно: двадцать элементов по три поля упирались в бюджет вызова и обрывались.
- sprint-поля считаются отдельно — не больше двух на порцию. Каждое переводится своим запросом к API, и общий счётчик текстов на них не распространялся: двадцать элементов со sprint-полем давали двадцать последовательных обращений в одном запросе.
- Потолок обращений к провайдеру считается на запрос, а не на пачку. Коррекция текста тоже под ним — раньше шла вообще без ограничения.
Стоп-кран останавливает всё, что тратит деньги
Кнопка «Остановить перевод» теперь действует и на массовый перевод инфоблока, и на перевод словаря. Раньше — только на очередь, хотя именно с массового прогона и начался случай, ради которого стоп-кран делался.
Расширенный режим свойств
Если у инфоблока есть и TITLE, и TITLE_RU, побеждает явный источник с суффиксом. Раньше оба уходили на перевод и писали в один TITLE_EN: платили дважды, а результат зависел от порядка обхода свойств.
6.3.2 — 2026-08-19
Вторая партия по аудиту словаря и фронта, раздел H в BUGS_REGISTRY.md.
- Новая фраза больше не рождается «переведённой». Исходный текст пишется только в базовый язык сайта; поле целевого языка остаётся пустым — это и есть «перевода нет». Раньше фраза, впервые показанная на английской странице, сразу получала русский текст в
UF_LANG_EN, и найти её среди непереведённых было невозможно. Существующие записи не затронуты, правка действует на вновь создаваемые. - Непереведённая фраза снова подсвечивается в режиме правки. «Новизна» определялась совпадением итогового текста с кодом, а после подстановки фолбэка это совпадение уже не срабатывало.
- Массовый перевод из списка останавливается после трёх ошибок подряд. Раньше отказ на первом элементе не мешал отправить запросы по всем остальным.
Lang::typo()не падает в агенте и CLI — константаLANGUAGE_IDбольше не стоит в значении по умолчанию.- Inline-редактор не показывает сырые подстановки после сохранения фразы с
#ПЕРЕМЕННОЙ#. - Поиск использований кода фразы ищет фиксированную строку, а не регулярное выражение.
6.3.1 — 2026-08-19
Разбор словаря LANG_VAR и фронтовых точек входа — того, что не попало в предыдущий проход. Подробности в BUGS_REGISTRY.md, раздел H.
- Словарь языка по умолчанию терялся на остаток запроса. Создание новой фразы на странице неосновного языка объявляло словарь базового языка загруженным, положив в него одну эту фразу. Все остальные коды после этого выводились как
#КОД#. - Рендер публичной страницы больше не пишет в словарь. Фолбэк закреплялся в поле языка прямо при выводе, и фраза после первого показа выглядела переведённой — найти её среди непереведённых было нельзя.
- Чистка языков не удаляет непроверенное. Проверка наличия данных на кривых входных данных отвечала «данных нет», то есть «удаляй», хотя в соседней ветке того же метода не сумевшая проверить логика осознанно отвечает наоборот.
- Перевод из карточки элемента и из списка больше не «обрывается» после записи. Признак «человек ждёт ответа» не отправлял ни один клиент, поэтому сервер работал по фоновому профилю дольше, чем браузер готов ждать. Заодно общий бюджет вызова теперь ограничивает и первую порцию.
- Удалён мёртвый
install/js/iblock_edit_translate.js— он дублировал логику вкладки, но не подключался ниоткуда.
Известная и не исправленная проблема: вкладка «Перевод» в карточке элемента не работает на сайтах со строгим 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 позиций молча превращается в счёт, который видишь постфактум.
Как устроено:
- События добавления и изменения элемента ставят лёгкую отметку «надо проверить» — без обращений к API и без чтения карты свойств, чтобы не замедлять обмен данными.
- Сравнение идёт по каждому свойству отдельно: правка
NAMEне делает устаревшимиPREVIEW_TEXTиDETAIL_TEXT. - Хешируются только исходные значения, поэтому ручная правка перевода в очередь не попадает.
- «Перевести накопленное» отправляет в API только разошедшиеся поля, а элементы с одинаковым набором изменений группируются в один запрос.
- Элементы, переведённые до включения очереди, принимаются за точку отсчёта и не попадают в оплату при первом пересчёте.
Настройки: список отслеживаемых инфоблоков и исходный язык. Пока инфоблоки не отмечены, события не делают ничего.
Прочее
- В требованиях указан честный минимум PHP — 8.1+. Раньше в README стояло 7.4, хотя код использует
readonly,array_is_list(),matchи nullsafe-оператор.
6.0.0 — 2026-08-17
Ревизия модуля по итогам аудита. Полный разбор с ID пунктов, файлами и строками — в BUGS_REGISTRY.md; ниже только то, что меняет поведение.
Требует внимания при обновлении
- Выложить файлы и нажать «Обновить» в Marketplace → Установленные решения. Именно
DoUpdate()дочиняет структуру: недостающие поля словаря и уникальный индекс поUF_CODE. Простой выкладки файлов недостаточно. - Открыть «Инфоблоки → Языки», проверить отметки и сохранить список. До сохранения модуль работает по автоопределению и показывает предупреждение. Языки без сайта, попавшие в список случайно, снимите.
- Проверить «Служебные → Языки и свойства». Если прежние версии наплодили комплекты свойств для лишних языков, здесь их видно: сначала выгрузка в CSV, потом удаление пустых.
- Посмотреть на страницы модуля. Красный баннер о структуре БД означает, что не удалось создать уникальный индекс — в журнале будут конкретные дублирующиеся коды.
Языки
- Появился единый источник истины
LanguageRegistry. Целевые языки задаются явно на вкладке «Инфоблоки → Языки»; раньше список брался из всей таблицыb_language, а на фронте подстановка шла по языку сайта — из-за расхождения появлялись мёртвые комплекты свойств, которые при этом переводились за деньги. - Добавление языка в Битриксе больше ничего не создаёт: модуль запоминает язык и показывает напоминание. Раньше событие выбирало до 500 инфоблоков и запускало фоновую синхронизацию по всем языкам сразу.
- Синхронизация принимает список языков и работает только по целевым.
- Коды языков ограничены двумя символами — как в схеме Битрикса. Составные локали вида
pt-brотвергаются явно, а не создают нечитаемое свойство. - Новая вкладка «Служебные → Языки и свойства»: отчёт по следам лишних языков, выгрузка в CSV и удаление только пустых объектов.
Синхронизация свойств
- Клонируются только переводимые типы: строки (включая HTML и sprint.editor) и списки. Файлы, привязки, числа и служебные типы больше не плодят копии на каждый язык.
- Существующее свойство-перевод не перетирается настройками источника: приводится только структура. Название, сортировка, обязательность и прочее сохраняются.
- Варианты списков больше не удаляются: вместе с вариантом Битрикс стирает значения у элементов.
Перевод
- Ответ API сверяется по количеству: расхождение отменяет запись целиком. Раньше соответствие «текст → перевод» держалось только на порядке массива, и сдвиг молча раскладывал переводы по чужим полям.
- Появились ретраи для случаев, когда запрос заведомо не дошёл (
0, 408, 429, 502, 503), с разными профилями для интерактива и фона.500и504не повторяются: сервер мог уже выполнить работу и списать деньги. - Разделы переводятся с правильного языка: источник зависит от выбранного исходного, а не всегда от стандартных полей.
- Исходный язык во всех трёх интерфейсах берётся из одного места. Групповое действие в списке брало язык интерфейса админки, а массовая вкладка имела захардкоженный русский.
- Групповое действие в списке элементов наконец работает: список языков читался не из того поля ответа и всегда был пуст.
- Пустые ответы API считаются отдельно, попадают в журнал и показываются в интерфейсе, а не сливаются с «пропущено».
- Перевод «сам в себя» и несуществующий язык отклоняются.
- Стандартные поля элемента пишутся одним обновлением вместо трёх.
Массовый прогон
- Размер пачки вынесен в настройку (по умолчанию 20 вместо жёстких 3), при обрыве пачка уменьшается вдвое и шаг повторяется.
- Клиентские таймауты больше серверного лимита — браузер больше не рвёт живой запрос.
- После обрыва появляется кнопка «Продолжить с места остановки».
Фронт и режим правки
- В режиме правки текст выводится как есть: в словаре допустим HTML, и редактор видит то же, что посетитель. Раньше фразы с тегами показывались сырым исходником.
Decoratorбольше не выкидывает свойства видаVOLUME_ML,POWER_HP,TYPE_ID: языковой суффикс определяется по реальным кодам языков, а не регуляркой «две заглавные буквы».processD7Resultне декодирует HTML-сущности: сохранённый как текст<script>больше не превращается в живой тег.- Починка незакрытых тегов сузилась до инлайновых: раньше она ломала валидную вложенность
div. - Документированный D7-паттерн исправлен: селекты с языковыми свойствами работают только с сущностью инфоблоков 2.0, для чего добавлен
IblockHelper::getEntityDataClass(). НаElementTableони падали с «Unknown field definition».
Админка
- Вкладка «Где используется» не сканирует дерево проекта при каждом открытии карточки фразы — только по кнопке.
- После перевода из карточки страница перезагружается: иначе «Сохранить» затирало свежий перевод.
- Экранированы данные в логе синхронизации — там был XSS через названия вариантов списка.
- «Выбрать все по фильтру» в списке элементов честно отклоняется вместо тихого перевода одной страницы.
- Импорт CSV проверяет размер, тип и число строк, сам определяет кодировку и не делает запрос на каждую строку.
- Мусорщик удаляет только то, что действительно не используется, умеет выгружать список в CSV и пишет удаления в журнал.
- Установка стала идемпотентной, а неудача с уникальным индексом больше не проходит молча.
Прочее
- В репозитории появился
.gitattributes(LF), реестр дефектов и этот файл. - Документация приведена в соответствие с кодом.
Бэкенд (отдельный репозиторий)
Правки живут в отдельном репозитории бэкенда и выкатываются отдельно от модуля.
- Допуск клиентов. Приветственный баланс больше не выдаётся на любой Bearer-токен. Новый клиент заводится с нулевым балансом и не обслуживается, пока его не одобрят в админке (
/admin→ «Клиенты» → «Одобрен»). Стартовые деньги выдаются один раз на домен. Существующие клиенты помечены одобренными миграцией. - Троттлинг на маршрутах API — раньше его не было вовсе.
GET /v1/balanceбольше не создаёт клиента.- Ссылка возврата после оплаты собирается корректно и для URL без строки запроса.
- Идемпотентность списаний по заголовку
X-Idempotency-Key, который модуль шлёт с версии 6.0.0.
Порядок выкладки бэкенда: git pull, php artisan migrate --force, сброс кешей конфига и маршрутов.
5.3.4 — 2026-06-03
Последняя версия до ревизии. История до неё не велась.