Локализация или: Как я научился не беспокоиться и полюбил babel-plugin-react-intl

Интернационализация и локализация или i18n тупо определяются как процесс, посредством которого в систему вводится поддержка новых языков. IDAGIO - это сервис потоковой передачи классической музыки. Мы хотели обратиться к международной аудитории, поэтому начали с поддержки английского (британский английский; если быть точным, en-GB).

Но вскоре мы захотели иметь возможность обслуживать и более широкую, не говорящую по-английски, аудиторию. Мы находимся в Берлине, Германия, поэтому, естественно, мы выбрали второй язык немецкий. Мы выпустили нашу платформу в locale de-DE в начале 2017 года.

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

В области внешнего интерфейса javascript де-факто решением является FormatJS, созданный командой Yahoo Presentation Team. Он построен на основе ECMA Internationalization API, и вместе взятые они представляют собой удивительную попытку стандартизированного решения этих проблем. Согласно определению Yahoo:

FormatJS - это модульная коллекция библиотек JavaScript для интернационализации, ориентированная на форматирование чисел, дат и строк для отображения людям. Он включает набор основных библиотек, основанных на встроенных Intl JavaScript и отраслевых стандартах i18n, а также плюс набор интеграций для общих библиотек шаблонов и компонентов.

В IDAGIO, когда мы начали создавать наш веб-интерфейс, мы выбрали React. Предлагаемое решение FormatJS в этой экосистеме - библиотека yahoo / react-intl.

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

Теперь, когда вы хотите интернационализировать строку, вы используете компоненты. r eact-intl предоставляет их, например, FormattedMessage, который принимает id и значение по умолчанию как реквизит. Идентификатор хранится в json-файле перевода. Этот файл содержит объект json с парами ключ-значение. Ключ - это идентификатор строки. У вас будет по одному из этих файлов для каждого поддерживаемого вами языка.

Мы взяли все статические строки, которые требовалось локализовать в нашем приложении, и поместили их в файл en-GB.json. После завершения мы изменили наше приложение, чтобы использовать этот файл json. Разбив наш локализуемый контент таким образом, мы получили возможность встраивать любые другие файлы локализации, которые могут нам понадобиться в будущем.

Растущая боль

Для начала, когда нам требовался перевод слова, мы просили его на нашем канале # deutsch Slack. Затем мы обновим файл немецкого перевода, и все. Это продолжалось стабильно, и очень скоро в наших файлах локализации оказалось около 500 строк перевода.

Это был плохой процесс. Отсутствие прав собственности привело к непоследовательности тона и сообщения. Плюс это не будет масштабироваться, если мы добавим дополнительные языки.

Когда мы наняли немецкоязычного менеджера по коммуникациям, мы обнаружили, что это хорошая возможность для формализации этого процесса. Для этого мы ввели в наш рабочий процесс POEditor. Это место, где можно централизовать все наши файлы локализации на всех платформах. В нем пользователи могут свободно изменять переводы, доступны атрибуция и управление версиями, и когда это будет сделано, разработчики могут загрузить соответствующие файлы json.

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

Подождите ... Ключ, который, очевидно, не используется, все еще используется?
- IDAGIO Communications Manager

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

ПЛОХИЕ РАЗРАБОТЧИКИ, ПЛОХО!

Человеку свойственно ошибаться. Мы должны обращаться к великим мыслителям за указаниями:

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

Обмануть меня дважды

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

Это называется линтинг, и все классные ребята этим занимаются. Для нашей кодовой базы мы используем eslint и style-lint с довольно агрессивными настройками и очень этому рады. У нас также есть политика без мертвого кода, которая применяется нашими линтерами.

Понятно, что нам нужно что-то подобное для линтинга файлов локализации. Этот инструмент должен посмотреть на английский json (источник истины) и сравнить его с использованием в системе. Кроме того, если он обнаруживает несоответствия, мы должны быть уведомлены, и процесс линтинга должен завершиться ошибкой. Не пройти мимо; не собирайте зеленый значок. 🚨

Уровень техники: babel-plugin-react-intl

Существует другой подход, который полностью решает эту проблему: yahoo / babel-plugin-react-intl.

Идея этого проекта заключается в том, что вы должны сгенерировать резервный файл локализации (en-GB.json для нас) из ваших исходных файлов; эффективно инвертировать элемент управления обратно в исходные файлы. Это отличная идея, поскольку она избавила бы от необходимости заниматься линтингом.

Но проблема заключалась в том, что, когда мы попробовали это, мы поняли, что нам удалось втиснуть в микс некоторые неподдерживаемые способы использования. Из-за этого инструмент не мог надежно определить, какие строки используются.

