Имея более 35 000 звезд на GitHub и более 1000 пакетов NPM в своей экосистеме, я думаю, можно с уверенностью сказать, что JavaScript-фреймворк Svelte нашел для себя солидную нишу. И не зря: в отличие от Angular, React и Vue, в Svelte нет виртуального DOM. По сути, это компилятор, который не поставляется с вашим приложением, а это означает, что он почти не занимает места в вашем комплекте приложений. Svelte также исключительно минимален и элегантен по своему дизайну, что делает его довольно удобным для изучения. На самом деле, если вы еще не пробовали Svelte, я призываю вас попробовать.

Но я предполагаю, что проповедую хору, и что вас уже укусил жук Svelte. Ставлю на другое предположение: у вас есть приложение Svelte, и вы хотите его интернационализировать и локализовать. Ну, не смотрите дальше. В этой статье мы создадим небольшое демонстрационное приложение и воспользуемся библиотекой svelte-i18n Christian Kaisermann для его интернационализации. Давайте приступим!

🗒Примечание »Мы ранее писали о Svelte i18n. В этой статье мы использовали svelte-i18n версии 1-beta. Эта статья представляет собой обновление, которое охватывает svelte-18n версии 3.

Наше демонстрационное приложение

Наше приложение Rebel Voter позволяет пользователям голосовать за и против персонажей «Звездных войн».

Хотя Rebel Voter на самом деле не будет подключаться к серверу для сохранения своих данных о голосовании, он послужит отличной демонстрацией i18n для Svelte и svelte-i18n 3. Давайте быстро создадим его и перейдем к локализовать его.

🔗 Ресурс » Вы можете получить весь код проекта из нашего сопутствующего репозитория на GitHub.

Установка Svelte

Я считаю, что самый быстрый способ установить Svelte — использовать команду npx degit из командной строки.

$ npx degit sveltejs/template rebel-voter
$ cd rebel-voter
$ npm install

Эти команды создадут шаблон нашего приложения с использованием официального шаблона Svelte и установят все зависимости пакета NPM, необходимые для начала приготовления.

Со всем этим мы можем запустить наш сервер разработки через npm run dev.

Основное приложение

Во-первых, давайте изменим файл App.svelte, который поставляется с шаблоном Svelte, чтобы он выглядел следующим образом.

<script>
  import Header from './components/Layout/Header.svelte';
  import Footer from './components/Layout/Footer.svelte';
  import CharacterList from
    './components/Characters/CharacterList.svelte';
</script>

<Header />

<main role="main">
  <CharacterList />
</main>

<Footer />

<style>
  main { padding: 0 1rem; }
</style>

Начнем с трех компонентов. Header и Footer — компоненты презентационного макета. Сердце и душа нашего приложения лежат в CharacterList. Давайте взглянем на код всех трех этих компонентов.

Верхний и нижний колонтитулы

Наши Header и Footer — это простые презентационные компоненты.

<header class="hero">
  <div class="hero-body">
    <div class="container">
      <h1 class="title">Rebel Voter</h1>

      <h2 class="subtitle">Your favorite Star Wars characters</h2>
     </div>
  </div>
</header>
<style>
  /* Styles are omitted for brevity
     You can grab them on GitHub. */
</style>

<footer class="footer">
    <div class="content has-text-centered">
        <p>
            Companion to a
            <a href="https://phrase.com/blog">Phrase blog
            </a> article. Made with
            <a href="https://svelte.dev/">Svelte</a> &amp;
            <a href="https://bulma.io/">Bulma</a>.
        </p>
    </div>
</footer>

🔗 Ресурс » Здесь мы используем изящную структуру Bulma CSS для стилизации. Фактически, большинство классов CSS, которые вы увидите в этой статье, являются классами Bulma.

Список персонажей

В нашем компоненте CharacterList начинается основное действие. Компонент загружает наши демонстрационные данные JSON и представляет их.

Давайте посмотрим, как устроены наши данные.

[
  {
    "id": 1,
    "name": "Luke Skywalker",
    "imageUrl": "https://upload.wikimedia.org/wikipedia/en/9/9b/Luke_Skywalker.png",
    "firstAppearedInFilm": {
      "title": "A New Hope",
      "releasedAt": "1977-05-25"
    },
    "upVoteCount": 22,
    "downVoteCount": 4
  },
  {
    "id": 2,
    "name": "Princess Leia",
    "imageUrl": "https://upload.wikimedia.org/wikipedia/en/1/1b/Princess_Leia%27s_characteristic_hairstyle.jpg",
    "firstAppearedInFilm": {
      "title": "A New Hope",
      "releasedAt": "1977-05-25"
    },
    "upVoteCount": 19,
    "downVoteCount": 2
  },
  // ...
]

Теперь давайте используем эти данные в нашем компоненте Character.

