Локализация вспомогательных текстов

В процессе локализации переводятся не только основные элементы интерфейса, но и многочисленные вспомогательные сообщения:

  • placeholder в полях ввода;
  • tooltip-подсказки;
  • aria-label для accessibility;
  • helper text;
  • уведомления об ошибках;
  • тексты загрузки;
  • служебные подписи;
  • сообщения валидации;
  • заголовки модальных окон;
  • пустые состояния (empty state);
  • подсказки для onboarding.

В приложениях с поддержкой нескольких языков такие строки быстро превращаются в один из самых крупных слоёв локализации. Библиотека FormatJS предоставляет единый механизм работы с подобными текстами через ICU MessageFormat и API react-intl.


Организация вспомогательных сообщений

Типичная проблема

Во многих проектах вспомогательные тексты хранятся прямо внутри компонентов:

<input placeholder="Введите email" />

При масштабировании возникают проблемы:

  • строки невозможно переиспользовать;
  • отсутствует централизованная локализация;
  • усложняется поддержка переводов;
  • возникают дубли;
  • переводчики не понимают контекст.

FormatJS решает эту проблему через декларативные сообщения.


Базовая локализация placeholder

Исходная конфигурация

npm install react-intl

Создание переводов

// locales/ru.js
export default {
  'form.email.placeholder': 'Введите email',
}
// locales/en.js
export default {
  'form.email.placeholder': 'Enter email',
}

Подключение IntlProvider

import { IntlProvider } from 'react-intl'
import messages from './locales/ru'

<IntlProvider locale="ru" messages={messages}>
  <App />
</IntlProvider>

Использование useIntl

import { useIntl } from 'react-intl'

function EmailField() {
  const intl = useIntl()

  return (
    <input
      placeholder={intl.formatMessage({
        id: 'form.email.placeholder',
      })}
    />
  )
}

Локализация helper text

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

Пример

export default {
  'password.helper':
    'Пароль должен содержать минимум 8 символов',
}
function PasswordHelper() {
  const intl = useIntl()

  return (
    <small>
      {intl.formatMessage({
        id: 'password.helper',
      })}
    </small>
  )
}

Использование defineMessages

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

Пример

import { defineMessages, useIntl } from 'react-intl'

const messages = defineMessages({
  emailPlaceholder: {
    id: 'form.email.placeholder',
    defaultMessage: 'Enter email',
  },

  passwordHelper: {
    id: 'password.helper',
    defaultMessage:
      'Password must contain at least 8 characters',
  },
})

Использование сообщений

function AuthForm() {
  const intl = useIntl()

  return (
    <>
      <input
        placeholder={intl.formatMessage(
          messages.emailPlaceholder
        )}
      />

      <small>
        {intl.formatMessage(
          messages.passwordHelper
        )}
      </small>
    </>
  )
}

Преимущества defineMessages

Централизованное хранение

Все строки находятся в одном месте.


Удобная экстракция переводов

FormatJS CLI умеет автоматически извлекать сообщения:

formatjs extract "src/**/*.{js,jsx,ts,tsx}"

Наличие defaultMessage

Даже при отсутствии перевода интерфейс остаётся работоспособным.


Поддержка описаний

const messages = defineMessages({
  searchPlaceholder: {
    id: 'search.placeholder',
    defaultMessage: 'Search...',
    description:
      'Placeholder inside global search input',
  },
})

Описание помогает переводчикам понимать назначение строки.


Локализация tooltip

Tooltip часто содержит короткие контекстные сообщения.

Пример

export default {
  'profile.edit.tooltip':
    'Редактировать профиль',
}
<button
  title={intl.formatMessage({
    id: 'profile.edit.tooltip',
  })}
>
  Edit
</button>

Использование FormattedMessage

Для вспомогательных текстов обычно применяется formatMessage, но иногда удобен и компонент FormattedMessage.

import { FormattedMessage } from 'react-intl'

<small>
  <FormattedMessage id="form.required" />
</small>

Локализация aria-label

Accessibility-тексты должны переводиться так же, как и обычный интерфейс.

Пример

export default {
  'modal.close': 'Закрыть окно',
}
<button
  aria-label={intl.formatMessage({
    id: 'modal.close',
  })}
>
  ×
</button>

Локализация сообщений валидации

Простые ошибки

export default {
  'validation.required':
    'Поле обязательно',
}
{
  intl.formatMessage({
    id: 'validation.required',
  })
}

Параметризованные сообщения

Validation-сообщения часто содержат динамические значения.

Пример минимальной длины

