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

  • Строка перевода пустая

  • Есть висячие пробелы в начале или в конце строки

  • Строка содержит несколько пробелов, идущих подряд

  • Строка не переведена и совпадает в главным или вторичным языком (вшито исключение для слов emailapiipurluriid)

  • В строке перевода потерян {{плейсхолдер}}, который есть в главном языке

  • В строке перевода есть устаревший {{плейсхолдер}}, которого уже нет в главном языке

  • Количество переносов строк (\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 - вам не сложно, мне приятно)

Комментарии (2)


  1. domix32
    08.07.2025 11:10

    Fluent на пыхе?


    1. XAKEPEHOK Автор
      08.07.2025 11:10

      Это совершенно другой продукт для других задач