<script>
  import Character from './Character.svelte';

  function fetchCharacters() {
    return fetch('/data/characters.json')
      .then(response => response.json());
  }
</script>

{#await fetchCharacters()}
  <p>Loading...</p>
{:then characters}
  <div class="columns is-mobile is-multiline">
    {#each characters as character}
      <div
        class="column is-one-third-desktop is-half-tablet is-full-mobile"
      >
        <Character {character} />
      </div>
    {/each}
  </div>
{:catch error}
  <p>There was a problem loading characters.</p>
{/await}

Мы просто fetch() наши данные, перебираем массив, который он нам дает, и передаем каждый элемент в массиве Character для его отображения.

<script>
  import VotingButton from '../UI/VotingButton.svelte';

  export let character;

  const {
      name,
      imageUrl: src,
      firstAppearedInFilm,
  } = character;

  let {
    upVoteCount,
    downVoteCount,
  } = character;
</script>

<style>
  /* Styles are omitted for brevity
     You can grab them on GitHub. */
</style>

<div class="box">
  <div class="columns is-mobile">
    <div class="column is-one-quarter img-container">
      <img {src} alt="{name}">
    </div>

    <div class="column">
      <h3 class="is-size-5 is-uppercase name">
        {name}
      </h3>

      <p class="first-appeared">
        First appeared in
        <span class="first-appeared-title">
          {firstAppearedInFilm.title},
        </span>
        {firstAppearedInFilm.releasedAt}
      </p>

        <div class="buttons has-addons">
          <VotingButton
            type="up"
            count={upVoteCount}
            on:click={() => upVoteCount += 1}
          />

          <VotingButton
            type="down"
            count={downVoteCount}
            on:click={() => downVoteCount += 1}
          />
        </div>
    </div>
  </div>
</div>

Character управляет двумя VotingButton, которые помогают нам продемонстрировать динамическое поведение голосов за и против.

<script>
  export let count = 0;
  export let type = "up";
</script>

<style>
  .button { min-width: 5rem; }

  .button-count { font-size: 0.9rem; }
</style>

<button class="button" on:click>
  <span class="icon">
    <span class="far fa-thumbs-{type}" />
  </span>

  <span class="button-count">{count}</span>
</button>

Классы fa-thumbs-up и fa-thumbs-down — это CSS-классы FontAwesome, которые отображают значок большого пальца вверх и значок большого пальца вниз соответственно.

Теперь мы можем добраться до наших i18n и l10n.

🗒Примечание »Если вы еще не занимались программированием и хотите начать прямо сейчас, когда мы занимаемся i18n, клонируйте себе копию нашего репозитория Git с GitHub и ознакомьтесь с start отделение.

Установка svelte-i18n

svelte-i18n устанавливается, как вы понимаете, через NPM.

$ npm install --save svelte-i18n

✋🏽 Внимание! » После установки svelte-i18n и запуска сервера разработки вы можете получить предупреждение о том, что (!) это было переписано в неопределенное. Известная проблема при использовании Rollup с некоторыми модулями. Код, на который ссылаются предупреждения, кажется, отлично работает в нашем случае. Тем не менее, если вы хотите избавиться от предупреждений, вы можете изменить конфигурацию Rollup проекта, чтобы передать модулям значение this, которое они ожидают. По сути, мы сделали всю работу за вас, и она находится в нашем репозитории на GitHub.

Начальная загрузка svelte-i18n

svelte-i18n относительно прост в использовании. Просто нужно немного настроить, чтобы начать работу.

Словари сообщений перевода

svelte-i18n работает со словарями сообщений перевода ключ/значение, по одному для каждого языка. Библиотека принимает эти словари через функцию addMessages(). Мы вернемся к этому через мгновение. Давайте сначала организуем наш проект, разместив наши словари перевода в файлах для каждого языка.

{
  "app_title": "Rebel Voter",
  "app_slogan": "Your favorite Star Wars characters"
}
{
  "app_title": "الناخب المتمرد",
  "app_slogan": "شخصياتك المفضلة في حرب النجوم"
}

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

Работа с JSON в Svelte

На момент написания, если мы хотим работать с файлами JSON в Svelte, нам нужно установить плагин JSON Rollup.

$ npm install --save-dev @rollup/plugin-json

Также нам нужно настроить плагин через файл rollup.config.js.

// ...
import json from "@rollup/plugin-json";

// ...

export default {
  // ...
  plugins: [
    json(),
    svelte({
      // ...
    }),

    // ...
};

// ...

Теперь мы можем импортировать наши файлы перевода JSON и передать их addMessages() svelte-i18n. Нам также нужно вызвать библиотечную функцию init(), чтобы сообщить svelte-i18n, на каком языке должно загружаться наше приложение.

<script>
  import { addMessages, init } from "svelte-i18n";

  // ...

  import en from "./lang/en.json";
  import ar from "./lang/ar.json";

  addMessages("en", en);
  addMessages("ar", ar);

  init({
    initialLocale: "en",
  });
</script>

<Header />

  <main role="main">
        <CharacterList />
  </main>

<Footer />

Базовый перевод

Хотите верьте, хотите нет, но на данный момент мы интернационализировали наше приложение. Теперь мы можем использовать _ (подчеркивание) Svelte store из svelte-i18n для отображения переведенных сообщений вместо жестко закодированных.

<script>
  import { _ } from "svelte-i18n";
</script>

<header class="hero">
    <div class="hero-body">
        <div class="contiainer">
            <h1 class="title">{$_("app_title")}</h1>

            <h2 class="subtitle">{$_("app_slogan")}</h2>
        </div>
    </div>
</header>

Мы просто передаем наши ключи сообщений перевода в хранилище $_(). Магазин всегда будет использовать сообщения из нашей активной локали/языка. $_(), как и любой другой магазин Svelte, является реактивным, что означает, что он заставит наши компоненты повторно отображать, если активная локаль или сообщения перевода изменятся.

Если мы запустим наше приложение сейчас, все будет выглядеть так же. Однако, если мы изменим значение initialLocale в нашем файле App.svelte на "ar", в заголовке нашего приложения будут отображаться наши арабские переводы.

Загрузка файла асинхронного перевода

Наша текущая настройка отлично подходит для небольших приложений. Однако, если наши файлы перевода станут значительно больше, мы можем начать облагать налогом пропускную способность наших пользователей (и терпение, поскольку наше приложение может замедлиться). Это связано с тем, что в настоящее время мы объединяем файлы перевода для всех языков в наш основной пакет приложения.

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

Первое, что нам нужно сделать, это сделать наши файлы перевода доступными для скачивания, переместив их в каталог public.

src/lang/en.json → public/lang/en.json
src/lang/ar.json → public/lang/ar.json

Теперь мы можем написать обертку вокруг функций svelte-i18n, которая загружает наши файлы и настраивает svelte-i18n для их использования.

import { get } from "svelte/store";
import {
  addMessages,
  locale,
  init,
  dictionary,
  _,
} from "svelte-i18n";

const MESSAGE_FILE_URL_TEMPLATE = "/lang/{locale}.json";

function setupI18n(options) {
  const { withLocale: locale_ } = options;

  // Initialize svelte-i18n
  init({ initialLocale: locale_ });

  // Don't re-download translation files
  if (!hasLoadedLocale(locale_)) {
    const messagesFileUrl = 
      MESSAGE_FILE_URL_TEMPLATE.replace(
        "{locale}",
        locale_,
      );

    // Download translation file for given locale/language
    return loadJson(messagesFileUrl).then((messages) => {
      // Configure svelte-i18n to use the locale
      addMessages(locale_, messages);

      locale.set(locale_);
    });
  }
}

function loadJson(url) {
  return fetch(url).then((response) => response.json());
}

function hasLoadedLocale(locale) {
  // If the svelte-i18n dictionary has an entry for the
  // locale, then the locale has already been added
  return get(dictionary)[locale];
}

// We expose the svelte-i18n _ store so that our app has
// a single API for i18n
export { _, setupI18n };

Наш модуль предоставляет функцию setupI18n(options: Object), которая на данный момент принимает один вариант, withLocale: string. withLocale — это просто язык, который мы хотим настроить, загрузив его файл перевода и инициализировав svelte-i18n для его использования.

Теперь мы можем обновить наш компонент App.svelte, чтобы использовать наш новый модуль.

<script>
  // Remove all code that references svelte-i18n
  // directly and use our wrapper library instead
  import { setupI18n } from "./services/i18n";
 
  // ...

   setupI18n({ withLocale: "ar" });
</script>

<!-- ... -->

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

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

🗒Примечание »svelte-i18n любезно предупреждает нас о подобных сценариях, записывая полезные сообщения в консоль браузера.

Рендеринг после загрузки локали

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

import { get, derived, writable } from "svelte/store";

// ...

let _activeLocale;

// Internal store for tracking network
// loading state
const isDownloading = writable(false);

function setupI18n(options) {
  // ...

  if (!hasLoadedLocale(locale_)) {
    isDownloading.set(true);

    // ...

    return loadJson(messagesFileUrl).then((messages) => {
      _activeLocale = locale_;

      addMessages(locale_, messages);

      locale.set(locale_);

      isDownloading.set(false);
    });
  }
}

const isLocaleLoaded = derived(
  [isDownloading, dictionary],
  ([$isDownloading, $dictionary]) =>
    !$isDownloading &&
    $dictionary[_activeLocale] &&
    Object.keys($dictionary[_activeLocale]).length > 0,
);

// ...

export { _, setupI18n, isLocaleLoaded };

isLocaleLoaded — это производное хранилище Svelte: оно слушает изменения в двух других хранилищах, нашем собственном isDownloading и сообщениях svelte-i18n dictionary. Когда наш активный файл перевода завершил загрузку, и svelte-i18n загрузил свои переводы в свой словарь сообщений isLocaleLoaded === true.

Как и в любом другом магазине Svelte, мы можем использовать isLocaleLoaded с префиксом $ в наших компонентах. Когда мы это сделаем, Svelte подпишет наш компонент на хранилище и повторно отобразит его при обновлении хранилища. Svelte также отпишется от магазина, когда наш компонент будет уничтожен.

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

<script>
  import { setupI18n, isLocaleLoaded } from "./services/i18n";
  
  // ...

  setupI18n({ withLocale: "ar" });
</script>

{#if $isLocaleLoaded}
  <Header />

  <main role="main">
    <CharacterList />
  </main>

  <Footer />
{:else}
  <p>Loading...</p>
{/if}

<!-- ... -->

Этот UX немного лучше, согласны?

Получение и установка активной локали

Получить и установить вручную активную локаль в svelte-i18n можно, взаимодействуя с хранилищем locale Svelte.

<script>
  import { locale } from "svelte-i18n";

  // React to locale changes
  locale.subscribe((newLocale) => {
    // Do something with newLocale, or not.
  });

  // Set the active locale to Canadian French
  locale.set("fr-CA");
</script>

Конечно, мы можем использовать синтаксис $locale в разметке наших компонентов, чтобы получить легкую реактивность.

<!-- Class will be .button.button-fr-CA when
     our locale is Canadian French -->
<div class="button button-{$locale}">
  {$_("subscribe")}
</div>

Простой переключатель языка

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

<script>
  import { createEventDispatcher } from "svelte";

  export let value;

  const dispatch = createEventDispatcher();

  function handleLocaleChange(event) {
    event.preventDefault();

    dispatch("locale-changed", event.target.value);
  }
</script>

<style>
  /* ... */
</style>

<div class="locale-selector">
  <div class="select">
    <select value={value} on:change={handleLocaleChange}>
      <option value="en">English</option>
      <option value="ar">عربي</option>
    </select>
  </div>
</div>

LocaleSelector не имеет никакого внутреннего состояния. Вместо этого он предоставляет реквизит value, который соответствует активному элементу в его внутреннем раскрывающемся списке <select>. LocaleSelector также запускает пользовательское событие locale-changed всякий раз, когда пользователь выбирает новую локаль в раскрывающемся списке.

Теперь мы можем использовать LocaleSelector для обновления нашей активной локали в App.svelte.

Во-первых, давайте представим хранилище locale svelte-i18n через нашу собственную библиотеку-оболочку.

import {
  addMessages,
  locale,
  init,
  dictionary,
  _,
} from "svelte-i18n";

// ...

export { _, setupI18n, isLocaleLoaded, locale };

Это обеспечивает согласованность нашего API i18n. Давайте импортируем locale и подключим его к нашему новому LocaleSelector.

<script>
  import {
    setupI18n,
    isLocaleLoaded,
    locale,
  } from "./services/i18n";
  // ...
  
  import LocaleSelector from
    "./components/UI/LocaleSelector.svelte";

  // ...
</script>

{#if $isLocaleLoaded}
  <Header />

  <LocaleSelector
    value={$locale}
    on:locale-changed={e =>
      setupI18n({ withLocale: e.detail }) }
  />

  <!-- ... -->
{/if}

<!-- ... -->

И вуаля!

Автоматическое определение локали пользователя

Выпадающий список языков часто необходим, но иногда мы хотим угадать предпочитаемый пользователем язык из настроек браузера или каким-либо другим способом. В svelte-i18n есть несколько встроенных способов сделать это. Давайте рассмотрим один из самых распространенных: обнаружение через браузер.

🔗Ресурс »Ознакомьтесь с документацией svelte-i18n, чтобы узнать обо всех методах автоматического определения локали, предоставляемых библиотекой.

Поддерживаемые локали

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

// Locales our app supports
const locales = {
  en: "English",
  ar: "عربي",
};

// Locale to show when we don't support the
// requested locale
const fallbackLocale = "en";

export { locales, fallbackLocale };

Обнаружение с откатом

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

// ...

// options object is now an optional param
function setupI18n(options = {}) {

  // If we're given an explicit locale, we use
  // it. Otherwise, we attempt to auto-detect
  // the user's locale.
  const locale_ = supported(
    options.withLocale ||
      language(getLocaleFromNavigator()),
  );

  init({ initialLocale: locale_ });

  // ...
}

// ...

// Extract the "en" bit from fully qualified
// locales, like "en-US"
function language(locale) {
  return locale.replace("_", "-").split("-")[0];
}

// Check to see if the given locale is supported
// by our app. If it isn't, return our app's 
// configured fallback locale.
function supported(locale) {
  if (Object.keys(locales).includes(locale)) {
    return locale;
  } else {
    return fallbackLocale;
  }
}

// ...

setupI18n теперь можно вызывать без каких-либо параметров, что приводит к срабатыванию автоопределения. Мы используем стратегию getLocaleFromNavigator() svelte-i18n, которая извлекает локаль с наивысшим приоритетом, которую пользователь настроил в настройках своего браузера.

Давайте воспользуемся автоопределением в нашем компоненте App.

<script>
// ...

   setupI18n();
</script>

{#if $isLocaleLoaded}
  <!-- ... -->

  <!-- This stays the same -->
  <LocaleSelector
    value={$locale}
    on:locale-changed={e =>
      setupI18n({ withLocale: e.detail }) }
  />
{:else}
  <!-- ... -->
{/if}

<!-- ... -->

Мы вызываем setupI18n() без параметров при инициализации нашего App, в результате чего svelte-i18n автоматически определяет язык пользователя при первой загрузке. Если пользователь вручную выбирает язык через наш LocaleSelector, мы вызываем setupI18n({ withLocale: "fr" }), как и раньше, чтобы явно установить активную локаль.

Обновление нашего языкового селектора

Давайте реорганизуем наш LocaleSelector в #each по нашим настроенным поддерживаемым локалям вместо использования жестко закодированных магических значений.

<script>
 import { locales } from "../../config/l10n";

 // ...
</script>

<!-- ... -->

<div class="locale-selector">
  <div class="select">
    <select value={value} on:change={handleLocaleChange}>
      {#each Object.keys(locales) as locale}
        <option value={locale}>{locales[locale]}</option>
      {/each}
    </select>
  </div>
</div>

Направление макета: слева направо и справа налево

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

import { get, derived, writable } from "svelte/store";

// ...

const dir = derived(locale, ($locale) =>
  $locale === "ar" ? "rtl" : "ltr",
);

// ...

export { _, setupI18n, isLocaleLoaded, locale, dir };

Локализация направления документа

Теперь мы можем реагировать на $dir в нашем приложении и соответствующим образом устанавливать направление HTML document.

<script>
  import {
    setupI18n,
    isLocaleLoaded,
    locale,
    dir,
  } from "./services/i18n";
  // ...

  setupI18n();

  $: if (document.dir !== $dir) {
    document.dir = $dir;
  }
</script>

<!-- ... -->

📖 Go Deeper » Синтаксис $: объявляет реактивный оператор Svelte, о котором вы можете прочитать больше в официальном руководстве.

Загрузка CSS в зависимости от направления

Наши стили часто значительно различаются между макетами LTR и RTL. В случае с нашим демо-приложением мы используем фреймворк Bulma CSS. Bulma предоставляет версию LTR по умолчанию, а также специальную версию RTL.

У нас есть Bulma CSS <link>ed в качестве первой таблицы стилей в нашем документе <head>.

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
    <title>Svelte app</title>

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
    />
    
    <!- ... -->
  </head>

  <body></body>
</html>

Однако мы хотим иметь возможность условно выбирать между bulma.min.css и bulma-rtl.min.css в зависимости от направления активной локали. Для этого давайте зададим URL-адрес файла в соответствующем теге <link> с помощью JavaScript, а не жестко запрограммируем его.

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
    <title>Svelte app</title>

    <!-- We give our link tag an id so we can 
         reference it in our JavaScript -->
    <link id="bulmaCssLink" rel="stylesheet" />
    
    <!- ... -->
  </head>

  <body></body>
</html>

🗒 Примечание » svelte-i18n автоматически переопределяет атрибут html[lang], чтобы мы соответствовали его активному $locale.

Теперь давайте отреагируем на изменения направления макета в App.svelte и обновим URL-адрес <link>, чтобы он соответствовал направлению.

<script>
  import {
    setupI18n,
    isLocaleLoaded,
    locale,
    dir,
  } from "./services/i18n";
  import { bulmaUrl } from "./services/css";

  // ...

  setupI18n();

  $: if (document.dir !== $dir) {
    document.dir = $dir;

    document.getElementById("bulmaCssLink").href =
      bulmaUrl($dir);
  }
</script>

<!-- ... -->

Мы используем вспомогательную функцию bumlaUrl(), чтобы получить файл CSS, соответствующий текущему направлению.

function bulmaUrl(dir = "ltr") {
  const suffix = dir == "rtl" ? "-rtl" : "";

  return (
    "https://cdn.jsdelivr.net/npm/[email protected]/css/" +
    `bulma${suffix}.min.css`
  );
}

export { bulmaUrl };

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

Локализация названия документа

Наш HTML-документ <title> часто нуждается в локализации. Мы можем легко сделать это с помощью другого реактивного оператора.

<script>
  import {
    setupI18n,
    isLocaleLoaded,
    locale,
    dir,
    _,
  } from "./services/i18n";
 
  // ...

  $: if ($isLocaleLoaded) {
    document.title = $_("app_title");
  }
</script>

<!-- ... -->

Сообщения о переводе

Мы поцарапали поверхность того, что возможно с сообщениями перевода svelte-i18n. Тем не менее, мы можем сделать гораздо больше, чем мы видели до сих пор.

По сути, svelte-i18n — это легкая оболочка Svelte вокруг библиотеки FormatJS, поэтому она обеспечивает поддержку сообщений ICU.

🔗Ресурс »Узнайте больше о том, как svelte-i18n обрабатывает форматирование сообщений, в официальном документе по форматированию. А если вам нужно подробное руководство по формату сообщений ICU, ознакомьтесь с нашей статьей об этом Отсутствующее руководство по формату сообщений ICU. Обратите внимание, что для использования svelte-i18n не обязательно понимать формат ICU. Формат интуитивно понятен.

Давайте еще раз взглянем на наш компонент Header.

<script>
  import { _ } from "svelte-i18n";
</script>

<header class="hero">
    <div class="hero-body">
        <div class="container">
            <h1 class="title">{$_("app_title")}</h1>

            <h2 class="subtitle">{$_("app_slogan")}</h2>
        </div>
    </div>
</header>

Вы помните, что наши вызовы $_() вводятся в наши словари сообщений перевода.

{
  "app_title": "Rebel Voter",
  "app_slogan": "Your favorite Star Wars characters"
}
{
  "app_title": "الناخب المتمرد",
  "app_slogan": "شخصياتك المفضلة في حرب النجوم"
}

Вложение сообщений перевода

Мы также можем группировать сообщения в наших словарях перевода.

// In our dictionary, we nest messages under a namespace
{
  "app": {
    "title": "Rebel Voter",
    "slogan": "Your favorite Star Wars characters" 
  }
}

// In our component, we use dot notations to refine into
// the dictionary
<h1 class="title">{$_("app.title")}</h1>

<h2 class="subtitle">{$_("app.slogan")}</h2>

Интерполяция

Часто мы хотим ввести динамическое значение в сообщение перевода. Мы можем сделать это с синтаксисом {variable}.

// In our dictionary
{
  "hello_user": "Hello, {name}!"
}

// In our component
<p>{$_("hello_user", {values: {name: "Adam"}})</p>

Второй параметр $_() — это объект опций, который может содержать сам объект values. values содержит пары имя/значение, где значения заменят соответствующие именованные заполнители во время выполнения.

✋🏽 Внимание » Если вы объявляете интерполированное значение, например {name}, в сообщении о переводе, вы не должныпредоставлять соответствующую пару имя/значение при вызове $_(). В противном случае svelte-i18n выдаст ошибку, и ваше приложение выйдет из строя.

🔗Ресурс »Ознакомьтесь с официальной документацией svelte-i18n, чтобы узнать обо всех параметрах, которые предоставляет $_().

Использование HTML в сообщениях перевода

Иногда нам нужно иметь HTML внутри наших переводческих сообщений. svelte-i18n не поддерживает это из коробки. Однако мы можем использовать интерполяцию и небезопасную директиву @html Svelte, чтобы обойти это.

Например, в нашем Footer мы можем захотеть иметь ссылки как часть нашего сообщения о переводе. Мы можем добавить интерполированные значения, такие как {phraseUrl}, чтобы вставлять URL-адреса во время выполнения, сохраняя при этом HTML-код ссылки в самом сообщении.

{
  // ...

  "footer": "Companion to a <a href=\"{phraseUrl}\">Phrase blog</a> article. Made with <a href=\"{svelteUrl}\">Svelte</a> &amp; <a href=\"{bulmaUrl}\">Bulma</a>."
}
{
  // ...

  "footer": "ملحق لمقال <a href=\"{phraseUrl}\">مدونة Phrase</a>. صنع بواسطة <a href=\"{svelteUrl}\">Svelte</a> و <a href=\"{bulmaUrl}\">Bulma</a>."
}

Затем в нашем компоненте Footer мы можем назначить URL-адреса как обычные values, которые мы передаем $_().

<script>
  import { _ } from "../../services/i18n";
</script>

<!-- ... -->

<footer class="footer">
  <div class="content has-text-centered">
    <p>
      {@html $_("footer", { values: {
        phraseUrl: "https://phrase.com/blog",
        svelteUrl: "https://svelte.dev/",
        bulmaUrl: "https://bulma.io/s" }})}
    </p>
  </div>
</footer>

Здесь мы используем специальную директиву Svelte @html. Обычно Svelte экранирует символы HTML, когда мы выводим их с помощью {}. @html говорит Svelte пропустить шаг экранирования и просто буквально выводить символы HTML. Это то, что нам нужно здесь, так как у нас есть <a> тегов в нашем сообщении о переводе.

✋🏽Осторожно »Будьте осторожны с @html, так как Svelte не будет очищать вывод от XSS-атак, когда вы его используете.

Использование глобального CSS для стилизации элементов в сообщениях

В приведенном выше коде, когда мы переместили наши теги <a> внутрь нашего сообщения о переводе, мы фактически создали проблему — мы потеряли наши пользовательские стили CSS для наших тегов <a>.

Это связано с тем, что Svelte обычно ограничивает селекторы <style> так, чтобы они не просачивались за пределы связанного с ними компонента. Чтобы проиллюстрировать это, давайте взглянем на наш компонент Footer до того, как мы его локализовали.

<style>
  .footer {
    margin-top: 2rem;
    background-color: #4a4a4a;
    color: #f7f7f7;
  }

  a, a:active, a:hover, a:visited {
    color: #65b6e3;
  }
</style>

<footer class="footer">
  <div class="content has-text-centered">
    <p>
      Companion to a
      <a href="https://phrase.com/blog">Phrase blog
      </a> article. Made with
      <a href="https://svelte.dev/">Svelte</a> &amp;
      <a href="https://bulma.io/">Bulma</a>.
    </p>
  </div>
</footer>

Предыдущий код на самом деле скомпилирован Svelte, чтобы во время выполнения он выглядел примерно так:

<!-- In our app's CSS bundle -->
<style>
.footer.svelte-ub7hie {
  margin-top: 2rem;
  background-color: #4a4a4a;
  color: #f7f7f7;
}

a.svelte-ub7hie,
a.svelte-ub7hie:active,
a.svelte-ub7hie:hover,
a.svelte-ub7hie:visited {
    color: #65b6e3;
}
</style>

<!-- Our rendered component -->
<footer class="footer svelte-ub7hie">
  <div class="content has-text-centered">
    <p>Companion to a
      <a href="https://phrase.com/blog" class="svelte-ub7hie">Phrase blog</a> article. Made with <a href="https://svelte.dev/" class="svelte-ub7hie">Svelte</a> &amp; <a href="https://bulma.io/" class="svelte-ub7hie">Bulma</a>
    </p>
  </div>
</footer>

Обратите внимание, что Svelte присвоил стилизованным HTML-элементам нашего компонента специальный хеш, ub7hie. Это относится к селекторам <style>, которые мы предоставили нашему компоненту. В большинстве случаев это здорово. Однако, если мы переместим HTML из нашего компонента и вставим его динамически во время выполнения, Svelte не добавит этот хэш во внедренный HTML. Таким образом, правила CSS a.svelte-ub7hie не будут нацелены на HTML внутри наших сообщений перевода.

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

<script>
  import { _ } from "../../services/i18n";
</script>

<style>
  .footer {
    margin-top: 2rem;
    background-color: #4a4a4a;
    color: #f7f7f7;
  }

  .footer :global(a),
  .footer :global(a:active),
  .footer :global(a:hover),
  .footer :global(a:visited) {
    color: #65b6e3;
  }
</style>

<footer class="footer">
  <div class="content has-text-centered">
    <p>
      {@html $_("footer", { values: {
        phraseUrl: "https://phrase.com/blog",
        svelteUrl: "https://svelte.dev/",
        bulmaUrl: "https://bulma.io/s" }})}
    </p>
  </div>
</footer>

При этом хэш области видимости не будет добавлен в селекторы стилей. Это означает, что наш CSS будет отображаться как просто .footer a. Наши ссылки в нижнем колонтитуле теперь будут соответствовать селекторам и отображаться точно так же, как до того, как мы локализовали компонент.

множественное число

Разные языки имеют разные правила множественного числа, поэтому приятно, когда библиотека i18n знает об этих языковых особенностях. К счастью, svelte-i18n, созданный поверх FormatJS, очень хорошо знает об этих множественных правилах.

Давайте воспользуемся этой возможностью для локализации нашего компонента VotingButton. Мы добавим метку, показывающую общее количество голосов, плюсы + минусы.

<script>
  // ...

  export let character;

  // ...

  let {
    upVoteCount,
    downVoteCount,
  } = character;

  $: totalVoteCount = upVoteCount + downVoteCount;
</script>

<!-- ... -->

<div class="box">
  <div class="columns is-mobile">
    <!-- ... -->

    <div class="column">
      <!-- ... -->


      <p class="is-size-7">{totalVoteCount} votes</p>
    </div>
  </div>
</div>

Текст общего количества голосов в настоящее время жестко запрограммирован. Давайте локализуем его. Чтобы добавить множественное число к сообщению перевода в svelte-i18n, мы используем формат сообщения ICU.

{
  // ...

  "total_votes": "{n, plural, =0 {No votes yet} one {# vote} other {# votes}}",

  // ...
}
{
  // ...

  "total_votes": "{n, plural, =0 {لا توجد أصوات بعد} one {صوت #} two {صوتان} few {# أصوات} other {# صوت}}",

  // ...
}

Обернутое в {} выражение во множественном числе объявляет переменную count n. Мы можем ссылаться на n в наших сообщениях, используя специальный символ #.

Внутри выражения мы можем иметь столько пар правило/сообщение, сколько захотим. Существуют именованные правила, которые являются общими для разных языков. Например, в английском языке есть два именованных правила множественного числа: one и other. В арабском языке шесть правил множественного числа. Нам не обязательно использовать все правила множественного числа для языка, но всегда требуется правило other.

Если мы хотим указать определенное количество, например 13, мы можем использовать синтаксис =13 в нашем множественном выражении. Мы сделали это с помощью правила =0 выше. (В этом случае мы могли бы также использовать именованное правило zero.)

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

<!-- ... -->

<div class="box">
  <div class="columns is-mobile">
    <!-- ... -->

    <div class="column">
      <!-- ... -->

      <p class="is-size-7">
        {$_("total_votes", {values: {n: totalVoteCount}})}
      </p>
    </div>
  </div>
</div>

Это заставит svelte-i18n выводить соответствующее сообщение во множественном числе для нашей активной локали во время выполнения. Значение n используется для выбора, и его значение заменяется на символ # в наших сообщениях.

🔗 Ресурс »Узнайте больше о правилах множественного числа сообщений ICU в Руководстве по FormatJS и в нашей специальной статье Отсутствующее руководство по формату сообщений ICU.

Форматирование даты

В нашем компоненте Character в настоящее время мы отображаем даты выхода фильмов «Звездных войн» точно так же, как мы получаем их в данных JSON.

Мы можем локализовать эти даты и указать для них некоторые правила форматирования, используя реактивное хранилище $date svelte-i18n.

Давайте откроем хранилище в нашей библиотеке-оболочке.

// ...
import {
  _,
  date,
  init,
  locale,
  dictionary,
  addMessages,
  getLocaleFromNavigator,
} from "svelte-i18n";

// ...

export {
  _,
  dir,
  date,
  locale,
  setupI18n,
  isLocaleLoaded,
};

Теперь давайте воспользуемся хранилищем в нашем компоненте Character для локализации наших дат.

<script>
  import { _, date } from "../../services/i18n";
  
  // ...
</script>

<!-- ... -->

<div class="box">
  <div class="columns is-mobile">
    <!-- ... -->

    <div class="column">
      <!-- ... -->

      <p class="first-appeared">
        <!-- ... -->

        {$date(
          new Date(firstAppearedInFilm.releasedAt),
          { format: "medium" }
        )}
      </p>

      <!-- ... -->
    </div>
  </div>
</div>

$date принимает объект JavaScript Date, поэтому мы анализируем нашу строку даты в одну, прежде чем передать ее. Хранилище локализует данную дату в активном языковом стандарте и реагирует на изменение языкового стандарта, заставляя вывод даты повторно отображаться. $date принимает второй необязательный аргумент, который может содержать один из предустановленных formats. Здесь мы выбрали средний формат.

🔗Ресурс »Все доступные вам форматы даты находятся в официальной документации svelte-i18n.

🗒 Примечание » Если вам нужно настраиваемое форматирование даты помимо того, что дают вам предустановленные форматы, вы можете получить прямой доступ к средству форматирования даты, чтобы предоставить настраиваемые форматы.

С учетом этого мы локализовали все наше демо-приложение 😉

🔗 Ресурс »Вы можете получить весь код для завершенного демо на Github.

Статьи по Теме

У нас есть больше вводных и подробных сведений о JavaScript i18n/l10n для вашего удовольствия.

Мир вне

Мы надеемся, что вам понравилось это пошаговое руководство по локализации приложения Svelte с помощью svelte-i18n. Мы что-то пропустили? Есть ли что-то, о чем вы хотите узнать больше? Дайте нам знать в комментариях ниже.

И если вы хотите вывести свою игру локализации на новый уровень, взгляните на Phrase. Профессиональное решение для локализации, Phrase предлагает гибкий API, интерфейс командной строки и отличную веб-консоль для переводчиков. Автоматическая синхронизация GitHub, Bitbucket и GitLab обеспечивает беспрепятственную передачу файлов перевода между вами и вашей командой переводчиков. А беспроводные переводы для мобильных приложений означают, что больше не нужно ждать обзоров в App Store, чтобы добавить новые переводы. Ознакомьтесь со всеми возможностями Phrase и зарегистрируйтесь для получения бесплатной 14-дневной пробной версии.

Первоначально опубликовано в The Phrase Blog.