Rozwiąż część problemów wielojęzyczności za pomocą API Intl!

Tłumaczenie w przeglądarce dzięki obiektowi Intl

Spis Treści

  1. Wprowadzenie
  2. Sortowanie z uwzględnieniem znaków diaktrycznych
    1. Intl.Collator
    2. Przykład sortowania z użyciem Int.Collator.compare
      1. Podpowiedzi z użyciem elementu Datalist
  3. Formatowanie dat
    1. Intl.DateTimeFormat
    2. Przykład użycia Intl.DateTimeFormat
  4. Tłumaczenie nazw
    1. Intl.DisplayNames
    2. Przykład użycia Intl.DisplayNames
  5. Względne formatowanie dat
    1. Intl.RelativeTimeFormat
    2. Przykład Intl.RelativeTimeFormat
  6. Kod przykładów na Github
  7. Interaktywny przykład na Github Pages
  8. Podsumowanie

Wprowadzenie

Internacjonalizacja aplikacji niesie za sobą wiele wyzwań – różne języki w różny sposób formatują daty, liczby czy czas relatywny, są to rzeczy bardzo kontekstowe które nie zawsze da się rozwiązać z poziomu tłumaczeń np. z plików JSON. Wyświetlanie nazw państw, regionów czy jednostek administracyjnych również nastręcza problemów ponieważ jest tego bardzo dużo. Jest też potrzeba formatowania wyświetlania walut.

Choć trzymanie tłumaczeń plikach JSON nie zniknie zapewne nigdy, część przypadków da się rozwiązać natywnie – przy użyciu obiektu Intl. Obiekt ten zawiera metody zwracające obiekty które mogą tłumaczyć relatywny czas czy nazwy krajów w podanym języku, czym oszczędzą nam pracy przy tłumaczeniu aplikacji.

Sortowanie z uwzględnieniem znaków diaktrycznych

Intl.Collator

Problemem podczas sortowania alfabetycznie list jest to, że domyślne sortowanie nie uwzględnia specyficznych dla danego języka znaków diaktrycznych.

Np. w języku polskim litera ą występuje po a i przed b. W innym językach potrafi być jeszcze ciekawiej – w języku niemieckim ä jest po a a w szwedzkim po z.

Z pomocą tutaj przychodzi nam obiekt Collator, który po utworzeniu udostępnia metodę compare, potrafiącą radzić sobie ze znakami diaktrycznymi.

Przykład sortowania z użyciem Int.Collator.compare

Stwórzmy następującą strukturę HTML:

<div>
  <label for="collator-locale">Locale</label>
  <input list="collator-locales" type="text" id="collator-locale">
  <datalist id="collator-locales">
    <option>pl-PL</option>
    <option>en-GB</option>
    <option>en-US</option>
  </datalist>
  <br>
  <button id="collator-sort">Sort</button>
  <pre id="collator-result"></pre>
  <br>
</div>

Struktura ta pozwoli wyświetlić nam rezultat sortowania tablicy w zależności od wybranego języka, z którym zainicjowany będzie Collator.

Kod JavaScript prezentuje się następująco:

const arrayToSort = ['aa', 'ab', 'ąa', 'ba'];

document.querySelector('#collator-result').innerHTML = JSON.stringify(arrayToSort);

document.querySelector('#collator-sort').addEventListener('click', () => {
  const localArrayToSort = [...arrayToSort]; // sort modifies array instead of returning new array
  const locale = document.querySelector('#collator-locale').value;
  const compare = locale? new Intl.Collator(locale).compare : undefined;
  localArrayToSort.sort(compare);
  document.querySelector('#collator-result').innerHTML = JSON.stringify(localArrayToSort);
});

Najpierw tworzymy tablicę którą chcemy sortować. Następnie pod kliknięcie przycisku Sort podpinamy funkcję, która skopiuje główną tablicę, pobierze język dla którego chcemy tablicę posortować i posortuje elementy wg tego języka. Rezultat zostanie wpisany do odpowiedniego elementu w HTML.

Końcowy efekt przykładu prezentuje się następująco:

Przykład sortowania z użyciem Intl.Collator
Przykład sortowania z użyciem Intl.Collator – rozwinięte podpowiedzi

Podpowiedzi z użyciem elementu Datalist

Użycie elementu Datalist który zawiera opcje wyboru pozwoli nam zaimplementować ciekawą funkcjonalność – będzie można wybrać element z listy która wyświetli się po kliknięciu w pole bądź wpisać ręcznie. Dodatkowo, lista ta będzie filtrowana znakami które już wpiszemy w polu.

