"Боль это боль, как ее ты не назови.
Это страх, там где страх, места нет любви."
Агата Кристи

Сколько проектов не проходило через мои руки — всегда одно и то же — файлы переводов в ужасном состоянии. Лучше всего эту проблему выражают слова Агаты кристи в начале статьи. Здесь не будут разбираться лингвистические нюансы и качество переводов. Предположим у нас имеются отменные переводы. Под катом разбираются те проблемы, которые можно проконтролировать техническими средствами и инструменты предназначенные для этого. Материал рассчитан на людей имеющих опыт работы с Symfony и в целом с консолью Linux. Также предполагается, что вы умеете подключать сторонние бандыл в проект. Поэтому часть вопросов не разбирается с позиции “и так всё понятно”.


Из всех проблем связанных с переводами есть те, которые очень трудно контролировать техническими средствами. Например, качество переводов, их семантическое значение и уместность использования определенных слов в той или иной части пользовательского интерфейса. И есть те, что довольно просто контролировать технически. Из последней категории, с чем обычно сталкиваемся на проектах, можно выделить:

  1. Отсутствие части переводов. Даже если добавлены переводы для одного-двух языков, то не добавлены для остальных.

  2. Не добавлены переводы вовсе.

  3. Использование похожих, но тем не менее разных ключей для одних и тех же переводов.

  4. Разные переводы для одних и тех же ключей (дубликаты ключей). В результате получаем, что нужный перевод есть, но отображается не то, что ожидаем.

Но это внешнее проявление проблемы. Что лежит в её основе?

Посидев и подумав что мешает разработчикам поддерживать их в хорошем состоянии я выделил для себя 3 основных причины:

  1. Всем людям удобно пользоваться отсортированными списками. Но что реально видит перед собой разработчик в файлах? Всё перемешано. Один кинул в спешке всё как получилось. Другой просто поленился упорядочить ключи. Сверху наслоились мерджи веток и решение конфликтов в них. И вот уже в файлах чёрт ногу сломит.

  2. С первой проблемой тесно связана вторая: разносить переводы по файлам реально долго и нудно. Явно не то, чем хотел бы заниматься разработчик, хоть это и часть его работы. И далеко не всегда у разработчика есть время привести всё в порядок. И всё равно есть вероятность механической ошибки.

  3. Где же наши тесты? Ну правда. Есть прекрасные инструменты для тестирования кода. Например PHPUnit. Как насчёт тестов файлов с переводами, которые можно легко настроить в CI проекта? И пусть каждый мердж-реквест проверяется на предмет того, что всё добавлено.

Первым делом я стал искать существующие инструменты. Казалось, что проблема стара как мир и уже должно быть написано 100500 инструментов. Посмотрел, что предоставляет нам стандартный пакет “symfony/translation”. Погуглил инструменты для разработчиков, на сколько мне хватило терпения, но того что искал не нашёл. Если в комментариях дадут ссылки на существующие инструменты, предназначенные для решения проблем описанных в статье, то буду очень благодарен.

Имея такие вводные я решил создать свой инструмент. Если модераторы пропустят, то здесь будет ссылка на репозиторий. В противном случае ищите проект на Github-е. В текущей реализации проект представляет собой бандл для Symfony 4.4+ проектов. Ниже описано какие конкретно проблемы решает пакет и как им пользоваться.

Разбираемся с кашей в файлах

Первое, на что я нацелился, — это “каша” в файлах. На всех последних проектах, где я работал и работаю переводы хранятся в YAML файлах. С них и начал. Их особенность — возможность хранения ключей в виде структурированного удобочитаемого дерева. При загрузке в траслейтор они склеиваются через точку. Кроме плюсов, это дает возможность задать разные переводы для одних и тех же ключей даже не заметив этого. Значит нужно сделать две вещи:

  1. Преобразовать ключи. Где возможно, разделить ключи склеенные через точку, а где-то наоборот склеить, чтобы построить более компактное дерево.

  2. Отсортировать по ключам.

Так проще искать и применять существующие ключи. И подобные ключи (с опечатками и дубли) окажутся рядом и их будет проще исправить. Кроме удобства чтения и поиска существующих ключей, мы получаем ещё и меньше конфликтов при мердже веток.

Для этого была создана соответствующая консольная команда:

aeliot_trans_maintain:yaml:transform PATH_TO_FILE_IN PATH_TO_FILE_OUT

Вызывается как обычная команда Symfony. Если передан только один аргумент, то PATH_TO_FILE_OUT становится равным PATH_TO_FILE_IN. Это даёт простор. Добавляем несколько стандартных команд Linux типа find или grep и вот уже можно преобразовать все нужные файлы. Например, можно преобразовать все YAML файлы в определённой директории:

find PATH_TO_DIRECTORY -type f \( -iname \*.yml -o -iname \*.yaml \) | sort | xargs  -I {} -t  php  bin/console aeliot_trans_maintain:yaml:transform $1{}

Находим и переводим всё, что пропущено

Дальше нужно добавить пропущенные переводы. Проще всего — это взять список пропущенных переводов и закинуть в переводчик, например Google Translate. Он неплохо справляется с переводами на европейские языки, например с английского на немецкий или французский. Можно сразу с ключами. Потом поправить ключи и вставить в нужный файл.