Неподдерживаемое использование

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

(fnName) =>
  <FormattedMessage
    id={`profile.functions.${fnName}`}
    defaultMessage={capitalize(fnName)}/>

Наш файл en-GB содержит идентификаторы всех функций профиля:

"profile.functions.composer": "composer",
"profile.functions.soloist": "soloist",
"profile.functions.conductor": "conductor",
...

Он делает две вещи:

  1. совпадает с идентификатором, , например, profile.functions.soloist, и, если он найден, использует его
  2. если он не найден, будет использоваться заглавная версия значения, полученного из API (Soloist)

Почему такое использование вызывает проблемы? Когда статический анализатор не может вычислить это значение по умолчанию, он не может соотнести один перевод (или группу переводов) с точкой использования.

При наличии profile.functions. $ {FnName} статический анализатор не может узнать, что fnName будет допустимым значением (например, solist) . soloist исходит из API, а babel-plugin-react-intl не знает об API и о том, что он может вернуть.

Поскольку он не знает значений по умолчанию, он не может создать файл локали по умолчанию (en-GB.json).

Если бы вы запустили плагин, вы бы получили такую ​​ошибку:

Messages must be statically evaluate-able for extraction

Как избежать неподдерживаемого использования

С некоторыми изменениями мы можем изменить код, чтобы избежать ошибки. Сначала нам нужно определить ключи внутри defineMessages. Это действует как ловушка для системы, при которой неоднозначность все еще существует, но сосредоточена в содержимом этой функции.

const profileFunctions = defineMessages({
  composer: {
    id: 'profile.functions.composer',
    defaultMessage: 'Composer',
  },
  soloist: {
    id: 'profile.functions.soloist',
    defaultMessage: 'Soloist',
  },
...
});

И измените функцию рендеринга на использование intl.formatMessage

(fnName) => profileFunctions[fnName]
  ? this.props.intl.formatMessage(profileFunctions[fnName])
  : capitalize(fnName)

Когда API возвращает fnName для профиля (например, солиста), это только перекрестная ссылка на объявление defineMessages. Это эффективно ограничивает двусмысленность набором предварительно определенных fnNames, определенных в объекте.

Определив идентификатор и значение по умолчанию в абсолютном выражении, мы сделали их статически анализируемыми.

Отсутствие вызовов функций для определения id, ранее…

`profile.functions.${fnName}`

… Теперь просто перечисление возможных значений, например:

  • profile.functions.composer
  • profile.functions.soloist
  • и т.п.

То же самое и с defaultValues; вместо вызова функции мы просто определяем их явно.

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

С помощью этих модификаций мы сделали код статически анализируемым, а babel-plugin-react-intl - счастливым. Привет, зеленый значок! ПРОЙДИТЕ ИДИТЕ. ✅

Если бы у нас еще не было этих способов использования, мы могли бы полностью отказаться от линтинга и просто использовать плагин babel для генерации файла json. Если бы мы начали все сначала, мы бы изо всех сил старались избегать этих обычаев и использовали плагин babel, но пока мы должны извлечь максимум из этой несовершенной ситуации.

Работа с несовершенной ситуацией

В то время у нас не было ресурсов, чтобы исправить неподдерживаемое использование. Мы застряли в прежнем режиме, обновляя как исходные файлы, так и файлы переводов вручную. Поэтому вполне вероятно, что мы снова совершим ошибки. Мы хотели идти на компромисс, чтобы найти способ оставаться на прямом и узком пути, пока мы не сможем перейти на babel-plugin-react-intl.

Мы собрали простой скрипт, чтобы сделать 3 вещи:

  1. Используйте plugin-react-intl, чтобы определить все однозначные использования
  2. Выполните поиск с точным соответствием в исходных файлах на предмет неоднозначных
  3. Проверьте список игнорируемых использований (проблемы, о которых мы знаем)

Решение «по максимуму» для реального мира.

Затем мы подключили это к нашему стандартному скрипту линтинга, что означало, что мы получили его как значок на нашем CI внутри github. Вы можете найти сценарий в этой сути.

Опять же ... у вас не должно быть этой проблемы, но если вы это сделаете, не стесняйтесь использовать наш скрипт, чтобы максимально использовать его.

Уроки выучены

  1. RTFM. Для меня это большое. Я все еще бросаюсь в разработку, не понимая проблемного пространства.
  2. Коды выхода: 0 - хорошо, 1 - плохо, приятно освежиться.
  3. Технический долг закрадывается туда, где вы меньше всего этого ожидаете.
  4. Стоит потратить время на то, чтобы найти правильный процесс, который включает в себя как людей, так и инструменты.