Предыстория

Среди различных задач, с которыми я столкнулся в течение рабочего дня, больше всего я боролся с локализацией. Не потому, что это сложная задача, требующая дополнительных навыков, а потому, что она заставляет вас выполнять ручную работу без автоматизации. В то время не было инструмента, который я мог бы использовать для упрощения процесса локализации, и мне казалось, что я зря трачу время на монотонные и повторяющиеся ручные действия. Чтобы понять, почему локализация далась мне нелегко, давайте кратко рассмотрим проект, над которым я работаю, и наш подход к работе с интернационализацией (i18n) в нашей команде.

Есть статья (на французском языке), написанная hugo mercier, в которой дается некоторый обзор структуры проекта. Я бы рекомендовал прочитать его, если вы хотите иметь полное представление о том, с чем мы работаем. Здесь я дам краткое описание проекта с точки зрения локализации.

Итак, есть приложение Angular для 4 стран, каждая дополнительно разделена на десктопные и мобильные проекты, всего получается 8 проектов. Мы используем базовый подход для интернализации с хранением локализации в файлах xlf (файл в формате xml), это означает, что один файл translation.xlf относится к одному проекту. Использование отдельных файлов translation.xlf для настольных и мобильных проектов для одной страны, поскольку обычно текст, подходящий для настольных компьютеров, может быть трудно отобразить на мобильных устройствах.

Одна из самых частых задач при локализации - добавление новых ключей. Предположим, есть заголовок страницы:

@@ PAGE_TITLE определяет ключ, который будет искать в соответствующем файле translation.xlf в процессе сборки. Добавление локализации в нашем случае означает открытие всех файлов перевода [locale] .xlf (их 8) и добавление следующего кода с локализацией, указанной в целевом теге:

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

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

  • переименование ключей (@@ page_title становится @@ component_title)
  • удаление ненужных ключей локализации в html-шаблонах и файлах локализации
  • повторное использование существующих переводов

В целом, разработчик несет ответственность за то, чтобы:

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

Упомянутые выше действия требуют интенсивного использования текстового поиска в IDE, что замедляет темпы разработки при большом количестве файлов. В общем, для выполнения этих простых задач требуется больше времени, снижается продуктивность и внимание, что приводит к ошибкам, которые, мы надеемся, будут обнаружены QA. Возврат задач в разработку - неприятный момент для разработчика.

Поскольку мы используем Visual Studio Code в качестве нашей IDE, следующим логическим шагом был поиск подходящего расширения для упрощения работы по локализации в Angular. Почти все проблемы, с которыми я сталкивался, были ранее решены, и все, что мне нужно было сделать, это найти и использовать уже существующие решения или инструменты. Однако в случае с i18n мне не повезло. Я искал различные расширения для VS Code, но не нашел ничего, что могло бы упростить мои проблемы с локализацией. В этот момент я понял, что «если нет подходящего расширения - мне нужно его написать».

Знакомство с VS Code Extensions

Я начал изучать расширения VS Code не из документации, а из уже существующего расширения, написанного одним из наших разработчиков Билелем Мсекни. После некоторого изучения кода и отладки я получил некоторые знания и начал понимать, как работают расширения. После этого я начал читать документацию. VS Code предоставляет комплексный и мощный API для добавления различных функций, необходимых для вашей работы.

Вначале меня интересовала реализация двух функций, которые улучшили бы мою продуктивность при работе над локализацией:

  • проверка атрибутов i18n в html-файлах и отображение предупреждений, если в любых связанных файлах локализации отсутствуют переводы
  • иметь быстрый обзор существующих локализаций для данного ключа, не открывая файлы xlf

