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

Нам казалось, что в диспетчерской Яндекс Go всё под контролем. Потом мы запустили аналитический скрипт и выяснили, что 37% интерфейса частично не переведено и пользователи за рубежом видят винегрет из родного языка и дефолтного английского.

Я Ира Туманова, разработчик интерфейсов Яндекс Go. В этой статье расскажу про эволюцию контроля переводов: от ручного труда до автоматизации жизненного цикла ключей. Вы поймёте, почему важно не только настроить работу с переводами на старте проекта, но и отслеживать её качество на всех этапах, а также узнаете, какие маленькие хитрости способны избавить команду от внезапных «переводческих завалов».


Давным‑давно, когда всё только начиналось…

Ещё до того момента, когда в проектах появилась интернационализация, фразы в коде прописывались как есть, безо всяких библиотек и прикрас. Это была эра чистого хардкода.

<div>
   <div>Всем привет!</div>
   <div>
  	Рады, что сегодня вы читаете 	
  	эту статью на Хабре!
   </div>
   <div>Надеемся, вы найдёте для себя что-то интересное и полезное!</div>
</div>

Скорость добавления новой фразы при таком подходе равна скорости набора текста: никаких файлов локализации, ключей и сборок. Текст неотрывен от кода, так что, когда вы удаляете компонент с кнопкой, текст исчезает вместе с ней. В проекте физически не может образоваться «мусор» из неиспользуемых фрагментов локализации. Всё прекрасно, но, чтобы добавить новый язык, придётся переписывать весь проект.

Со временем индустрия шагнула в эру интернационализации (так называется процесс подготовки продукта к добавлению новых языков). По сути, это переход от хардкода к функциям‑обёрткам, которые подставляют нужный текст в зависимости от локали пользователя.

Рынок предлагает три основных решения для интернационализации: i18next, Lingui и FormatJS. Lingui на тот момент был относительно молодым и не слишком популярным решением. i18next и FormatJS были одинаково популярными библиотеками, мы остановились на FormatJS.

Этот инструмент показался нам наиболее поддерживаемым и комфортным для нашего стека. В нём есть богатый набор компонентов и функций для форматирования значений, возможность экстракции сообщений через babel plugin, а также ICU MessageFormat (созданный консорциумом Unicode стандартизированный синтаксис для форматирования сообщений, который позволяет разработчикам учитывать сложности человеческого языка при локализации приложений). 