Formatowanie dat

Intl.DateTimeFormat

Jeżeli chcielibyśmy sformatować datę w danym języku trzeba pamiętać o kilku kwestiach takich jak przetłumaczenie nazwy dnia tygodnia, miesiąca czy kolejność. Przykładowo w języku angielskim w odmianie brytyjskiej najpierw występuje dzień, potem miesiąc i rok. W odmianie amerykańskiej za to najpierw jest miesiąc, potem dzień i na końcu rok. Z pomocą przychodzi nam obiekt DateTimeFormat, który po utworzeniu udostępnia między innymi metody format i formatRange.

Przykład użycia Intl.DateTimeFormat

Do utworzenia daty użyjemy pola typu datetime-local, który umożliwia wybór zarówno daty jak i godziny. W przeglądarce Chrome prezentuje się on następująco:

rozwinięte pole datetime-local

Stwórzmy następujący kod HTML:

<label for="datetime-format-date">Date</label>
<input id="datetime-format-date" type="datetime-local">
<br>
<label for="datetime-format-locale">Locale</label>
<input list="locales" type="text" id="datetime-format-locale">
<br>
<button id="datetime-format-transform-date">Transform date</button>
<pre id="datetime-format-transform-date-result"></pre>

UI który ten kod reprezentuje pozwoli nam wybrać datę wraz z godziną, ustawić język i po kliknięciu przycisku Transform date wyświetlić datę przetłumaczoną na wybrany język.

Stwórzmy kod JS:

document.querySelector('#datetime-format-transform-date').addEventListener('click', () => {
  const date = new Date(document.querySelector('#datetime-format-date').value);
  const locale = document.querySelector('#datetime-format-locale').value;
  const dateTimeFormat = new Intl.DateTimeFormat(locale, {dateStyle: 'full', timeStyle: 'short'});
  const formatted = dateTimeFormat.format(date);
  document.querySelector('#datetime-format-transform-date-result').innerHTML = formatted;
});

Kod ten, po kliknięciu przycisku Transform date pobiera datę i język. Następnie tworzy obiekt DateTimeFormat z opcjami dzięki którymi wyświetlimy pełną datę – zawierającą nazwę miesiąca i dnia tygodnia oraz godzinę w formie krótkiej, bez strefy czasowej. Dla tego przykładu uproszczona konfiguracja wystarczy, mnogość opcji dostępna przy tworzeniu obiektu pozwoli na ewentualną szerszą konfigurację.

Efekt działania kodu wygląda następująco:

Formatowanie daty dla amerykańskiego angielskiego

Sprawdźmy w jaki sposób przykład ten sformatuje datę dla brytyjskiej odmiany angielskiego:

Formatowanie daty dla brytyjskiego angielskiego

Tłumaczenie nazw

Intl.DisplayNames

Jeśli chodzi o tłumaczenie nazw krajów, języków i walut pomoże nam obiekt utworzony przez konstruktor DisplayNames. Dane do przykładu pobierzemy z API udostępnianego przez RestCountries z którego pobierzemy listę krajów członkowskich Unii Europejskiej – konkretnie spod adresu https://restcountries.eu/rest/v2/regionalbloc/eu. Z obiektów które przyjdą w odpowiedzi na żądanie weźmiemy tylko kod kraju, jego języki oraz waluty. Dodatkowo dane wpiszemy do localStorage, by nie nadużywać darmowego API.

Przykład użycia Intl.DisplayNames

Pomijając elementy które powtarzają się z poprzednich przykładów (pełny kod dostępny będzie na Github i na Github Pages w wersji live) czyli pole wyboru języka stwórzmy kod HTML który pozwoli wyświetlać dane krajów w tabelce:

<details>
  <summary>Show EU countries with languages and currencies</summary>
  <table id="display-names-table">
    <thead>
    <tr>
      <th>Country</th>
      <th>Languages</th>
      <th>Currencies</th>
    </tr>
    </thead>
    <tbody>
      <tr>
        <td colspan="3">Click button to see translated data</td>
      </tr>
    </tbody>
  </table>
</details> 

Element details pozwoli nam stworzyć element w którym po kliknięciu jego zawartość się rozwinie i zobaczymy tabelkę z nazwą kraju, językami i walutami w nich używanymi.

