"Боль — это боль, как ее ты не назови.
Это страх, там где страх, места нет любви."
Агата Кристи
Сколько проектов не проходило через мои руки — всегда одно и то же — файлы переводов в ужасном состоянии. Лучше всего эту проблему выражают слова Агаты кристи в начале статьи. Здесь не будут разбираться лингвистические нюансы и качество переводов. Предположим у нас имеются отменные переводы. Под катом разбираются те проблемы, которые можно проконтролировать техническими средствами и инструменты предназначенные для этого. Материал рассчитан на людей имеющих опыт работы с Symfony и в целом с консолью Linux. Также предполагается, что вы умеете подключать сторонние бандыл в проект. Поэтому часть вопросов не разбирается с позиции “и так всё понятно”.
Из всех проблем связанных с переводами есть те, которые очень трудно контролировать техническими средствами. Например, качество переводов, их семантическое значение и уместность использования определенных слов в той или иной части пользовательского интерфейса. И есть те, что довольно просто контролировать технически. Из последней категории, с чем обычно сталкиваемся на проектах, можно выделить:
Отсутствие части переводов. Даже если добавлены переводы для одного-двух языков, то не добавлены для остальных.
Не добавлены переводы вовсе.
Использование похожих, но тем не менее разных ключей для одних и тех же переводов.
Разные переводы для одних и тех же ключей (дубликаты ключей). В результате получаем, что нужный перевод есть, но отображается не то, что ожидаем.
Но это внешнее проявление проблемы. Что лежит в её основе?
Посидев и подумав что мешает разработчикам поддерживать их в хорошем состоянии я выделил для себя 3 основных причины:
Всем людям удобно пользоваться отсортированными списками. Но что реально видит перед собой разработчик в файлах? Всё перемешано. Один кинул в спешке всё как получилось. Другой просто поленился упорядочить ключи. Сверху наслоились мерджи веток и решение конфликтов в них. И вот уже в файлах чёрт ногу сломит.
С первой проблемой тесно связана вторая: разносить переводы по файлам реально долго и нудно. Явно не то, чем хотел бы заниматься разработчик, хоть это и часть его работы. И далеко не всегда у разработчика есть время привести всё в порядок. И всё равно есть вероятность механической ошибки.
Где же наши тесты? Ну правда. Есть прекрасные инструменты для тестирования кода. Например PHPUnit. Как насчёт тестов файлов с переводами, которые можно легко настроить в CI проекта? И пусть каждый мердж-реквест проверяется на предмет того, что всё добавлено.
Первым делом я стал искать существующие инструменты. Казалось, что проблема стара как мир и уже должно быть написано 100500 инструментов. Посмотрел, что предоставляет нам стандартный пакет “symfony/translation”. Погуглил инструменты для разработчиков, на сколько мне хватило терпения, но того что искал не нашёл. Если в комментариях дадут ссылки на существующие инструменты, предназначенные для решения проблем описанных в статье, то буду очень благодарен.
Имея такие вводные я решил создать свой инструмент. Если модераторы пропустят, то здесь будет ссылка на репозиторий. В противном случае ищите проект на Github-е. В текущей реализации проект представляет собой бандл для Symfony 4.4+ проектов. Ниже описано какие конкретно проблемы решает пакет и как им пользоваться.
Разбираемся с кашей в файлах
Первое, на что я нацелился, — это “каша” в файлах. На всех последних проектах, где я работал и работаю переводы хранятся в YAML файлах. С них и начал. Их особенность — возможность хранения ключей в виде структурированного удобочитаемого дерева. При загрузке в траслейтор они склеиваются через точку. Кроме плюсов, это дает возможность задать разные переводы для одних и тех же ключей даже не заметив этого. Значит нужно сделать две вещи:
Преобразовать ключи. Где возможно, разделить ключи склеенные через точку, а где-то наоборот склеить, чтобы построить более компактное дерево.
Отсортировать по ключам.
Так проще искать и применять существующие ключи. И подобные ключи (с опечатками и дубли) окажутся рядом и их будет проще исправить. Кроме удобства чтения и поиска существующих ключей, мы получаем ещё и меньше конфликтов при мердже веток.
Для этого была создана соответствующая консольная команда:
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 и ни один мердж не пройдёт с неполными переводами. Это конечно не защищает от того, если какой-то ключ перевода не добавлен ни в один файл, но уже сильно снижает число проблем.
Планы на будущее. Послесловие
В ближайших планах:
Добавить возможность проверки на соответствие ключей определённому паттерну и возможность проверки отдельных доменов. На тот случай, если на проекте разным доменам предъявляются разные требования.
Расширить совместимость с более старыми проектами.
Так же планируется реализовать использование API Google/Yandex для автоматических переводов. Для ряда проектов это вполне приемлемо.
Проект открытый. Буду рад любому участию и здоровой критике в комментариях.