Чтобы иметь возможность проверять файлы, необходимо иметь информацию обо всех доступных ключах и их переводах (файлы translation.xml), а также о том, где они используются (файлы HTML). Первое, что приходит в голову, это парсинг файлов с помощью регулярных выражений. Учитывая размер приложения и потенциальное количество файлов, которые необходимо проанализировать (это сотни файлов), сразу становится ясно, что это ресурсоемкий процесс, который легко приостановит пользовательский интерфейс VS Code, если он выполняется в том же потоке. . Немного почитав документацию, я нашел то, что мне нужно - Расширение языка. Важнейшей особенностью этого типа расширения является архитектура клиент-сервер, в которой сервер является отдельным процессом Node, который можно использовать для тяжелых задач. Кроме того, Language Extension предоставляет базовые операции, которые чрезвычайно важны: наведение курсора мыши, IntelliSense, Найти все ссылки, Перейти к определению и т. Д. Все эти функции доступны по умолчанию; вам просто нужно подключить их к своей реализации.

Чтобы понять, как работает подход клиент-сервер, я исследовал lsp-sample. Первое, что меня интересовало, это понимание того, как клиент подключается к серверу. Я нашел ответ в файле extension.ts:

Пример очень хорошо прокомментирован, нет смысла перебирать каждую строчку кода. Единственное, что следует упомянуть, это то, что клиент используется для связи с сервером. Сервер находится в файле server.js, в котором создается соединение с клиентом:

В данном коде объект соединение используется для обмена сообщениями с клиентом. Функция onInitialize вызывается перед инициализацией соединения и может использоваться для дополнительной настройки при запуске.

Стоит отметить, что API предоставляет диспетчер документов (объект документы). Он содержит различные методы для управления открытыми файлами в рабочей области VS Code:

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

Создание расширения

Отправной точкой при написании расширения было более глубокое изучение файла Angular.json и того, как он может помочь в моей задаче. Давайте посмотрим на него поближе (на самом деле файл содержит гораздо больше конфигурации, но я оставил только самые важные):

Как показано, все проекты, определенные в нашем приложении, перечислены в ключе projects:

  • sourceRoot - исходный каталог; оказался полезным, так как позволяет искать нужные файлы в файловой системе
  • tsConfig - файл с настройками приложения. Помимо других настроек, он позволяет вам писать шаблоны для исключения каталогов и файлов из процесса сборки. Они действительно важны для расширений и могут использоваться для определения того, каким проектам принадлежит конкретный файл template.html. Используя эту информацию, можно установить связь "один ко многим" между template.html ‹-› translation.xlf
  • i18nFile - файл локализации для конкретного проекта.

Вот пример файла tsConfig:

Идея exclude такая же, как и в случае с файлом gitignore: все файлы или каталоги, соответствующие хотя бы одному шаблону, исключаются из проекта на этапе сборки.

Все проекты находятся в едином репозитории и имеют один и тот же код; каждый проект создается независимо и включает только связанные с ним файлы. Это сделано для минимизации размеров пакетов. По умолчанию все файлы в репозитории связаны с каждым проектом. Чтобы определить, какие из них следует опустить во время сборки, используются шаблоны tsConfig и exclude. Путь к каждому файлу проверяется на соответствие шаблонам исключения. Если совпадений нет, то файл включается в комплект.

Вот почему tsConfig так важен и интенсивно используется в моем расширении. Это помогает установить связь между файлом html и его локализацией.

Поскольку файл Angular.json хранит данные в формате JSON, его легко читать и работать как с обычным объектом JSON в JavaScript.

Основная функция, которой мне не хватало при работе с локализацией, - это проверка шаблона на соответствие подходящим переводам. Для этого файл template.html (тот, с которым вы работаете и который открыт в VS Code) должен быть связан с одним или несколькими файлами translation.xlf. . Имея такую ​​информацию, можно проверить, содержит ли файл перевода все ключи, объявленные в шаблоне. Вот небольшая схема поиска файлов локализации:

Собрав нужную мне информацию - я приступил непосредственно к программированию. Когда клиент запускается, нужно выполнить три основных шага:

  1. найти и проанализировать файл angular.json;
  2. найти и отправить информацию о файлах локализации на сервер;
  3. найти пути ко всем html файлам проекта и отправить их на сервер:

Следует отметить, что локализация и парсинг html файлов происходит на стороне сервера, а клиент отвечает только за их поиск. Это связано с тем, что сервер имеет ограниченный API для работы с рабочим пространством и намного проще искать файлы на стороне клиента и отправлять найденные пути к файлам на сервер для дальнейшей обработки. Уведомления на сервер отправляются путем вызова метода client.sendNotification.

Написав базовые действия клиента, я переключил фокус на сервер, и первое, что нужно было сделать, это уловить события от клиента:

Итак, имея всю необходимую информацию на сервере, остается только проанализировать файлы html и xlf. Что я сделал с RegExp. Анализ HTML-шаблонов подразумевает поиск атрибутов, начинающихся с префикса i18n, его значение - это ключ локализации, который я искал. Анализ файлов локализации означает поиск всех тегов ‹trans-unit /›, а также извлечение ключей, которые будут использоваться в качестве идентификатора для поиска переводов, например:

Суть синтаксического анализа состоит в том, чтобы найти в шаблоне @@ page_header и соответствующую ему локализацию. Ключ page_header используется как идентификатор для присоединения; ну прям как в SQL!

Когда файлы проанализированы, самое время перейти непосредственно к проверке шаблонов html. Будут проверяться только файлы, открытые в среде IDE, что можно сделать с помощью диспетчера документов:

Событие onDidChangeContent запускается в двух случаях: при открытии или редактировании файла; как раз то, что мне было нужно.

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

Несмотря на то, что код выглядит запутанным, я старался сохранить его как можно более чистым. Объект Диагностика содержит информацию (начальный и конечный индексы), в которой должно отображаться предупреждение, а также текст для отображения. С помощью this.connection.sendDiagnostics собранная информация отправляется в среду IDE для отображения предупреждений:

Если локализации нет - атрибут i18n подчеркнут зеленым цветом. При наведении курсора мыши всплывает всплывающая подсказка с информацией о локализации:

Начало неплохое, но этого недостаточно, чтобы упростить подход к управлению локализацией; кроме информации об отсутствии перевода, необходимо знать, в каком файле translation.xlf его нет. Вопрос в том, как отображать переводы; В итоге я выбрал самый простой способ: при наведении курсора мыши на атрибут i18n должны отображаться доступные переводы или указание на их отсутствие. Как я уже упоминал, VS Code предоставляет мощный API:

Все, что вам нужно сделать, это подписаться на событие registerHoverProvider объекта languages ​​. Оба являются частью API. Это событие запускается, когда пользователь наводит курсор на любое слово. Обработчику передается не только активный документ, но и позиция выделенного слова (позиция - это индекс в строке). Наличие активного слова дает нам возможность проверить, является ли это атрибутом i18n. Если да, ищем возможные переводы, создаем ответ и отправляем его в IDE. Вкратце это выглядит так:

В IDE результат выглядит многообещающим - всплывающее окно содержит актуальные значения локализации:

После такого результата я был настолько воодушевлен, что добавил в расширение кучу функций:

  • Go To конкретный файл перевода из всплывающего окна.
  • Go To Definition из шаблона html в соответствующий файл перевода
  • Find All References для отображения использования единиц перевода в html шаблонах
  • IntelliSense
  • Rename применяет изменения как для html шаблона, так и для файла перевода
  • Generate translation unit(-s) для единичных или групповых единиц перевода, генерирующих
  • Команда для removing translations и все ссылки на нее

Пример работы этих функций можно посмотреть по этой ссылке.

Я сделал немного больше, чем ожидал с самого начала. Изначально для меня было важно сократить ручные действия при работе с локализацией. В общем, мне понравился не только процесс разработки, но и конечный результат. Я потерял ужас перед задачами локализации, которые сейчас вызывают у меня живой интерес, так как это дополнительная возможность протестировать расширение еще раз :)

p.s. В качестве неожиданного побочного эффекта написания этого расширения VS Code я решил написать еще одно для Google Chrome, чтобы избавить меня от других задач, с которыми я борюсь каждый день (да, к сожалению, у нас их достаточно).

Исходный код можно найти здесь, а расширение на торговой площадке - здесь.