Dane pobierzemy w następujący sposób:

if (!data) {
  const response = await fetch('https://restcountries.eu/rest/v2/regionalbloc/eu');
  const body = await response.json();
  data = body.map(el =>
    ({
      code: el.alpha2Code,
      languages: el.languages.map(el => el.iso639_1),
      currencies: el.currencies.map(el => el.code),
    })
  );
  localStorage.setItem(localStorageKey, JSON.stringify(data));
}

Po kliknięciu odpowiedniego przycisku utworzymy translatory:

const locale = document.querySelector('#display-names-locale').value;
const namesTranslator = new Intl.DisplayNames(locale, { type: 'region' });
const languageTranslator = new Intl.DisplayNames(locale, { type: 'language' });
const currencyTranslator = new Intl.DisplayNames(locale, { type: 'currency' });

Następnie przy pomocy metody of przetłumaczymy nazwę kraju, jego języki i waluty i przemapujemy eja wiersze tabeli. Pamiętać musimy by dla języków i walut użyć bloku try..catch, ponieważ nie możemy być pewni że API zawsze zwróci kody obsługiwane przez DisplayNames:

const tableRowsStr = data.map(el => {
  const languages = el.languages.map(el => {
    try {
      return languageTranslator.of(el);
    }
    catch (e) {
      return '';
    }
  }).join(',');
  const currencies = el.currencies.map(el => {
    try {
      return currencyTranslator.of(el);
    }
    catch (e) {
      return '';
    }
  }).join(',');
  return (
    `
      <tr>
        <td>${namesTranslator.of(el.code)}</td>
        <td>${languages}</td>
        <td>${currencies}</td>
      </tr>
      `
   );
}).join('');
document.querySelector('#display-names-table').innerHTML = tableRowsStr;

Względne formatowanie dat

Intl.RelativeTimeFormat

Spójrzmy na screen z Messengera, który pokazuje daty otrzymania wiadomości sformatowane relatywnie:

Względne formatowanie dat przez Messenger

Określenia typu X dni czy X godzin to właśnie formatowanie względne. Osiągnąć je możemy bez użycia bibliotek zewnętrznych dzięki RelativeTimeFormat.

Przykład Intl.RelativeTimeFormat

Stwórzmy przykład który pozwoli wybrać datę a następnie wyświetlić względną datę w stosunku do obecnej daty.

Utwórzmy kod HTML:

<label for="relative-time-date">Date</label>
<input id="relative-time-date" type="datetime-local">
<br>
<label for="relative-time-locale">Locale</label>
<input list="locales" type="text" id="relative-time-locale">
<br>
<button id="relative-time-transform">Transform to relative</button>
<pre id="relative-time-transform-result"></pre>

A następnie kod JS który pobierze dane z pól, policzy w jakiej jednostce chcemy formatować i wyświetli te dane użytkownikowi:

const locale = document.querySelector('#relative-time-locale').value;
const date = new Date(document.querySelector('#relative-time-date').value);
const current = new Date();
let value;
let unit;
if (date.getFullYear() !== current.getFullYear()) {
  value = date.getFullYear() - current.getFullYear();
  unit = 'year';
}
else if (date.getMonth() !== current.getMonth()) {
  value = date.getMonth() - current.getMonth();
  unit = 'month';
}
else if (date.getDate() !== current.getDate()) {
  value = date.getDate() - current.getDate();
  unit = 'day';
}
else if (date.getHours() !== current.getHours()) {
  value = date.getHours() - current.getHours();
  unit = 'hour';
}
else if (date.getMinutes() !== current.getMinutes()) {
  value = date.getMinutes() - current.getMinutes();
  unit = 'minute';
}
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto'});
document.querySelector('#relative-time-transform-result').innerHTML = formatter.format(value, unit);

Kod przykładów na Github

Pełny kod przykładu znajdziesz na:

https://github.com/radek-anuszewski/intl-example

Interaktywny przykład na Github Pages

Jeżeli chcesz sprawdzić działanie przykładu na żywo, znajdziesz go na stronie:

https://radek-anuszewski.github.io/intl-example/index.html

Podsumowanie

Choć Intl nie zastąpi klasycznej obsługi tłumaczeń, pomaga w kilku specyficznych przypadkach. Tematy typu formatowanie dat w danym języku czy względne daty potrafią być trudne w obsłudze i bardzo dobrze, że istnieją możliwości by obsłużyć te przypadki natywnie.