Наш кейс: в приложении есть русский (наш нативный) и английский языки. Надо быстро и просто добавлять другие (по запросам от клиентов). В файлах с переводами был хаос: дублирование строк, конкатенация вместо плейсхолдеров, разный порядок строк в файлах переводов для 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 - вам не сложно, мне приятно)
domix32
Fluent на пыхе?
XAKEPEHOK Автор
Это совершенно другой продукт для других задач