export default {
  'validation.minLength':
    'Минимальная длина — {count} символов',
}
intl.formatMessage(
  {
    id: 'validation.minLength',
  },
  {
    count: 8,
  }
)

ICU MessageFormat

FormatJS основан на ICU-синтаксисе.

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

'validation.range':
  'Введите значение от {min} до {max}'
intl.formatMessage(
  {
    id: 'validation.range',
  },
  {
    min: 1,
    max: 10,
  }
)

Множественные формы

Для русского языка особенно важны plural-формы.

Пример

'files.count':
  '{count, plural, ' +
  '=0 {Нет файлов} ' +
  'one {# файл} ' +
  'few {# файла} ' +
  'many {# файлов} ' +
  'other {# файла}}'
intl.formatMessage(
  {
    id: 'files.count',
  },
  {
    count: files.length,
  }
)

Локализация empty state

Пример

export default {
  'notifications.empty':
    'Уведомлений пока нет',
}
function EmptyNotifications() {
  return (
    <div>
      <FormattedMessage id="notifications.empty" />
    </div>
  )
}

Локализация loading-сообщений

Пример

export default {
  'loading.data': 'Загрузка данных...',
}
<p>
  {intl.formatMessage({
    id: 'loading.data',
  })}
</p>

Разделение сообщений по доменам

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

Пример структуры

src/
  locales/
    ru/
      auth.js
      profile.js
      validation.js
      dashboard.js

Пример auth.js

export default {
  'auth.email.placeholder':
    'Введите email',

  'auth.password.placeholder':
    'Введите пароль',

  'auth.login.button':
    'Войти',
}

Пространства имён

Идентификаторы должны быть предсказуемыми.

Рекомендуемый формат

section.component.element.type

Примеры

auth.email.placeholder
auth.password.helper
profile.avatar.tooltip
settings.theme.description
validation.required

Локализация placeholder с React-компонентами UI-библиотек

Material UI

<TextField
  placeholder={intl.formatMessage({
    id: 'search.placeholder',
  })}
/>

Ant Design

<Input
  placeholder={intl.formatMessage({
    id: 'search.placeholder',
  })}
/>

Избежание дублирования

Плохой вариант

'search.placeholder.header'
'search.placeholder.sidebar'
'search.placeholder.modal'

Если текст одинаковый, достаточно одного сообщения:

'search.placeholder'

Контекстные различия

Иногда одинаковая строка имеет разный смысл.

Пример

Слово «Close» может означать:

  • закрыть окно;
  • приблизиться;
  • завершить соединение.

В таких случаях нужны разные идентификаторы:

modal.close.button
map.zoom.close
connection.close

Локализация Markdown-подобных подсказок

FormatJS умеет вставлять React-элементы внутрь сообщений.

Пример

export default {
  'terms.notice':
    'Прочитайте <link>условия использования</link>',
}
<FormattedMessage
  id="terms.notice"
  values={{
    link: chunks => (
      <a href="/terms">{chunks}</a>
    ),
  }}
/>

Многострочные вспомогательные тексты

Пример

export default {
  'upload.instructions':
    'Перетащите файл сюда\nили нажмите кнопку загрузки',
}

Отображение

<div style={{ whiteSpace: 'pre-line' }}>
  {intl.formatMessage({
    id: 'upload.instructions',
  })}
</div>

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

Toast-сообщения

export default {
  'toast.saved':
    'Изменения успешно сохранены',
}
toast.success(
  intl.formatMessage({
    id: 'toast.saved',
  })
)

Локализация ошибок API

Нежелательный вариант

setError(error.message)

Backend-сообщения могут быть:

  • не локализованы;
  • небезопасны;
  • непонятны пользователю.

Рекомендуемый подход

const errorMessages = {
  EMAIL_EXISTS: 'api.email.exists',
  INVALID_PASSWORD: 'api.invalid.password',
}
intl.formatMessage({
  id: errorMessages[error.code],
})

fallback-механизмы

FormatJS поддерживает fallback через defaultMessage.

Пример

intl.formatMessage({
  id: 'profile.status',
  defaultMessage: 'Active',
})

Если перевод отсутствует, будет показан defaultMessage.


Форматирование дат во вспомогательных текстах

Пример

'profile.lastSeen':
  'Последний вход: {date}'
intl.formatMessage(
  {
    id: 'profile.lastSeen',
  },
  {
    date: intl.formatDate(lastSeen, {
      day: 'numeric',
      month: 'long',
    }),
  }
)

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

Пример

'storage.used':
  'Использовано {size} ГБ'
intl.formatMessage(
  {
    id: 'storage.used',
  },
  {
    size: intl.formatNumber(12.5),
  }
)