Для этой задачи тоже создана команда:

bin/console aeliot_trans_maintain:yaml:export_missed_translations DOMAIN DONOR_LOCALE FILTER_BY_LOCALE

Где:

  • DOMAIN — обрабатываемый домен. 

  • DONOR_LOCALE — локаль из которой берутся переводы 

  • FILTER_BY_LOCALE — локаль для которой идёт выгрузка. Если указана, то будут выгружены только переводы, пропущенные в этой локале. Иначе все, что пропущены хотя бы в одной локали этого домена.

Ключи будут к склеенном виде. Полученные данные выгружаются в StdOut. Можно направить его сразу во временный файл. Например:

bin/console a:y:e messages en de > ./donor.txt

Если кто-то ещё не знал, то имена консольных команд в Symfony можно сокращать. Например как здесь по первым буквам.

Я обычно создаю ещё один файл для данных из онлайн переводчика и с помощью сравнения двух файлов в PhpStorm быстренько поправляю ключи. После чего эти данные можно закидывать в целевой файл. Конечно, для получения идеальных переводов нужна работа профессиональных переводчиков, но это тема другой статьи.

Внимание! У всех этих команд есть одно ограничение. Поскольку для создания YAML файлов используется стандартный Symfony дампер (\Symfony\Component\Yaml\Yaml::dump()), то все переводы состоящие из одного слова без спецсимволов не будут заковычены. Да, пока не идеально. В дальнейшем это планируется исправить.

Декоратор для транслейтора

Ещё один инструмент для того, чтобы все переводы были добавлены — декоратор для стандартного транслейтора. Включается в настройках бандла (ключ конфига: insert_missed_keys). После его включения все ключи, вызванные во время работы проекта, но пропущенные в файлах переводов, будут добавлены в соответствующие домен и локаль.

На текущий момент принимает значения:

  • no — отключен. Значение по умолчанию.

  • end — вставлять найденные ключи в конец файл в склеенном виде

  • merge — встроить в дерево ключей. Экспериментальный вариант и будет дорабатываться.

На текущий момент рекомендованы к использованию только значения: no и end. Также рекомендуется включать декоратор только в dev режиме и только на время, т.к. сильно замедляет работу сайта. Из-за того, что постоянно изменяются файлы переводов и из-за этого постоянно сбрасывается и пересобирается кэш.

Пример подключения в конфиге:

parameters:
   env(TRANS_MAINTAIN_INSERT_MISSED_KEYS): no

aeliot_trans_maintain:
   insert_missed_keys: "%env(TRANS_MAINTAIN_INSERT_MISSED_KEYS)%"

После этого легко включать/отключать декоратор транслейтора изменяя значение ключа TRANS_MAINTAIN_INSERT_MISSED_KEYS в .env или .env.local файлах без риска закоментить чего-нибудь лишнего.

Отлично. Файлы преобразовали. Ошибки исправили. Пропущенные переводы добавили. Всё бы хорошо… но консистентность переводов всё ещё на совести разработчика. А как же тесты? Куда без них в современно проекте?

Автоматическое тестирование переводов в проекте

Для тестирования переводов создана команда:

aeliot_trans_maintain:lint:yaml KEY_1 KEY_2 KEY_N

Возвращает статус 0 если проблем не найдено и 1 если есть проблемы. И в StdOut отдаёт таблицы отчётов. В качестве аргументов принимает ключи проверок или названия пресетов. Может принимать сразу несколько аргументов.

Пресеты:

  • base — выполнить основные проверки подходящие большинству проектов (рекомендовано).

  • all — выполнить все возможные проверки. Зарезервировано на будущее. Сейчас то же, что base за тем исключением, что all должен быть единственным аргументом команды.

Ключи проверок:

  • files_missed — проверяет существование файлов домена перевода для всех локалей, использованных в проекте. Выводит таблицу доменов переводов и локалей, пропущенных для каждого домена. Если домен представлен во всех локалях, то в список не попадёт.

  • keys_missed — проверяет наборы ключей, использованных в разных локалях одного домена. Выводит домено, результирующих ключей (конкатенированных через точку) и список локалей, где они пропущены. В список попадут только ключи, отсутствующие, хотя бы в одной локале.

  • keys_duplicated — проверяет наличие дубликатов ключей. Выводит таблицу домен, локаль, ключ.

Ключи проверок могут идти в комбинации с base. Если указать один и тот же улюч несколько раз, то проверка будет произведена однократно.

Теперь запуск этой команды можно настроить в CI и ни один мердж не пройдёт с неполными переводами. Это конечно не защищает от того, если какой-то ключ перевода не добавлен ни в один файл, но уже сильно снижает число проблем.

Планы на будущее. Послесловие

В ближайших планах:

  1. Добавить возможность проверки на соответствие ключей определённому паттерну и возможность проверки отдельных доменов. На тот случай, если на проекте разным доменам предъявляются разные требования.

  2. Расширить совместимость с более старыми проектами.

  3. Так же планируется реализовать использование API Google/Yandex для автоматических переводов. Для ряда проектов это вполне приемлемо.

Проект открытый. Буду рад любому участию и здоровой критике в комментариях.