Наш кейс: в приложении есть русский (наш нативный) и английский языки. Надо быстро и просто добавлять другие (по запросам от клиентов). В файлах с переводами был хаос: дублирование строк, конкатенация вместо плейсхолдеров, разный порядок строк в файлах переводов для ru/en, висячие пробелы и многое другое.
Я решил написать вспомогательный инструмент, который помог решить все эти проблемы. Сейчас мы добавляем новый язык буквально за 40 минут и 2$. Все получилось настолько хорошо, что я решил причесать и выложить проект в open-source.
Главная фишка: перевод на новые языки делается сразу с 2х языков: первичного (в нашем случае — русский) и вторичного (у нас — английский). Вторичный язык на практике не обязателен, но желателен. Сколько бы ни было у вас переводов на другие языки, в контекст LLM будет попадать только первичный и вторичный языки.
К слову, в контекст также передается содержимое соседних строк и глоссарий (о нем ниже), а промпт построен так, что в LLM сначала пишет комментарий о том, что это за строка, где используется и для чего, и только потом осуществляет перевод. Комбинация этих подходов, по моим тестам, кратно увеличивает качество перевода. Выглядит все это так:
Под капотом вы можете использовать любую LLM, совместимую с OpenAI API (chat/completions). По моим тестам, DeepSeek (не думающий) справляется не хуже передовых моделей OpenAI. Мы у себя используем DeepSeek с deepinfra.com — одно время там было даже чуть дешевле, чем официальное API, но мы перешли туда из-за большей скорости ответа (возможно сейчас ситуация уже изменилась — не знаю)
Глоссарий
Какой бы умной не была LLM, переводить какую-то специфическую терминологию вашего приложения она всегда будет немного по-разному. Для этих случаев в приложении предусмотрен глоссарий, который также попадает в контекст при переводе строк. Чтобы не забивать контекст лишней информацией, туда попадают только те термины, которые есть в первичном языке. Для поиска терминов используется комбинация similar_text и levenshtein благодаря чему разные падежи и склонения вашей терминологии все равно будут попадать в контекст и осуществлять единообразный перевод. Если кому интересно, я пробовал использовать embeddings через BAAI/bge-m3-multi, но на практике это работало хуже.
Добавление новых языков в глоссарий делается сразу через LLM, с автоматическим переводом всех терминов. Однако тут я все же рекомендую отдельно проконсультироваться по каждому термину либо с носителем языка, либо с любой LLM чтобы она вам объяснила в каком контексте и как используется тот или иной термин, а затем при необходимости внеси корректировки вручную.
Также, есть функционал создания специальных глоссариев, которые используются для ключей с определенными префиксами. Это актуально для случаев, когда у вас есть отдельные, крупные «зоны предметной области», имеющие свою терминологию.
Про переводы
-
Поддержка форматов: пока только JSON (flat & structured) + плюрализация i18next, но добавить другие форматы файлов очень легко.
-
Плюрализация: поддерживаются
cardinalиordinalформы. Пример:{"key_one": "1 файл","key_other": "{{count}} файлов"} -
Плейсхолдеры:
${likeJs},{{doubleCurve}},{singleCurve}— легко добавить новые форматы. Предпочитаемый формат задается в настройках для каждого проекта -
Порядок строк который был в вашем файле сохраняется! Это важно для смысла и для LLM.
-
Многострочные строки: поддержка
\rи\n(настраивается). -
Комментарии к строкам: можно добавить пояснения, которые хранятся только в приложении. По умолчанию генерируются через LLM автоматически.
-
Рекомендованное значение: можно не менять перевод, а указать отдельно (например профессиональный переводчик или функция AI Suggest).
-
Массовый или одиночный перевод, с выбором LLM для каждого языка.
-
Reuse переводов: при массовом переводе берутся уже переведённые совпадающие строки.
-
Старые строки и переводы не удаляются, а продолжают хранится в базе. Это частично покрывает функционал ветвления в git, когда в одной ветке у вас уже есть новые переводы, а в другой их еще нет. Ничего не потеряется.
Валидация строк
Когда мы плотно подошли к вопросу переводов и локализации, то быстро выяснилась, что в файлах переводов творится настоящая каша. Помимо непереведенных строк, были и лишние переводы (те строки, что уже были удалены из первичного языка). Было много мест, где вместо плейсхолдеров использовалась конкатенация строк, были переводы, где в первичном (русском) языке используется символ двоеточия, а во вторичном (английском) — нет, или в первичном есть символ переноса строки \n, а во вторичном — нет. И даже строки, в которых в русском был плейсхолдер, а в английском его забыли.
Все эти кейсы были учтены, и сейчас все загруженные и переведённые строки проходят проверку и получают флаг ⚠️ Warning если:
-
Строка перевода пустая
-
Есть висячие пробелы в начале или в конце строки
-
Строка содержит несколько пробелов, идущих подряд
-
Строка не переведена и совпадает с главным или вторичным языком (вшито исключение для слов
email,api,ip,url,uri,id) -
В строке перевода потерян
{{плейсхолдер}}, который есть в главном языке -
В строке перевода есть устаревший
{{плейсхолдер}}, которого уже нет в главном языке -
Количество переносов строк (
\rили\n) в главном языке и переводе не совпадает -
Количество символов двоеточия
:в главном языке и переводе не совпадает -
Нет плюрализованного значения, которое должно быть задано для языка перевода
-
Есть плюразованное значение, которого не должно быть для языка перевода
-
Плюрализованные значения содержат разное количество переносов строк или символов двоеточия
Вне зависимости от валидации, пользователь может принудительно пометить строку как верифицированную, что дает возможность гибко фильтровать строки и управлять массовым переводом
Навигация, фильтрация и сортировка

