Переводчики (PRO)
Машинный перевод контента. Переводчик — это строка в PageBlocks → Мультиязычность → Переводчики; компонент дёргает API провайдера и сохраняет результат как перевод оригинала.
Речь про контент, а не про строки интерфейса — те живут в лексиконах, ими занимается Локализация.
Строка — это профиль, а не «переводчик сайта»
Переводчиков может быть сколько угодно, и выбор делает язык, а не сайт: у каждого языка есть поле Переводчик, в котором выбирается один из профилей. Отсюда обычная рабочая схема — DeepL на европейские языки, языковая модель на остальные.
Язык без выбранного переводчика не переводится вовсе: в интерфейсе это «Для этого языка не настроен переводчик».
Драйверы
| Драйвер | Провайдер | Модель | Промпт |
|---|---|---|---|
chatgpt | OpenAI | gpt-4o по умолчанию | да |
claude | Anthropic | claude-opus-4-8 по умолчанию | да |
gemini | обязательна | да | |
grok | xAI | обязательна | да |
deepl | DeepL | не используется | не используется |
Четыре из них — языковые модели, один специализированный переводчик. Интерфейс у всех общий, поэтому смена драйвера это правка одной строки, а не переделка настроек.
SDK провайдеров компонент не тащит: каждый драйвер — обычный cURL-клиент на пять методов. Поэтому и «свой шлюз» работает без оговорок — достаточно, чтобы он отвечал в формате провайдера.
Общие поля
| Поле | Что это |
|---|---|
| Название | Человеческое имя профиля — его видно в списке у языка |
| Провайдер | Один из пяти драйверов |
| Ключ API | Ключ провайдера. Только на запись: обратно в браузер не отдаётся |
| Модель | Идентификатор модели провайдера |
| Базовый URL | Переопределить адрес — прокси, свой шлюз или выбор тарифа у DeepL |
| Промпт | Шаблон инструкции, только для LLM-драйверов |
| Опции | JSON с дополнительными настройками — см. ниже |
| Активен | Неопубликованный переводчик не используется |
Ключ при правке остаётся пустым — так и надо
Он хранится только на запись и в браузер не возвращается, поэтому в окне правки поле пустое, а не заполненное звёздочками. Оставили пустым — сохранится прежний ключ. Вписали новый — заменится. Стереть ключ через это поле нельзя, и это сознательно: пустая форма, отправленная случайно, не должна обнулять доступ.
В списке вместо ключа колонка Ключ API с галочкой: видно, что ключ есть, и не видно, какой.
Настройка по провайдерам
ChatGPT (OpenAI)
- Ключ — platform.openai.com/api-keys. Нужен пополненный баланс в Billing: бесплатного уровня у API нет, и свежий ключ без оплаты отвечает отказом по квоте.
- Провайдер:
ChatGPT (OpenAI). - Ключ API: строка вида
sk-…. - Модель: можно не заполнять — возьмётся
gpt-4o. Любой другой идентификатор из консоли подставляется как есть. - Базовый URL: пусто. Заполняется только под прокси или совместимый шлюз — к адресу дописывается
/v1/chat/completions, то есть указывать нужно корень, а не эндпоинт.
Запрос уходит с temperature: 0, промпт едет системным сообщением.
Claude (Anthropic)
- Ключ — console.anthropic.com, раздел API Keys. Баланс так же нужен.
- Провайдер:
Claude (Anthropic). - Ключ API: строка вида
sk-ant-…. - Модель: можно не заполнять — возьмётся
claude-opus-4-8. - Базовый URL: пусто; к нему дописывается
/v1/messages.
Две особенности, обе уже стоили времени:
- Параметров выборки нет.
temperature,top_p,top_kна Claude Opus 4.7 и новее отклоняются с 400, поэтому драйвер их не отправляет вовсе. - Размышление выключено явно. На новых моделях оно включено по умолчанию, и ответ приходит с блоками
thinkingвпереди текста: на одном описании это 5,6 с и 226 выходных токенов против 2,2 с и 114 без него. Переводу строки бюджет на размышление не нужен. Модель, которая отказывается его выключать (отвечает 400), повторяется один раз без параметра — неверная догадка про одну модель не должна уронить весь прогон.
Отказ классификатора приходит с HTTP 200, пустым содержимым и stop_reason: refusal. Драйвер пишет в лог «Провайдер отказался переводить этот текст», а не молча возвращает пустоту.
Gemini (Google)
- Ключ — aistudio.google.com/apikey.
- Провайдер:
Gemini (Google). - Модель — обязательно. Значения по умолчанию здесь нет: идентификатор модели у Google едет в адресе запроса, а не в теле, и подставлять туда догадку нельзя. Берите из AI Studio — например
gemini-2.5-flashдля массового перевода илиgemini-2.5-proтам, где качество важнее цены. - Базовый URL: пусто (
https://generativelanguage.googleapis.com); к нему дописывается/v1beta/models/<модель>:generateContent.
Пустая модель здесь даёт внятную ошибку
Драйвер проверяет её сам и пишет «Для этого переводчика не задана модель», не отправляя запрос. У Grok такой проверки нет — там пустая модель уходит провайдеру и возвращается его собственной ошибкой HTTP.
Grok (xAI)
- Ключ — console.x.ai, раздел API Keys.
- Провайдер:
Grok (xAI). - Модель — обязательно, значения по умолчанию нет. Идентификатор берите из списка моделей в консоли xAI.
- Базовый URL: пусто (
https://api.x.ai); к нему дописывается/v1/chat/completions.
xAI отдаёт API, совместимый с OpenAI, поэтому драйвер целиком наследует запрос, заголовок авторизации и разбор ответа от chatgpt — различается только адрес.
DeepL
- Ключ — личный кабинет DeepL, раздел API keys. Бесплатный тариф существует: 500 тысяч символов в месяц.
- Провайдер:
DeepL. - Модель и промпт не заполняются — DeepL не языковая модель. Ему отдаётся текст и два кода языка, инструкции он не понимает.
- Базовый URL — здесь это выбор тарифа, а не прокси:
| Тариф | Базовый URL |
|---|---|
| DeepL API Free | https://api-free.deepl.com (подставляется, если поле пусто) |
| DeepL API Pro | https://api.deepl.com |
Ключ Pro на бесплатном адресе не работает, и наоборот
Это самая частая поломка на DeepL: ключ правильный, адрес от другого тарифа — и провайдер отвечает отказом авторизации. Купили Pro — впишите https://api.deepl.com вручную, само оно не переключится.
Разметка сохраняется: запрос уходит с tag_handling: html, потому что значения блоков сплошь и рядом несут инлайновый HTML.
Промпт
По умолчанию просит вернуть только перевод, сохранив HTML и плейсхолдеры:
Translate the following text from [[+from]] to [[+to]].
Reply with the translation only, preserving any HTML markup and placeholders:Переводимый текст дописывается драйвером сам — в шаблоне его подставлять не надо и экранировать нечего.
[[+from]] и [[+to]] подставляются НАЗВАНИЕМ языка, а не кодом
UKRAINIAN, а не UK. И это не косметика: uk — украинский по ISO 639-1, но «UK» для любой модели ещё и United Kingdom. Промпт «translate to UK» давал английский текст в шести случаях из тридцати, с UKRAINIAN — ноль из тридцати. На боевом так испортилось 604 украинских описания, прежде чем это заметили.
Ловушка не одна: cs путают с сербским, sl со словацким, hy с венгерским. Поэтому название подставляется всем языкам, а не только украинскому. Чего нет в списке названий, уходит кодом, как раньше.
Промпт — та ручка, за которую надо браться, когда провайдер упорно дописывает вокруг перевода пояснения или корёжит ваши {$плейсхолдеры}.
Опции
Поле Опции — это JSON, который складывается в data строки. Всё необязательное.
| Ключ | По умолчанию | Что делает |
|---|---|---|
timeout | 60 | Таймаут запроса, секунды |
retries | 3 | Сколько раз повторять запрос, упёршийся в частоту |
model_long | — | Вторая модель, для длинных текстов |
model_long_from | 400 | Порог в символах, с которого берётся model_long |
model_by_language | — | Модель, назначенная конкретным языкам: {"de": "…"} |
{
"timeout": 90,
"model_long": "gpt-4o-mini",
"model_long_from": 400,
"model_by_language": {"ro": "gpt-4o"}
}Две модели: по длине и по языку
Сайту обычно нужны две. Сильная — для заголовков и описаний: они попадают в выдачу, и сухой, глотающий артикли перевод там виден. Дешёвая и быстрая — для текста под сгибом, где та же сухость ничего не стоит.
Выбор идёт по длине, а не по имени поля: драйвер получает одну строку и не знает, что это такое, а протащить имя поля через три слоя значило бы выражать то же, что уже выражает длина (мета — пара сотен символов, текст — тысячи). Пока model_long не задан, работает одна модель и поведение ровно прежнее.
model_by_language старше выбора по длине. Длина говорит о тексте, язык — о том, насколько дорого ошибиться: витринный язык ведут целиком, и там качество важнее цены. Ключ — код языка в том виде, в каком его получает драйвер.
Расход считается в разрезе моделей, иначе общий счёт при двух моделях бесполезен: дешёвая и дорогая складываются в одно число, по которому деньги не посчитать. Модель берётся из ответа провайдера, а не из настройки, — при разделении по длине настройка назвала бы одну.
Что провайдеру не отправляется
Три вида значений отсеиваются до запроса и возвращаются как есть:
- пустая строка — переводить нечего;
- число — отправить
2024в языковую модель и получить обратно пересказ вполне реальный риск, а смысла нет; - адрес, а не фраза — значение без пробелов, содержащее
/,?,=или&.
Третье правило появилось не из теории. Поля конструктора типизированы виджетом, а не смыслом: ссылка в textfield для фильтра по типу выглядит точно так же, как заголовок. На одном сайте order_url хранил …/dobavit-gruz?transport_type=2 и уезжал в переводчик на каждой странице каждого языка — десятки тысяч платных запросов за тем же самым ответом, и битая ссылка в день, когда модель решит перевести слаг.
Правило намеренно узкое: нужны оба признака — нет пробелов и есть символ адреса. Одно обычное слово («Компании») не имеет ни того ни другого и переводится как обычно.
Короткая строка переводится в одну строку
Если в исходнике нет перевода строки, из ответа берётся только первая непустая строка.
На коротких надписях модель иногда не останавливается после перевода, а продолжает «похожим» текстом: к переводу «Кто это видит» трижды подряд приходил верный «Cine vede acest lucru», а следом — десяток чужих фраз про cookies, выход из аккаунта и даже CSS с другого сайта. Повтор не спасает, на этой строке срабатывает устойчиво. Верный перевод при этом всегда идёт первой строкой. Многострочные исходники не трогаются — там разбивка осмысленна.
Он никогда не бросает исключение
translate() всегда возвращает строку. Любой сбой возвращает исходный текст и пишет причину в лог MODX:
| Ситуация | Результат |
|---|---|
| Пустой текст, число или адрес | Возвращается как есть, запрос не уходит |
| Нет ключа API | Исходный текст, причина в логе |
Нет модели у gemini | Исходный текст, причина в логе |
| Сбой транспорта или ошибка провайдера | Исходный текст, причина в логе |
| Отказ провайдера переводить | Исходный текст, причина в логе |
| Ответ не разобрался | Исходный текст, ответ в логе |
| Неизвестный драйвер | Фабрика возвращает пустоту, строка в логе |
Так задумано: прогон перевода по сотням строк не должен обрываться на середине из-за одного таймаута. Но из этого же следует, что тихий сбой выглядит как «ничего не перевелось». Если так вышло — сперва читайте лог, а уже потом меняйте настройки.
Частота и суточный лимит — это разные отказы
Провайдер отвечает 429 в двух случаях, и путать их дорого.
- «Слишком часто» лечится паузой. Если провайдер сам назвал её («try again in 8.64s»), берётся она, не больше минуты; не назвал — пять секунд. Попыток столько, сколько в
retries. - «Запросов на сегодня больше нет» паузой не лечится: до полуночи ответ не изменится. Такой отказ узнаётся по тексту (
per day,RPD,daily) и останавливает весь прогон, а не одну строку.
Второе отделено от первого не на всякий случай: прогон, который этого не понимал, выпустил 74 165 отказов за один вечер и раздул лог.
Бюджет токенов
У LLM-драйверов лимит на ответ считается от входа — примерно двадцать токенов на слово плюс минимум для коротких строк, потолок 8192. Перевод по длине сопоставим с оригиналом, а потолок стоит, чтобы не оплатить сорвавшийся ответ.
Что переводится
Переводы хранятся по языкам в pb_translations, языки настраиваются в pb_languages. Страница показывает оригинал, когда перевода нет, поэтому недоделанный перевод никогда не опустошает сайт.
Платите вы
Все эти провайдеры берут деньги за запрос. Компонент не группирует запросы, не кэширует их и не сверяется с прошлыми прогонами — перевести одно и то же поле дважды стоит вдвое. Начните с нескольких строк и посмотрите на результат, прежде чем натравливать его на целую таблицу.