API для інтернаціоналізації JavaScript: реалізація в Firefox

Що таке інтернаціоналізація?

Інтернаціоналізація (internationalization, а для стислості — i18n, тобто i, ще 18 літер і n; по-російськи це вийде и17я) — такий спосіб створення додатків, при якому їх можна легко адаптувати для різних аудиторій, що говорять на різних мовах. Дуже легко помилитися, припускаючи, що всі ваші користувачі походять з однієї місцевості і користуються однією мовою — особливо, якщо ви навіть не замислюєтеся про те, що передбачаєте саме це.

function formatDate(d)
{
// Всі ж пишуть дату, як місяць/день/рік. Адже Правда?
var month = d.getMonth() + 1;
var date = d.getDate();
var year = d.getFullYear();
return month + "/" + date + "/" + year;
}

function formatMoney(amount)
{
// Всі гроші - це долари, з двома знаками після коми. Адже так?
return "$" + amount.toFixed(2);
}

function sortNames(names)
{
function sortAlphabetically(a, b)
{
var left = a.toLowerCase(), right = b.toLowerCase();
if (left > right)
return 1;
if (left === right)
return 0;
return -1;
}

// Імена завжди сортуються за абеткою, чи не так?
names.sort(sortAlphabetically);
}


Історично підтримка i18n в JavaScript зроблена погано. Для форматування з підтримкою и17и використовуються методи toLocaleString(). Підсумкові рядки містять ті деталі, які надає конкретна реалізація мови — немає можливості вибрати (чи потрібний день тижня в даті? а рік важливий, чи ні?). Навіть якщо включені всі деталі, формат може бути невірним — десятковий замість відсотків, і т. д. І локаль вибирати не можна.

При сортуванні пропонується майже марна порівняння тексту з урахуванням локалі (collation). Існує localeCompare(), але з незручним інтерфейсом, невідповідним для сортування. І у неї теж можна вибирати локаль.

Ці обмеження настільки складні, що серйозні програми відправляють дані на сервер, щоб там провести операцію, чутливу до локалі, і потім отримують результат. Передача даних на сервер і назад, тільки для форматування грошових сум. Маячня.

Нове API для и17и JS

Нове ECMAScript Internationalization API збільшує можливості JS. Надаються всі навороти для форматування дат і чисел і тексту. Локаль можна вибирати, а в цілях швидкодії це можна зробити один раз, а не кожен раз перед операцією.

Але це API не панацея, а в кращому випадку «хороша спроба». Точний формат виводу не заданий. Реалізація може підтримувати екзотичні мови, або ж просто ігнорувати всі параметри форматування. Більшість реалізацій будуть підтримувати багато локалей, але без жодних гарантій.

Реалізація Firefox залежить від бібліотеки International Components for Unicode (ICU), яка сама залежить від Unicode Common Locale Data Repository (CLDR). При цьому більшість функцій ICU написані на JavaScript.

Інтерфейс Intl

API и17и живе в об'єкті Intl. Він містить три конструктора: Intl.Collator, Intl.DateTimeFormat і Intl.NumberFormat. Створення об'єкта відбувається так:

var ctor = "Collator"; // або іншої із трьох
var instance = new Intl[ctor](locales, options);


locales — рядок, що задає тег мову або об'єкт, що містить декілька тегів мов. Тег — рядки типу en (англійська), de-AT (австрійський, німецький) або zh-Hant-TW (тайванський китайської традиційної запису). Теги можуть включати розширення юнікод у вигляді-u-key1-value1-key2-value2..., де кожен ключ — ключ розширення. Різні філософи по-різному це інтерпретують.

options — об'єкт, чиї властивості визначають форматування і сортування.

Firefox підтримує більше 400 локалей для сортування і більше 600 для форматування — так що, швидше за все потрібна локаль знайдеться.

Intl не гарантує конкретного поведінки. Якщо запитана локаль не підтримується, Intl намагається найкращим чином обробити запит. Якщо підтримується, то поведінка його не задано жорстко. Ніколи не можна передбачати, що конкретний набір налаштувань відповідає конкретному формату. Це може змінюватися від браузера до браузеру або від версії до версії. Не задані компоненти форматування — коротка запис для дня тижня може бути «S», «Sa» або «Sat».

Форматування дати і часу

Налаштування
weekday, era
"narrow", "short", або "long". (era - довші проміжки року: BC/AD, час правління імператора Японії, і т. д.)
month
"2-digit", "numeric", "narrow", "short" або "long"
year
day
hour, minute, second
"2-digit" або "numeric"
timeZoneName
"short" або "long"
timeZone
Регистронезависимое "UTC" задає форматування з урахуванням UTC. Значення типу "CEST" і "America/New_York" не зобов'язані оброблятися, і поки не підтримуються в Firefox.