-
Language— Поиск/фильтрацию определенных значений можно осуществлять как по строкам на всех языках, так и выбрать конкретный язык в списке. -
Type— обычная строка или с плюрализацией (один, два, много итд) -
Updated— дата изменения значения в первичном языке (сортировка по клику на label) -
Touched— дата изменения любого значения ключа на любом языке (сортировка по клику на label) -
Suggestion— есть ли рекомендация к переводу (например когда переводчик прошелся по строкам и предложил для записи альтернативный перевод не заменяя существующий) -
Verified— верифицирован ли перевод человеком. Полезно в случаях, когда у строки перевода есть⚠️ Warning(например, переведенное значение совпадает с первичным) -
Position— нужен по большей части для сортировки строк в том порядке, в каком они идут у вас в файле переводов -
Те пункты, что не перечислил должны быть понятны интуитивно
Массовые операции
-
🔤 AI Batch translate — массовый перевод всех строк на новый язык. Параметры настраиваются при запуске. Можно например либо всё переводить всегда, либо брать переводы из уже переведенных, но идентичных строк.
-
💡 AI Batch suggest — рекомендации ИИ (например, исправить ошибки, добавить диакритику), промпт пишете сами, результаты можно принять или отклонить вручную. Я например просил так GPT и DeepSeek заменить
енаётам где это требуется по смыслу, но ни одна ни другая модель с этим нормально не справилась. -
✂️ Batch modify — массовое удаление комментариев, рекомендаций, висячих пробелов или переводов
Проекты, пользователи и роли
-
Неограниченное количество проектов, изоляция переводов и всего остального друг от друга.
-
Бэкап и восстановление проекта (возможно частичное восстановление, например только глоссарий). Бэкап шифруется ключом из .env, чтобы сохранить целостность данных и не слить в открытом доступе токен к LLM моделям
-
В проекте любое количество пользователей с разными ролями:
-
Admin — полный доступ
-
Developer — всё, кроме управления пользователями и LLM
-
Translator — работа только с разрешёнными языками, редактирование глоссария, может ставить
📢 Alert message -
Guest — только просмотр
-
LLM-модели
Как я уже упоминал выше, можно использовать любую LLM, совместимую с OpenAI API (chat/completions).

-
Можно задать цену за токены, и дальше видеть что и сколько стоило. Например, сколько вам обошелся перевод глоссария (видно прямо в глоссарии) или конкретного ключа (видно в каждом ключе).
-
В столбце
costвиден суммарный расход по конкретной модели -
Можно задать https или socks прокси для конкретной модели
Другие инструменты
-
📢 Alert message — системное уведомление прямо в интерфейсе. Например, когда работает человек-переводчик, он может попросить других не запускать массове операции.
-
🔠 Text translate — перевод произвольных текстов через LLM с учетом вашего глоссария (например, новостей для блога).
-
🔢 Plurals — формы плюрализации и примеры для выбранного языка
-
🏘️ Groups analyzer — анализ больших/маленьких групп строк (родительские ключи в JSON), помогает выявить висячие строки и навести порядок.
-
👯 Duplicate analyzer — поиск дубликатов строк, контроль единообразия переводов. Сейчас работает просто сравнивая строки «как есть», разве что без учета регистра. Тут я тоже пробовал применять embeddings через BAAI/bge-m3-multi, но толку было мало, только работало дольше. Отказался в пользу простого сравнения
-
🕳️ Loosed placeholders analyzer — поиск строк, где плейсхолдер должен быть, но его нет (например, когда используют конкатенацию вместо плейсхолдера).
Как развернуть у себя?
Повторять документацию не буду, все описано здесь.
Если ставите на новую, чистую VPS — берите вариант с Traefik — так меньше телодвижений с сертификатами.
Если хотите делать прокси через nginx или что-то еще, то берите вариант без Traefik.
Вы можете использовать проект по назначению, в том числе для перевода коммерческих продуктов абсолютно бесплатно и без ограничений. Лицензия запрещает извлекать коммерческую выгоду в виде предоставления доступа к локализатору как к SaaS-сервису или в виде готовых образов для быстрого развертывания на хостингах.
Оффтоп
P.S. Честно говоря, есть еще куча мелких фишек и тонкостей, о которых можно было бы рассказать, но честно говоря, я устал 🙂 Самой сложной частью было оформить все это для простого использования и развертывания в пару команд, написать readme да и саму эту статью
P.P.S. Почему не Symfony/Laravel/Nextjs/итд/итп? Почему не Doctrine а какая-то странная ORM? Потому что так сложилось. Это был внутренний проект, изначально как часть внутренней админки, на которую у нас и так вечно не хватает времени. ORM у нас своя, со своей спецификой работы одновременно с несколькими СУБД (на практике будет нужно 1% разработчиков). Когда-нибудь я возможно расщедрюсь на написание доки к ней и статьи на хабр. Когда-нибудь, возможно и этот проект перепишу по красоте на nextjs. Но пока так, что есть, то есть. Работает отлично, пользуйтесь на здоровье 🙂
P.P.P.S. Даже если оно вам не надо, поставьте звездочку на GitHub — вам не сложно, мне приятно 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/925110/
Добавить комментарий