Интернационализация: как сделать веб доступным для всех
Ecma International, Technical Committee 39 или по-простому TC39 — это группа JavaScript-разработчиков, создателей реализаций технологий, академиков и других заинтересованных сторон, которые вместе с сообществом поддерживают и развивают JavaScript как платформу.
Участники TC39 обычно рассказывают что-то интересное, пользуясь своим глубоким пониманием JavaScript. Но кое-кому кажется, что они слишком далеко ушли от проблем простых разработчиков. Где разработчик языка, и где человек, который каждый день на практике пишет фронтенды?
Давайте познакомимся с докладом, который сочетает и глубину понимания, и высокую практическую применимость. Встречайте новый рассказ Romulo Cintra о проблемах интернационализации, которые будут решены новым API, которое вскоре появится в JavaScript.
Romulo Cintra — делегат TC39, работает в разработке и архитектуре уже более 10 лет, специализируется на вебе, мобильной разработке и облаках. В этом докладе из первых рук сопредседателя MessageFormat Working Group вы узнаете, какие варианты решения существующих проблем есть уже сейчас, и в каком виде их собираются решать посредством нового API в самом JavaScript.
Под катом полная текстовая расшифровка доклада Romulo и ссылка на видео. Если вы любите читать — в этой статье есть все, вы ничего не пропустите. Если же у вас есть время запустить видеозапись, то вас ждет около часа хорошей видеозаписи с интересными слайдами и понятным английским языком.
Далее повествование от лица спикера.
О состоянии интернационализации и локализации на сегодняшний день нужно знать три вещи: всё очень, очень и очень плохо. Меня зовут Romulo Cintra, и я занимаюсь архитектурой приложений в области финансов. Я много общаюсь с людьми из TC39, и вижу, как они пытаются сделать мир JavaScript лучше. Помимо этого, я убежденный сторонник опенсорса, а в свободное время — преподаватель в Школе Технологий в Барселоне.
Вопрос интернационализации очень важен. Так получилось, что на нашей Земле существует множество различных народов и языков. В мире сегодня около 195 стран и 6 тысяч языков. Это делает нашу задачу крайне непростой. Подумайте ещё вот о чем: я пишу статьи и читаю доклады на английском, а не на русском; у нас с вами уже есть проблема интернационализации. Если кто-то не говорит по-английски, он будет исключен из нашего с вами разговора. Чтобы этого не происходило, была придумана интернационализация.
По-английски internationalization сокращают как i18n. Цифра 18 — это число символов между буквами i и n в этом слове. Коротко говоря, интернационализация — это проектирование софта с целью максимального упрощения локализации. Благодаря интернационализации софт может поддерживать местные настройки, язык, валюту и так далее. Интернационализация делает веб более доступным для всех. Здесь можно провести параллель с разработкой через тестирование (test-driven development): там сначала пишется тест, а затем код; с интернационализацией необходимо действовать так же. Обычно люди думают об интернационализации уже после того, как код написан, но это неправильно.
По аналогии с i18n, l10n — сокращение localization, и 10 — это число символов между первым l и последним n. Локализация — это адаптация продукта к языку и культурной среде, в которой он распространяется. То есть нужно не просто перевести «Hello» на «Привет», но и использовать местную валюту, десятичный разделитель и так далее, то есть сделать софт более привычным для пользователя. Это больше, чем просто перевод.
Сколько языков поддерживают ваши веб-проекты? У многих больше двух. Есть ли кто-то, у кого больше пяти? А 15? У нас поддерживается около 25 языков. У нас не очень хорошая поддержка, потому что интернационализация у нас налажена не самым лучшим образом. По ходу доклада я буду объяснять, как улучшить интернационализацию, и расскажу о мерах, которые мы предпринимаем.
Итак, повторю ещё раз: интернационализация означает упрощение локализации, обеспечение поддержки для неё на уровне архитектуры. А локализация — это приспособление софта к местным реалиям. Перевод очень часто не соответствует оригиналу — возьмем пример из киноиндустрии, где название фильма «Pain and Gain» было переведено как «Кровью и Потом: Анаболики».
Или другой пример: реклама русской бани, на которой в английском переводе написано «Russian crematorium» (русский крематорий).
Сомневаюсь, что это привлечет клиентов, по крайней мере живых. Интернационализация и локализация так важны именно потому, что они позволяют донести до пользователя ровно то, что мы хотим сказать. В сущности, интернационализация — это предоставление специальных возможностей, потому что если вы не можете понять софт, с которым работаете, то фактически ваши возможности ограничены. Всем выгодно, чтобы софт был более доступным, потому что это обеспечивает более широкий круг пользователей, а значит, более высокий доход; кроме того, это делает веб лучше.
MessageFormat
Рассмотрим примеры кода:
'es-ES': {
HELLO_WORLD: '¡Hola mundo!'
},
'en-GB': {
HELLO_WORLD: 'Hello world!'
Нам нужно перевести наши строковые объекты. У есть переменная `HELLO_WORLD
с соответствующими строками на каждом языке. Для такого перевода во многих языках (например, в Java) существует формат MessageFormat. Попробуем разобраться, что это такое. Сначала немного о некоторых базовых технологиях — начнем с Unicode. Это стандарт, который создает единое пространство для символов из разных языков. Проведем аналогию с шахматами: каждый тип фигур может иметь разную форму, но мы всегда знаем, где именно на доске они должны находиться. Ну и, конечно, есть разные форматы Unicode с разным количеством байт: UTF-8, UTF-16 и UTF-32. Сейчас наиболее часто в теге meta используется именно UTF-8. С Unicode браузер может отображать специальные символы, если же этот тег `meta
забыть, никто не поймет, что за символы у нас на странице.
Помимо Unicode две другие важные технологии — CLDR и ICU. CLDR — это своего рода база данных алфавитов, стран, валют, часовых поясов и т. п., хранящихся в разных языках мира. В ней присутствуют далеко не все 6 тысяч языков мира, над этой базой данных ещё ведется работа. Последнее обновление было в прошлом октябре. Другой важный проект — ICU. Это огромная база данных слов, цифр и символов из разных языков, которые предоставляются в виде методов для сортировки, нормализации, форматирования и т. п. Эти библиотеки используются во множестве языков программирования. В JavaScript ICU лежит в основе Intl API. Но в языках мира столько разнообразного материала, который необходимо отображать в браузерах, что работа по включению их в эти стандарты далека от завершения.
MessageFormat — это формат, позволяющий связать определенный ключ с определенным сообщением в определенном языке. В некоторых случаях в MessageFormat можно передавать переменные, он их определяет и вводит в итоговую строку. Та же проблема была решена несколько другим способом в других языках. В Android MessageFormat реализован на Java. Там для работы с ним форматом не нужна специальная библиотека, Android умеет с ним взаимодействовать сам. В iOS существует API, очень похожий на тот, который есть в JavaScript. Он встроен в систему, там тоже ничего не нужно скачивать, просто передаете необходимую строку в метод этого API.
Как была эта проблема решена в JavaScript? Пока никак. Но у нас есть много библиотек, которые предлагают вариант решения.
Здесь отображено количество скачиваний наиболее популярных из них (и двух менее популярных, fluent от Mozilla и fbt от Facebook). Каждую неделю происходит почти два миллиона скачиваний, так что потребность в библиотеках для интернационализации есть.
Библиотеки
Вкратце познакомимся с некоторыми из этих библиотек, и начнем с i18next. Её разработчики внесли много изменений в MessageFormat, и она не целиком следует ICU. Тем не менее, это очень хорошая библиотека. Её реализация МessageFormat имеет множество преимуществ, например, возможность использовать интерполяцию строк (которая отсутствует в формате ICU). Однако есть и недостатки, например, сообщения в форме множественного числа нельзя поместить в одну строку с единственным, как это можно сделать в ICU.
Одна из наиболее известных библиотек интернационализации — intl-messageformat. Каждую неделю её скачивают более 700 тысяч раз. Её поддержкой занимается мой коллега, Лонг Ху (Long Ho). Её популярность объясняется тем, что поверх неё создан react-intl. Так что если вы пользуетесь React, то, скорее всего, у вас есть и эта библиотека. Её разработчик также участвует в ECMA-402, и поэтому он старается соблюдать стандарт ICU.
var MESSAGES = {
'en-US': {
NUM_PHOTOS: 'You have {numPhotos, plural, ' +
'=0 {no photos.}' +
'=1 {one photo.}' +
'other {# photos.}}'
},
'es-MX': {
NUM_PHOTOS: 'Usted {numPhotos, plural, ' +
'=0 {no tiene fotos.}' +
'=1 {tiene una foto.}'
+'other {tiene # fotos.}}'
}
};
Её реализация очень похожа на MessageFormat. Здесь можно передавать переменные и указывать необходимость множественного числа.
Прежде чем перейти к примерам кода, я расскажу о ещё двух новых библиотеках, которые сейчас входят в моду, они созданы Facebook и Mozilla.
Целиком весь API не показать, но поверьте мне на слово: разработчики этих библиотек постарались на славу, здесь есть ровно то, чего нам сейчас не хватает. Правда, Facebook сделал её в своем стиле: своя разметка, способность запускаться во время компоновки, извлечение хэш-мапов из строк, которые можно перевести автоматически. Проблема в том, что всё это ориентировано на масштабы, с которыми среднестатистический программист работает редко. Проект совсем молодой, и его хотят интегрировать с другими известными библиотеками, например, с React. В будущем он, скорее всего, будет набирать популярность.
Всё перечисленное — это библиотеки, которые нужно дополнительно скачивать, они не встроены в браузер. С одним только браузером мы далеко не уедем, так что с локализацией у нас всё плохо. Изменить такое положение дел нам может помочь MessageFormat. Пока мы не можем им пользоваться, но поверьте мне: будущее именно за ним. Сейчас мы ведем над ним активную работу, устанавливаем заинтересованные стороны, ищем свежие идеи для нового MessageFormat. В своем первоначальном варианте этот формат уже устарел, потребности разработчиков существенно эволюционировали со времени его создания. Новый формат должен быть сделан качественно и быть простым в использовании.
Intl.DateTimeFormat
В браузерах уже есть множество встроенных механизмов для интернационализации и локализации, просто большинство о них не знает и не пользуется ими. Слышали ли вы об Intl.DateTimeFormat? В этом проекте мы постоянно создаем новые API. С большой вероятностью сейчас уже нет необходимости в Moment.js, Day.js, date-fns.
const myDate = new Date();
new Intl.DateTimeFormat('ru', { timeStyle : 'short'}).format(myDate);
// short → 19:49
// medium → 19:49:17
// long → 19:49:17 GMT+2
// full → 19:49:17 Центральная Европа, летнее время
Существует timeStyle, он был создан несколько месяцев тому назад и он позволяет форматировать дату и время, не обращаясь при этом к Moment.js. Кроме того, есть метод formatRange. Любая задача, связанная с выбором диапазона дат (как, например, на сайтах с функцией бронирования), всегда непростая. Но метод для этого уже существует в браузере. И, что самое важное, этот метод поддерживает интернационализацию, при этом избавляя от необходимости загружать дополнительные библиотеки.
Intl.RelativeTimeFormat
Я работал над документацией для второй части этого проекта, и если вы тоже хотите поучаствовать — нам нужна помощь с переводом на русский язык и с соответствием стандартам. RelativeTimeFormat необходим, когда нужно сделать обратный отсчет времени.
const myTime = new Intl.RelativeTimeFormat('ru', { style: 'narrow' });
myTime.format(2 , 'quarter');
//Style Narrow : +2 кв. → in 2 qtrs. → dentro de 2 trim.
//Style Long : через 2 квартала → in 2 quarters → dentro de 2 trimestres
Сейчас это сделать довольно просто, можно указать время через два дня, две недели, через квартал и т. п. Раньше такое форматирование в вебе не существовало.
const myTime = new Intl.RelativeTimeFormat('ru', { style: 'narrow' });
myTime.format(2 , 'day');
//Style Narrow : +2 дн. → in 2 days → dentro de 2 días
//Style Long : через 2 дня → dentro de 2 días myTime.format(-1 , 'day');
//Style Narrow : -1 дн. → 1 day ago → hace 1 día
//Style Long : 1 день назад → 1 day ago → hace 1 día //Numeric(auto) : вчера → yesterday → ayer
Вот пример на русском языке. Вы и сами можете проверить работу этого кода, потому что он уже есть в вашем браузере.
const myTime = new Intl.RelativeTimeFormat('ru', { style: 'narrow' });
myTime.format(20 , 'seconds');
//Style Narrow : +20 с → in 20 sec. → dentro de 20 s
//Style Long : через 20 секунд → in 20 seconds → dentro de 20 segundos
Этот метод очень полезный, он может давать время в коротком формате, который вы видите выше. Подчеркиваю, для всего этого не нужно использовать никаких сторонних библиотек.
Intl.NumberFormat
Следующее, чем я хотел поделиться — Intl.NumberFormat. Я буду говорить о третьей стадии, но на примерах представлена только вторая, потому что некоторые изменения все еще обсуждаюся. Intl.NumberFormat работает с единицами измерения и формами записи. Стоит обратить внимание на то, что он делает с единицами измерения: он позволяет работать с различными стилями.
new Intl.NumberFormat("ru", {
style: "unit",
unit: "liter", unitDisplay: "long"
}).format(16);
// → 16 литров → 16 liters → 16 litros
Все единицы измерения взяты из UTC 35, и там их очень много. В общей сложности тут представлено около 140 единиц для форматирования. Так что сейчас обеспечить интернационализацию проще, чем когда-либо. Нужно просто перевести свои строки, а вся необходимая динамика уже содержится в браузере.
const nbr = 987654321;
new Intl.NumberFormat('ru', { notation: 'scientific' }).format(nbr);
// → 9,877E8 → 9.877E8 (en-US)
new Intl.NumberFormat('ru', { notation: 'engineering' }).format(nbr);
// → 987,654E6 → 987.654E6 (en-US)
new Intl.NumberFormat('ru', { notation: 'compact' }).format(nbr);
// → 988 млн → 988M (en-US) → 9.9亿 (zn-CN)
new Intl.NumberFormat('ru', { notation: 'compact', compactDisplay: 'long' }).format(nbr);
// → 988 миллионов → 988 millions (fr)
Теперь что касается форм записи. Если честно, я не слишком часто ими пользуюсь, поскольку не пользуюсь формой записи с экспонентой (научной записью), и у меня нет необходимости представлять крупные цифры. Но если вам это нужно, то специально для вас есть соответствующий API.
Intl.ListFormat
Ещё один полезный API — Intl.ListFormat, он уже на третьей стадии и позволяет форматировать списки двумя различными способами. Предположим, мне нужно сказать фразу «Я собираюсь на HolyJS». Мы можем сделать список, включающий строки «Moscow» и «St. Petersburg», указать параметр «conjunction», и строки будут объединены союзом русского языка «и». Это совершенно новая функция, и очень полезная.
Если же указать «disjunction», то мы получим союз «или».
Наконец, функция может автоматически определить используемый язык и алфавит и отсортировать элементы списка соответствующим образом.
Intl.PluralRules
Другой важный API — Intl.PluralRules. Этот API из всех самый старый, но им почему-то никто не пользуется.
Когда я вижу списки финалистов в гонках или в футболе, там рядом с именами всегда указаны цифры:»1»,»2»,»3» и т. д. Но это ведь не соответствует тому, как мы говорим, было бы значительно ближе к речи написать »1й»,»2й»,»3й». И для этого есть специальные API, воспользоваться которыми не так уж и сложно.
Например, мы можем написать фразы »1 cat»,»0 cats»,»0.5 cats»,»1.5 cats», и API автоматически выберет правильное окончание множественного числа.
Intl.DisplayNames
Это один из наиболее популярных API, ведь нам очень часто приходится отображать списки стран. Предположим, что у нас есть список стран — например, в базе данных или в JSON. Тогда при каждом переключении языка нам нужно загружать отдельный JSON с новым списком стран, валют, и так далее. Этих JSON становится излишне много, и чем это заканчивается? Мы создаем микросервис, в который встроена база данных с различными языками, и вытягиваем все данные из него. Конечно, в примере со списком стран нам повезло и обновлять данные нужно нечасто —, но так ведь будет не всегда, правда? Мы не можем решить все проблемы сразу, но DisplayNames решает часть из них. У вас есть вот API как в примере ниже, и вы можете сделать запрос только на список валют или только на список стран:
const currencyNames = new Intl.DisplayNames(['en'], {type: 'currency'}); currencyNames.of('USD'); // "US Dollar"
currencyNames.of('EUR'); // "Euro"
currencyNames.of('TWD'); // "New Taiwan Dollar"
currencyNames.of('CNY'); // "Chinese Yuan"
const languageNames = new Intl.DisplayNames(['en'], {type: 'language'}); languageNames.of('fr'); // "French"
languageNames.of('de'); // "German"
languageNames.of('fr-CA'); // "Canadian French"
languageNames.of('zh-Hant'); // "Traditional Chinese"
Это очень полезная вещь. Она работает не только со странами и валютами: точно так же можно работать с месяцами, днями недели и многим другим, что необходимо вам как разработчику.
Итоги и планы на будущее
До сих пор мы говорили об уже существующих API. Давайте перейдем к нашим планам на будущее. Мой родной язык — португальский. Так что на моих сайтах мне нужно поддерживать как минимум португальский и английский. А поскольку мы находимся очень близко к Испании, то испанский тоже пригодится. Португалия — очень маленькая страна, и Франция тоже не так уж и далеко, так что было бы неплохо добавить к этому списку французский.
Для нас MessageFormat очень актуален, и он появится уже скоро. Существуют библиотеки, и есть разработчики, которые работают над ними. Все эти разработчики работают над связанными проблемами. Большинство создателей наиболее популярных библиотек и большинство крупных компаний (Netflix, Amazon, Facebook) согласны по меньшей мере в одном: сейчас есть острая потребность в интернационализации. Об этом же говорят два миллиона скачиваний в неделю. Так что сейчас мы можем себе позволить написать MessageFormat заново, и сделать это качественно.
Кто выиграет от правильной интернационализации? Весь веб: все компании, все проекты, все библиотеки. Библиотеки наподобие Intl.MessageFormat никуда не исчезнут, но станут работать по-новому. Не нужно будет загружать данные, поскольку все данные уже будут в браузере. Скорее всего, вам не понадобится переходить на новую библиотеку. Некоторые из этих библиотек уже функционируют как полифиллы для некоторых реализаций. Некоторые реализации, которые я упоминал, находятся на третьей стадии и не реализованы во всех браузерах. Но библиотеки вроде Intl.MessageFormat предоставляют полизаполнения для этой функциональности. В общем, грядет новая глава в истории веба — настоящая революция. Веб станет доступным и понятным для всех. Это крайне важно.
Я считаю, что очень важно обеспечить уникальность нашего проекта. Если существует один формат, который можно использовать и на C++, и на Java, и на JavaScript, то почему бы не использовать этот формат везде? Когда мы пишем веб-страницы, нам часто нужно создавать их мобильные версии, и в этом случае приходится много работы выполнять дважды. Если бы у нас был один формат для всего, то мы могли бы просто использовать уже имеющиеся ресурсы и API. Нам необходим новый уровень интеграции с инструментами. Интернационализация обеспечивается не только трудом непосредственно занятых ей разработчиков. Для неё крайне важна модульность, ведь зачастую бывает удобно использовать свои собственные средства форматирования, свой код. Поэтому не следует закрывать API, они должны быть открыты, чтобы к ним можно было подключить то, чего требует ситуация. Другой важный момент: эти API должны быть нативными. CLDR предоставляют данные, которые нужны API интернационализации. Если вы работаете под Windows или MacOS, то вы уже загружаете данные из CLDR. CLDR — уникальный репозиторий, его функцию никто не дублирует. Значит, данные можно загрузить всего один раз и сделать их общими для всей операционной системы. Если все данные для API Intl уже загружены в операционной системе, то почему бы не предоставить их для всего софта на этой системе?
Наш опыт научил нас помнить, что мы не одни, что над интернационализацией трудятся не только программисты. Мы с вами разработчики, а не переводчики. Предположим, нам необходимо сделать перевод строк в нашем интерфейсе, мы отправляем их компании, занимающейся переводами. Но у переводчиков часто нет никакого контекста для этих строк. Это в т. ч. отсутствует и в MessageFormat. Иногда это приводит к ошибкам, как мы уже видели в упомянутом примере с русским крематорием.
Наконец, я считаю, что API для интернационализации должны быть просты в использовании, и каждый должен иметь возможность сделать интернационализацию — это не должно занимать слишком много времени и сил. При написании кода на интернационализацию нужно ориентироваться с самого начала. Ведь с TDD вначале пишут тест, а потом код; давайте по этому принципу будем начинать наши веб-проекты с правильной интернационализации и локализации. Это позволит нам создавать сайты, которые будут удобны и доступны для всех.
На следующей конференции HolyJS 2020 Piter Сергей Фетискин выступит с докладом «Speak my language %app%». Если эта статья была более теоретическая и рассказывала о будущем, то доклад Сергея будет очень практическим и расскажет о вещах, актуальных прямо сейчас: вся боль и ужас перевода на практике и что с этим можно сделать. Конференция HolyJS 2020 Piter пройдет 10–11 апреля, почти полностью готовую программу уже можно увидеть на сайте и там же можно приобрести билеты.