Организация текстов интерфейса

Интерфейс любого сайта включает в себя не только визуальные компоненты, но и текст. Это могут быть названия кнопок, пунктов меню, сообщения об ошибках в форме, различные тексты, разбросанные по всему сайту.

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

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

При правильном подходе, подобные тексты хранятся в одном месте отдельно от кода. Такой способ значительно упрощает приложение:

  • Текстами проще управлять, выполнять массовое обновление, отслеживать то, что устарело.
  • Это могут делать не только программисты. Более того, тексты можно выгружать во внешние системы, которые дают возможность работать с ними множеству людей (об этом ниже).
  • Упрощается интернационализация и локализация.

Так как организовать хранение текстов? Ответ для многих программистов может показаться неожиданным. Даже если ваш сайт не собирается быть мультиязычным, для работы с текстами всё равно используют i18n-библиотеки. i18n – расшифровывается как интернационализация (internationalization). Этим термином в программировании обозначают всё, что связано с переводами. Как правило, речь идёт про специальные библиотеки, которые позволяют переводить интерфейсы, оставляя код приложения простым.

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

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

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

В мире JS наиболее популярной библиотекой для работы с текстами стала i18next. Это не просто библиотека, а целый фреймворк, имеющий интеграции со всеми популярными решениями, такими как Angular, React или Vue.js. Пример использования:

import i18next from 'i18next';
// Инициализация, выполняется ровно один раз в асинхронной функции, запускающей приложение
// Меняет объект i18next глобально
await i18next.init({
  lng: 'ru', // Текущий язык
  debug: true,
  resources: {
    ru: { // Тексты конкретного языка
      translation: { // Так называемый namespace по умолчанию
        key: 'Привет мир!',
      },
    },
  },
});

// Где-то в коде приложения обращаемся к ключу (key)
// Библиотека по умолчанию ищет так: <текущий язык>.translation.<ключ> => ru.translation.key 
i18next.t('key'); // "Привет мир!"

Единственное место где появляется понятие "язык" — это инициализация. Нужно указать текущий язык (lng) и добавить тексты для этого языка. На этом всё: дальше мы только управляем текстами. Если появляется новый текст, то для него придумывается ключ и добавляется в объект translation. Затем этот текст извлекается по указанному ключу. Из кода выше видно, что этот текст очень легко переиспользовать. Достаточно обратиться к этому же ключу в другом месте программы.

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

import i18next from 'i18next';
// Просто пример. Структура может быть любой.
import ru from './locales/ru.js';

await i18next.init({
  lng: 'ru',
  debug: true,
  resources: {
    ru,
  },
});

В принципе, выносить тексты лучше сразу. Их никогда не бывает мало.

i18next поддерживает такое понятие как "бэкенды". Она позволяет загружать тексты из внешних источников, например, через AJAX-запрос. Подробнее в официальной документации.

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

{
  translation: {
    key: 'Привет мир!',
    signUpForm: {
      name: 'Имя',
      email: 'Email',
    }
  }
}

i18next.t('signUpForm.name'); // Имя
i18next.t('signUpForm.email'); // Email

В некоторых ситуациях тексты зависят от различных динамических параметров, например, от имени пользователя. В таком случае используется встроенная интерполяция:

{
  translation: {
    greeting: 'Привет {{name}}!',
  }
}

i18next.t('greeting', { name: 'Иван' }); // "Привет Иван!"

В более сложных ситуациях одной интерполяции недостаточно. Представьте себе, что нам надо выводить количество баллов, как на Хекслете. Слово "балл" будет меняться в зависимости от числа баллов: 1 балл, 2 балла, 10 баллов. Как это сделать? С помощью плюрализации!

{
  translation: {
    { // Интерполяция не обязательна, зависит от задачи
      // Наименования ключей не соответствуют конкретным числам
      // Они обозначают разные группы чисел https://jsfiddle.net/sm9wgLze
      key_0: '{{count}} балл',
      key_1: '{{count}} балла',
      key_2: '{{count}} баллов',
    }
  }
}

i18next.t('key', { count: 0 }); // "баллов"
i18next.t('key', { count: 1 }); // "балл"
i18next.t('key', { count: 2 }); // "балла"
i18next.t('key', { count: 5 }); // "баллов"

Связь текстов с состоянием приложения

Типичная ошибка при работе с текстами – хранить их прямо в состоянии:

// Ошибки – это всего лишь один из возможных примеров
// То же самое касается любых других текстов
if (!isEmailUnique) {
  state.signUpForm.errors.email = i18next.t('signUpForm.errors.email.notUnique');
}

У такого подхода есть один очень серьезный недостаток. Он не сочетается с переключением языков. Представьте, что пользователь поменял язык интерфейса, а в состоянии в это время записаны тексты. Появляется проблема – как изменить тексты на правильный язык? В общем случае никак, потому что в строке текста нет информации о том, что это было. То есть невозможно сопоставить этот текст с ключом и найти соответствующий перевод в другом месте. Кроме того, сама задача очень непростая, текстов может быть много, они разбросаны по разным частям состояния. Придется писать специальную логику под каждую конкретную ситуацию (каждый конкретный кусок состояния).

Любые тексты, которые выводятся в зависимости от действий пользователя, не должны храниться в состоянии приложения. Эти тексты должны зависеть от состояния процессов:

// Где-то в представлении (View)
if (state.registrationProcess.finished) {
  div.innerHTML = i18next.t('registration.success');
}

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

// В файле переводов:
{
  translation: {
    key: 'Привет мир!',
    signUpForm: {
      name: 'Имя',
      email: 'Email',
      errors: [/* тут переводы ошибок */]
    }
  }
}

// Где-то в приложении
const state = {
  signUpForm: {
    valid: false,
    errors: {},
  }
};

// Где-то в обработчике
if (!isEmailUnique) {
  state.signUpForm.errors.email = 'signUpForm.errors.email.notUnique';
}

// Где-то во вью
div.innerHTML = i18next.t(state.signUpForm.errors.email);

В любом случае, готовые строки формируются только при выводе.

Для полного доступа к курсу, нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам Хекслета, даст возможность обращаться за помощью к менторам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».