Точний формат не задається, але сенс у тому, що «narrow», «short» і «long» видають результати різної довжини — «S» або «Sa», «Sat» і «Saturday». Висновок може бути двозначним Субота і Неділя в короткому вигляді можуть видати «S». «2-digit» і «numeric» означають двозначну або повнорозмірну запис дат: «70» і «1970».

Є і особливі налаштування:
hour12
12-годинний або 24-годинний формат. Зазвичай залежить від локалі. Від неї залежать деталі на зразок того, як записувати опівночі - 0 годин або 12pm, і чи треба писати ведучий нуль.


Є два особливі властивості localeMatcher (значення «lookup» або «best fit») і formatMatcher («basic» або «best fit»), за замовчуванням обидва мають значення «best fit». Задають те, як використовуються локаль і форматування. Вони використовуються дуже рідко і їх можна ігнорувати.

Налаштування, пов'язані з локаллю
DateTimeFormat дозволяє форматування за допомогою настроюваних календарних і числових систем. Ці деталі задаються в тезі мови у налаштуваннях Unicode-розширення.

Наприклад, тег тайської мови в Таїланді th-TH. Формат Unicode-розширення-u-key1-value1-key2-value2… Ключ календарної системи — ca, числовий — nu. У числовий системи Таїланду значення буде thai, а в китайській — Chinese. Тому для форматування дат ми приєднуємо ці розширення в кінець тега мови: th-TH-u-ca-chinese-nu-thai.

Подробиці читайте в документації.

Приклади
Після створення об'єкта DateTimeFormat треба використовувати його за допомогою функції format(). Це пов'язана функція, так що не потрібно викликати її безпосередньо. Їй передається часова мітка або об'єкт Date.

var msPerDay = 24 * 60 * 60 * 1000;

// July 17, 2014 00:00:00 UTC.
var july172014 = new Date(msPerDay* (44 * 365 + 11 + 197));


Давайте отформатируем дату для американського англійського. Включимо двозначні місяць/день/рік, години/хвилини і тимчасову зону в короткій записи.

var options =
{ year: "2-digit", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit",
timeZoneName: "short" };
var americanDateTime =
new Intl.DateTimeFormat("en-US", options).format;

print(americanDateTime(july172014)); // 07/16/14, 5:00 PM PDT


Тепер зробимо те ж для португальської бразильського і для португальської в Португалії. Формат зробимо достовірніше, з повною записом року і назвою місяця, але в зоні UTC.

var options =
{ year: "numeric", month: "long", day: "numeric",
hour: "2-digit", minute: "2-digit",
timeZoneName: "short", timeZone: "UTC" };
var portugueseTime =
new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options);

// 17 de julho de 2014 00:00 GMT
print(portugueseTime.format(july172014));


Компактне розклад швейцарських поїздів для UTC з використанням офіційних мов, перерахувавши від самого популярного до найменш популярного:

var swissLocales = ["de-CH", "fr-CH", "it-CH", "rm-CH"];
var options =
{ weekday: "short",
hour: "numeric", minute: "numeric",
timeZone: "UTC", timeZoneName: "short" };
var swissTime =
new Intl.DateTimeFormat(swissLocales, options).format;

print(swissTime(july172014)); // Do. 00:00 GMT


Спробуємо вивести дату по-японськи, з використанням японського календаря з роком та ерою:

var jpYearEra =
new Intl.DateTimeFormat("ja-JP-u-ca-japanese",
{ year: "numeric", era: "long" });

print(jpYearEra.format(july172014)); // 平成26年


А тепер — довга дата для Таїланду, з використанням тайських цифр і китайського календаря:

var options =
{ year: "numeric", month: "long", day: "numeric" };
var thaiDate =
new Intl.DateTimeFormat("th-TH-u-nu-thai-ca-chinese", options);

print(thaiDate.format(july172014)); // ๒๐ 6 ๓๑


Форматування чисел

Налаштування
Основні налаштування для форматування чисел наступні:

style
"currency", "percent" або "decimal" (за замовчуванням) для форматування значення
currency
трибуквенне позначення валюти USD або CHF. Обов'язково, якщо style = "currency", інакше сенсу не має.
currencyDisplay
"code", "symbol" або "name", за замовчуванням "symbol". "code" використовує трилітерний код, "symbol" використовує символ типу $ або £. "name" використовує назву валюти. minimumIntegerDigits
ціле від 1 до 21 (включно), за замовчуванням 1. За необхідності додаються лідируючі нулі.
minimumFractionDigits, maximumFractionDigits
ціле від 0 до 20 (включно). У рядку буде як мінімум minimumFractionDigits, і не більше maximumFractionDigits знаків після коми. За замовчуванням залежить від валюти (зазвичай 2, іноді 0 або 3) якщо style = "currency", інакше 0. Для відсотків максимум 0, 3 для десяткових чисел, а для валют - залежно від валюти.
minimumSignificantDigits, maximumSignificantDigits
цілі від 1 до 21 (включно). Якщо вказано, має перевагу перед попередніми налаштуваннями кількості цифр, та визначає мінімум і максимум значущих цифр.
useGrouping
логічне значення, за замовчуванням true. Визначає наявність в рядку групових роздільників (як , для поділу тисяч в англійській форматі).


Налаштування локалі
NumberFormat підтримує настроюється форматування чисел по ключу nu, точно так само, як це робить DateTimeFormat. Приміром, мовний тег для китайського — zh-CN. Система запису чисел Han задається як hanidec. Щоб відформатувати число для цієї системи, ми прицепляем Unicode-розширення у вигляді тега: zh-CN-u-nu-hanidec.

Повний опис можливостей див. у документації.

Приклади
Для початку, отформатируем валюти для китайської мови з використанням числовий запису Han. Виберіть стиль «currency», потім використовуйте код для китайського renminbi (yuan), grouping by default, with the usual number of fractional digits.

var hanDecimalRMBInChina =
new Intl.NumberFormat("zh-CN-u-nu-hanidec",
{ style: "currency", currency: "CNY" });

print(hanDecimalRMBInChina.format(1314.25)); // ¥ 一,三一四.二五


Тепер отформатируем вартість бензину за правилами США і UK

var gasPrice =
new Intl.NumberFormat("en-US",
{ style: "currency", currency: "USD",
minimumFractionDigits: 3 });

print(gasPrice.format(5.259)); // $5.259


Спробуємо арабські відсотки для використання в Єгипті. Переконаємося, що у них є мінімум два знаки після коми. Порядок цифр може бути іншою в системах з записом справа наліво.

var arabicPercent =
new Intl.NumberFormat("ar-EG",
{ style: "percent",
minimumFractionDigits: 2 }).format;

print(arabicPercent(0.438)); // ٤٣٫٨٠٪


Тепер — перську мову, використовуваний в Афганістані. Мінімум дві цифри цілої частини і не більше двох в дробовій.

var persianDecimal =
new Intl.NumberFormat("fa-AF",
{ minimumIntegerDigits: 2,
maximumFractionDigits: 2 });

print(persianDecimal.format(3.1416)); // ۰۳٫۱۴


І нарешті, виведемо кількість бахрейнських динар по-арабськи. Ці динари нехарактерно діляться на тисячні, тому у нас повинно бути три знаки після коми.

var bahrainiDinars =
new Intl.NumberFormat("ar-BH",
{ style: "currency", currency: "BHD" });

print(bahrainiDinars.format(3.17)); // د.ب. ٣٫١٧٠


Сортування

Налаштування
usage
"sort" або "search" (за замовчуванням "sort")
"base", "accent", "case" або "variant". Чутливість у тих випадках, коли одна і та ж основна буква має акценти, діакритичні знаки і регістр. Причому "базовость" літери залежить від локалі - в німецькому біля літер "a" and "ä" базова одна буква, а в шведському - різні. У разі "base" розглядається тільки базова буква (у разі німецької мови "a", "A" і "ä" будуть однаковими). "accent" розглядає базову букву і акцент, без обліку регістра ("a" і "A" будуть однаковими, а "ä" буде від них відрізнятися). "case" розглядає базову букву і регістр, ігноруючи акцент ("a" і "ä" будуть однакові, а "A" буде відрізнятися). Нарешті, "variant" відрізняє всі особливості букв. При використанні "sort" за замовчуванням використовується "variant"; інакше - в залежності від локалі.
numeric
логічне значення, визначає сортування чисел за значенням або за символами цифр. Тобто, у разі numeric послідовність "F-4 Phantom II", "F-14 Tomcat", "F-35 Lightning II"; у разі не numeric послідовність "F-14 Tomcat", "F-35 Lightning II", "F-4 Phantom II".
caseFirst
"upper", "lower" або "false" (за замовчуванням). Як враховується регістр при сортуванні - "upper" означає перевагу верхнього регістру ("B", "a", "с"), "lower" навпаки ("a", "с", "B") і "false" ігнорує регістр ("a", "B", "с").
ignorePunctuation
логічне значення, false за замовчуванням, визначає, чи треба ігнорувати пунктуацію при порівняннях (наприклад "biweekly" і "bi-weekly" будуть рівні).