Локализация относительного времени

Пример

intl.formatRelativeTime(-5, 'minute')

Результат:

5 минут назад

Использование Rich Text Formatting

Пример

'warning.delete':
  'Действие <b>необратимо</b>'
<FormattedMessage
  id="warning.delete"
  values={{
    b: chunks => <strong>{chunks}</strong>,
  }}
/>

Проверка отсутствующих переводов

FormatJS позволяет отслеживать missing translations.

Пример обработчика

<IntlProvider
  locale="ru"
  messages={messages}
  onEr ror={error => {
    console.error(error)
  }}
>
  <App />
</IntlProvider>

Оптимизация производительности

Частые вызовы formatMessage могут создавать лишние вычисления.

Мемоизация сообщений

const placeholder = useMemo(
  () =>
    intl.formatMessage({
      id: 'search.placeholder',
    }),
  [intl]
)

Вынесение общих текстов

Пример common.js

export default {
  'common.cancel': 'Отмена',
  'common.save': 'Сохранить',
  'common.delete': 'Удалить',
}

Локализация конфигурационных объектов

Пример

const fieldConfig = [
  {
    name: 'email',
    placeholderId:
      'auth.email.placeholder',
  },

  {
    name: 'password',
    placeholderId:
      'auth.password.placeholder',
  },
]

Использование

{
  fieldConfig.map(field => (
    <input
      key={field.name}
      placeholder={intl.formatMessage({
        id: field.placeholderId,
      })}
    />
  ))
}

Поддержка нескольких локалей

Пример структуры

const messages = {
  ru,
  en,
  de,
}
<IntlProvider
  locale={locale}
  messages={messages[locale]}
>
  <App />
</IntlProvider>

Lazy loading переводов

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

Пример

async function loadLocale(locale) {
  const messages = await import(
    `./locales/${locale}.js`
  )

  return messages.default
}

TypeScript и типизация идентификаторов

Пример

type MessageIds =
  | 'auth.email.placeholder'
  | 'auth.password.placeholder'
  | 'validation.required'

Использование

function t(id: MessageIds) {
  return intl.formatMessage({ id })
}

Централизованный helper для переводов

Пример

export function t(intl, id, values) {
  return intl.formatMessage(
    { id },
    values
  )
}

Использование

placeholder={t(
  intl,
  'search.placeholder'
)}

Частые ошибки при локализации вспомогательных текстов

Хардкод строк

placeholder="Search"

Использование текста вместо id

intl.formatMessage({
  id: 'Введите email',
})

Отсутствие defaultMessage

{
  id: 'search.placeholder'
}

Смешивание языков

{
  'profile.title': 'User profile'
}

при русской локали.


Нестабильные идентификаторы

Плохой вариант:

placeholder1
placeholder2
message3

Хороший вариант:

auth.email.placeholder
validation.required
profile.tooltip.edit

Рекомендации по архитектуре

Выделение слоёв

Полезно разделять:

  • UI-тексты;
  • validation;
  • accessibility;
  • системные уведомления;
  • API-ошибки;
  • onboarding.

Единый стиль именования

Во всём проекте должен использоваться одинаковый формат id.


Максимальная переиспользуемость

Общие сообщения должны храниться централизованно.


Отказ от строк внутри компонентов

Любой пользовательский текст должен быть локализован.


Интеграция с системой дизайна

Во многих design system вспомогательные тексты являются частью компонентов.

Пример

<FormField
  label={intl.formatMessage({
    id: 'profile.email.label',
  })}
  helperText={intl.formatMessage({
    id: 'profile.email.helper',
  })}
  errorText={intl.formatMessage({
    id: 'validation.invalid.email',
  })}
/>

Локализация динамических подсказок

Пример

'search.results':
  'Найдено {count} результатов'
intl.formatMessage(
  {
    id: 'search.results',
  },
  {
    count: resultCount,
  }
)

Подготовка переводов для переводчиков

FormatJS CLI позволяет экспортировать сообщения:

formatjs extract

Компиляция переводов

formatjs compile

Автоматическая проверка ICU-синтаксиса

FormatJS валидирует plural-формы и ICU-выражения во время сборки.

Пример ошибки

MISSING_OTHER_CLAUSE

Ошибка означает отсутствие блока other в plural-конструкции.


Поддержка RTL-языков

При локализации вспомогательных текстов для арабского или иврита необходимо учитывать:

  • направление placeholder;
  • выравнивание tooltip;
  • отображение mixed content;
  • переносы строк;
  • поведение иконок внутри input.

FormatJS корректно работает с RTL-локалями при правильной настройке интерфейса.