<div>
   <div>{message({id: "uniq_id_1", default: "Всем привет!")}</div>
   <div>
  	{message({id: "uniq_id_2", default:
  	"Рады, что сегодня вы читаете эту статью на Хабре!")}
   </div>
   <div>{message({id: "uniq_id_3", default: "Надеемся, вы найдёте для себя что-то интересное и полезное!")}</div>
</div>

Среди TMS (Translation Management System — система управления переводами) тоже есть много интересных решений: Lokalise, Phrase, Crowdin, POEditor. У каждого свои минусы и плюсы. Мы, например, выбрали путь написания своего «велосипеда», потому что нам была важна интеграция с другими внутренними сервисами. 

В чём же плюс интернационализации? Теперь добавить новый язык максимально просто. У вас есть файлик, в котором вы храните все локали. Вы вставляете туда новую локаль, добавляете файлы, и — вуаля — у вас уже есть новая локаль! Благодаря TMS можно смотреть, в каком статусе находится каждый ключ, на какие языки он переведён, на какие языки не переведён.

Однако придётся ощутить на себе и минусы такого подхода, так как теперь появляются ручные процессы. Во‑первых, ключи как‑то должны оказаться в TMS. Как им это сделать? Либо мы пишем какой‑то скрипт, который автоматически будет их выгружать, либо, если у нас такого скрипта нет, мы берём ключи и руками вносим их в TMS.

Во‑вторых, появляются неиспользуемые ключи. Теперь код и ключи — это немножко разные сущности. Ключи существуют в TMS. Код существует в кодовой базе. Удалили код — ключ удалился вместе с кодом, но остался в TMS. Соответственно, происходит небольшое захламление. Со временем там накапливаются «мёртвые» ключи, то есть переводы, которые уже никому не нужны. 

Что же делать дальше?

Масштаб и корни нашей проблемы

В какой‑то момент мы решили провести эксперимент — узнать текущее состояние переводов и улучшить его. В нашем продукте уже работала TMS, были инструменты интернационализации и куча ручных процессов. 

Написали скрипт, прогнали по проекту и ужаснулись. Из 9549 ключей около 37% частично не были переведены на целевые языки. Из непереведённых лишь 103 были в работе у переводчиков, а про остальные никто даже не подозревал. В результате в среднем по каждому из 27 языков отсутствовало около 25% переводов: каждый четвёртый элемент интерфейса зарубежные партнёры видели на английском.

В чём же проблема? Разработчик, дописав фичу, должен запустить скрипт экстракции ключей, залить их в TMS и создать задачу в трекере для переводчиков. На практике разработчики забывали запустить скрипт или создать тикет и ключи терялись. 

Ручные процессы порождают ошибки из‑за человеческого фактора. Можно забыть запустить скрипт, не завести задачу для переводчиков, не обновить переводы в приложении. В итоге люди, например, в Грузии или во Франции не смогут понять, что написано в диспетчерской.

CI/CD как единственный источник правды

Чтобы исправить ситуацию, мы решили убрать разработчика из цепочки доставки ключей. Теперь процесс запускается сам в момент, когда Pull Request вливается в основную ветку:

intl-push:
  	title: Intl Push
  	triggers:
    	- on: commit
      	into: trunk
  	max-active: 1
  	flow: intl-push

Скрипт проходит по изменениям в PR, находит добавленные ключи и формирует из них список. Затем для каждого воркспейса запускает команду, которая отправляет все ключи из кода в TMS, и туда же попадают новые. Свежие ключи улетают через API в систему управления переводами. На этом этапе ключи появляются в админке переводчиков. 

Дальше самое важное: интеграция. После загрузки ключей скрипт идёт в API корпоративного Трекера, создаёт задачу на отдел локализации, автоматически проставляет дедлайн и прикладывает ссылки на конкретные ключи в TMS, чтобы переводчикам не пришлось их искать.

Но отправка ключей — полдела, ещё нужно забирать готовые переводы обратно в код. Мы не хотели ждать, пока разработчик вспомнит и нажмёт кнопку «обновить перевод». Вместо этого ровно в 05:00 по Москве запускается cron‑job.

intl-pull:
  	title: Intl Pull
  	flow: intl-pull
  	max-active: 1
  	schedule:
    	timezone: MSK
    	cron: 0 5 * * *

Скрипт стучится в API TMS и спрашивает: «Что нового?» Он выкачивает переводы для всех 27 языков, раскладывает данные по файлам локализации в репозитории, сам создаёт Pull Request с изменениями и, если тесты проходят, отправляет его на автоапрув.

Плюс мы добавили в CI дополнительную проверку на дубликаты. Раньше можно было случайно слить два PR с одинаковыми ключами. Теперь если в коде обнаруживается конфликт ключей, CI просто не даёт слить такой Pull Request.

Две недели «крестового похода» и ручная чистка

Новый CI/CD прекрасно обрабатывал свежие фичи, но накопившийся лингвистический долг, к сожалению, никуда не делся. В итоге мы потратили две недели на то, чтобы просьбами, уговорами и угрозами мотивировать разработчиков разобрать старые ключи, определить контекст и завести на них тикеты. Это была тяжёлая, но необходимая работа. Когда она была закончена, результаты автоматизации проявились во всей красе.

Количество ключей, на которые забыли создать тикеты, сократилось с 3376 до 171. Ручные процессы в жизненном цикле переводов полностью исчезли: разработчики перестали быть узким горлышком и смогли сфокусироваться на коде. Time‑to‑market для переводов стал предсказуемым. Добавление новых языков тоже стало прозрачнее: дашборд сразу показывает огромный столбец «непокрытых» ключей, и видно, какой объём работы предстоит.

Результаты и метрики

Первое, что мы получили, — честная картина. Мы настроили аналитику, которая каждое утро (вместе с тем самым cron‑job) пробегает по проекту и обновляет дашборды. Раньше, чтобы узнать статус перевода на конкретный язык, нужно было проверять задачи для переводчиков или лезть в TMS. Теперь открываем дашборд и видим всё: синие столбцы означают ключи, у которых есть задача на перевод, красные — «нет задачи».

Если столбец какого‑то языка внезапно покраснел на 30%, мы сразу понимаем, что кто‑то влил крупную фичу, а переводы застряли, и можем реагировать до того, как это заметит пользователь.

Отдельно стоит упомянуть связку с фича‑флагами. У нас есть правило: фичу открываем на конкретную страну только после появления переводов на нужный язык. Английский и русский обязательны всегда. Если фича нужна только в России, она открывается только для российских парков. Если нужна на зарубежные рынки, то ждём, пока переводы доедут. Дашборд позволяет отслеживать этот момент в реальном времени.

Экономия рабочего времени

Автоматизация устранила рутину, которая казалась мелкой, но в сумме съедала ресурсы. В среднем разработчики заводили 2,5 задачи на перевод в день, тратя около 2 минут на каждую плюс порядка 15 минут на обновление файлов переводов. Итого: 20 минут ручной работы на команду ежедневно. Не катастрофа, но достаточно, чтобы выбить из рабочего потока. Теперь всё это делает CI. И мы можем гарантировать: если код в мастере сегодня, перевод будет на проде через N дней. Менеджеры планируют маркетинговые запуски и релизы в новых странах с точностью до дня.

Гигиена кода

Автоматизация подсветила ещё одну проблему — неиспользуемые ключи.

Когда вы удаляете React‑компонент, вы удаляете вызов <Message>. ID исчезает из кодовой базы, но в TMS он остаётся. Мы тратили ресурсы на хранение, а порой и за повторный перевод фраз, которые пользователь никогда не увидит. На момент аудита в TMS накопилось около 4–5 тысяч таких ключей‑призраков, почти половина от общего числа. Сейчас у нас есть скрипт, который сравнивает слепок ключей в коде со слепком в TMS, так что их можно постепенно удалять.

Задачи на будущее

Отдельная задача — визуальный контекст. В идеале к каждому ключу нужно прикладывать скриншот интерфейса с разметкой: вот элемент, вот его окружение. Автоматическую разметку скриншотов мы пока только мечтаем внедрить, а сейчас разработчики прикладывают их вручную. В Трекере, куда наш скрипт заводит задачи, есть поле для контекста: где расположен элемент, какие ограничения по длине и скриншот с пометками.

Помимо этого, есть ещё несколько идей на будущее. 

Во‑первых, полностью автоматическое добавление нового языка. Сейчас, чтобы запустить новую локаль, приходится вручную прописывать её в конфигах, добавлять в TMS, заказывать переводы по всем воркспейсам. Процесс не долгий, но ручной, а значит, уязвимый к ошибкам. Хотелось бы свести его к одной команде. 

Во‑вторых, синхронизация переводов с макетами в Figma. Сейчас ключи в TMS и тексты в дизайн‑макетах живут независимо: дизайнер пишет текст в макете, редактор может поправить его на этапе вычитки, и итоговая формулировка в коде может не совпадать с макетом. Мы начали выстраивать процессы с дизайн‑командой, но связь TMS → Figma пока остаётся ручной.

Заключение

Самый наглядный итог проделанной нами работы — цифры, с которых мы начали. Было 3376 ключей без задач на перевод, а стало — 171. Мы сократили долг по локализации почти в 20 раз.

Интернационализация как уборка в доме. Если делать её раз в год перед приездом гостей (запуском новой страны), то можно сойти с ума, разгребая завалы. Если запускать робот‑пылесос каждый день, просто забываешь, что пыль вообще существует. Автоматизация доставки переводов даёт разработчикам возможность сконцентрироваться на инженерных задачах, а пользователям — удобный, понятный интерфейс. 

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

  1. Автоматизируйте пулинг переводов из TMS в код, например, при помощи CI‑скрипта, который регулярно подтягивает свежие переводы.

  2. Автоматизируйте отправку новых ключей в TMS, чтобы не терять их по недосмотру. 

  3. Автоматизируйте создание задач, если TMS не умеет этого из коробки.

  4. Собирайте статистику и визуализируйте её, потому что нельзя управлять тем, чего не видишь. Даже самый простой дашборд поможет быстрее выявлять проблемы и реагировать на них.

Если у вас есть интересный опыт интернализации, то буду рада обсудить его в комментариях.

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