Перевод Angular-приложения одной командой: утилита translate-xlf
Каждая новая строка в интерфейсе — правка во всех языковых файлах. Рассказываю, как небольшая утилита на Node.js забрала эту рутину и какие решения в ней по-настоящему экономят время.
messages.xlf с исходными строками и по файлу на каждый язык — messages.en.xlf, messages.tr.xlf и так далее. Пока проект живёт, строки появляются постоянно: новая кнопка, подсказка, текст ошибки. И каждая такая строка — это правка во всех языковых файлах сразу.
Что должно было получиться
одна команда, без настроек и ключей; служебные теги внутри строк не ломаются; то, что уже перевёл человек, не перезаписывается; в merge request видны только переведённые строки; если что-то пошло не так, это видно сразу, а не после релиза.
Одна команда и ноль настроек
fs и https. Ставится одной командой и запускается в папке с переводами:
npm install -g translate-xlf
cd src/locale
translate-xlfmessages.en.xlf — английский, messages.kk.xlf — казахский. Файл без кода языка — это исходный messages.xlf, его утилита пропускает. Если какой-то язык ведёт живой переводчик, его можно исключить:
translate-xlf --exclude=enstate="new" и с русским текстом вместо перевода — так их добавляет, например,
Теги внутри строк остаются целыми
<source>Итого: <x id="INTERPOLATION" equiv-text="{{ total }}"/> ₽</source>__XML_TAG_0__, __XML_TAG_1__ и так далее. Такие метки переводчик оставляет как есть, а после ответа утилита возвращает теги на место. Регистр при этом не важен: бывает, что Google возвращает метку как __xml_tag_0__, и восстановление это учитывает. Результат:
<target state="translated">Total: <x id="INTERPOLATION" equiv-text="{{ total }}"/> ₽</target>Ручная работа не перезаписывается
state="translated" или state="final", утилита её не тронет — ни сейчас, ни при следующих запусках. Поэтому запускать её можно сколько угодно раз: каждый прогон переводит только новое.
--keep-state: переведённые строки остаются со state="new", и тот, кто проверяет переводы, точно знает, какие из них написала машина.
translated, и утилита её больше не трогает. Менять статус лучше сразу: пока строка в new, повторный запуск переведёт её заново.
В merge request — только переведённые строки
<target> нужной записи и заменяет только его, сохраняя отступ. Остальные строки не меняются. Вот изменение после прогона на тестовом файле:
- <target state="new">Итого: <x id="INTERPOLATION" equiv-text="{{ total }}"/> ₽</target>
+ <target state="translated">Total: <x id="INTERPOLATION" equiv-text="{{ total }}"/> ₽</target>Ошибка в одной строке не роняет весь прогон
--batch-size. Если строка не перевелась, утилита через секунду пробует ещё раз. Не получилось и со второй попытки — в перевод записывается исходный текст с меткой:
<target state="translated">[ОШИБКА ПЕРЕВОДА] Доставим завтра, если оформить заказ до 18:00</target>- <target state="translated">[ОШИБКА ПЕРЕВОДА] Доставим завтра, если оформить заказ до 18:00</target>
+ <target state="translated">We will deliver tomorrow if you place your order before 18:00</target>MAX_ELEMENTS=50 translate-xlf переведёт первые 50 строк каждого файла. Можно посмотреть на результат, прежде чем запускать всё.
Октябрь 2026: Google начал отвечать 429
Google отвечал не JSON, а HTML-страницей «We're sorry... your computer or network may be sending automated queries». Скрипт не проверял HTTP-код и пытался разобрать её как JSON, поэтому в логе была только невнятная «Ошибка парсинга ответа». Метка ошибки записывалась со state="translated". Следующий запуск считал такие строки переведёнными и пропускал — ошибки оседали в файлах навсегда.Итог в конце сообщал «Переведено элементов: N/N», даже если не перевелось ничего.
Утилита, которая ломается молча, опаснее ручной работы: ошибку замечаешь, когда её уже увидели пользователи.
curl возвращал 200, а через Node — 429. Значит, блокировали не сеть и не IP, а сам клиент. Встроенный в Node fetch тоже проходил, а модуль https — нет. Разница нашлась в TLS-рукопожатии: https по умолчанию не отправляет расширение ALPN, и по этому отпечатку Google принимал скрипт за бота. Лечится одной настройкой агента:
const translateAgent = new https.Agent({
keepAlive: true,
ALPNProtocols: ['http/1.1'],
});fetch, и запросы снова проходят.
в тексте ошибки — HTTP-код и начало ответа, например HTTP 429 Too Many Requests: We're sorry...;строки с меткой ошибки переводятся заново при следующем запуске; в итоге отдельно указано, сколько строк переведено и сколько упало; в README появился раздел «Если перевод перестал работать»: две команды, curlи Node, и подсказки, как читать их ответы. В следующий раз диагностика займёт минуту.
Чего утилита не делает
Не заменяет переводчика. Это быстрый черновик, его стоит вычитать — особенно короткие строки без контекста.Не работает с XLIFF 2.0. Поддерживается XLF 1.2 — формат, который Angular выдаёт по умолчанию.Не разбирает ICU-выражения. pluralиselectуходят в переводчик как обычный текст, их лучше проверять глазами.Не подходит для пользовательского контента. Эндпоинт неофициальный, и для сервиса, который переводит тексты пользователей, нужен платный API. Для черновиков интерфейса его хватает.
Итог
теги не ломаются — результату можно доверять; ручные правки не перезаписываются — запускать можно сколько угодно; в диффе только переводы — их можно проверить на ревью; ошибки видны и исправляются повторным запуском — сбой не прячется в файлах.