Налаштування локалі
Налаштування сортування в Unicode-розширення визначається як co, і задає тип сортування — адресна книга (phonebk), словник (dict), та інші.

Додатково ключі kn і kf можуть дублювати властивості numeric і caseFirst об'єкта options. Але їх підтримка не гарантована, тому краще їх не використовувати.

Приклади
У об'єктів Collator є функція compare. Вона приймає аргументи x і y і повертає число, менше нуля, якщо x < y, 0 якщо x = y, і число більше нуля, якщо x > y.

Спробуємо сортувати німецькі імена. У Німеччині є дві різні послідовності сортування — адресна книга і словник. Перша заснована на вимові, коли "ä", "ö" та інші розкриваються як «ae», «oe» і т. д.

var names =
["Hochberg", "Hönigswald", "Holzman"];

var germanPhonebook = new Intl.Collator("de-DE-u-co-phonebk");

// послідовність ["Hochberg", "Hoenigswald", "Holzman"]:
// Hochberg, Hönigswald, Holzman
print(names.sort(germanPhonebook.compare).join(", "));


Деякі слова з'єднуються умляутами, тому в словниках їх має сенс сортувати з ігноруванням умляутов (крім випадків, коли слова відрізняються лише умляутами, наприклад schon перед schön).

var germanDictionary = new Intl.Collator("de-DE-u-co-dict");

// послідовність ["Hochberg", "Honigswald", "Holzman"]:
// Hochberg, Holzman, Hönigswald
print(names.sort(germanDictionary.compare).join(", "));


Відсортуємо версії Firefox, зазначені з різними помилками, випадковими акцентами і диакритиками, за правилами американської англійської мови. Враховуємо номер версії і сортуємо за значенням числа, а не за символами цифр.

var firefoxen =
["FireFøx 3.6",
"Fire-fox 1.0",
"Firefox 29",
"FÍrefox 3.5",
"Fírefox 18"];

var usVersion =
new Intl.Collator("en-US",
{ sensitivity: "base",
numeric: true,
ignorePunctuation: true });

// Fire-fox 1.0, FÍrefox 3.5, FireFøx 3.6, Fírefox 18, Firefox 29
print(firefoxen.sort(usVersion.compare).join(", "));


Нарешті, виконаємо пошук рядків з ігноруванням регістра і акцентів.

var decoratedBrowsers =
[
"A\u0362maya", // Amaya
"CH\u035Brôme", // CHrôme
"FirefÓx",
"sAfàri",
"o\u0323pERA", // ọpERA
"I\u0352E", // IE
];

var fuzzySearch =
new Intl.Collator("en-US",
{ usage: "search", sensitivity: "base" });

function findBrowser(browser)
{
function cmp(other)
{
return fuzzySearch.compare(browser, other) === 0;
}
return cmp;
}

print(decoratedBrowsers.findIndex(findBrowser("Firêfox"))); // 2
print(decoratedBrowsers.findIndex(findBrowser("Safåri"))); // 3
print(decoratedBrowsers.findIndex(findBrowser("Ãmaya"))); // 0
print(decoratedBrowsers.findIndex(findBrowser("Øpera"))); // 4
print(decoratedBrowsers.findIndex(findBrowser("Chromè"))); // 1
print(decoratedBrowsers.findIndex(findBrowser("IË"))); // 5


Додаткова інформація

Може бути корисно визначити, чи підтримуються якісь операції в конкретних локалях, чи підтримується сама локаль. Для цього в кожному конструкторі є функція supportedLocales(), а в кожному прототипі — resolvedOptions().

var navajoLocales =
Intl.Collator.supportedLocalesOf(["nv"], { usage: "sort" });
print(navajoLocales.length > 0
? "Navajo collation supported"
: "Navajo collation not supported");

var germanFakeRegion =
new Intl.DateTimeFormat("de-XX", { timeZone: "UTC" });
var usedOptions = germanFakeRegion.resolvedOptions();
print(usedOptions.locale); // de
print(usedOptions.timeZone); // UTC


Спадкове поведінка

До ES5 у функцій toLocaleString і localeCompare не було такою просунутою семантики, вони не брали налаштування і були, по суті, марні. Тому їх поведінка була змінена для підтримки операцій Intl. Якщо вам не особливо важливо точне поведінку програми щодо локалей, можна використовувати і старі функції. В іншому випадку рекомендується використовувати примітиви з Intl безпосередньо.

Висновок

Інтернаціоналізація — дуже цікава тема, складність якої обмежена лише природою людського спілкування. API інтернаціоналізації звертається до невеликої, але потрібної частини цієї складності, і робить простіше написання веб-додатків, що враховують локалізацію.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.