В процессе локализации переводятся не только основные элементы интерфейса, но и многочисленные вспомогательные сообщения:
empty state);В приложениях с поддержкой нескольких языков такие строки быстро
превращаются в один из самых крупных слоёв локализации. Библиотека
FormatJS предоставляет единый механизм работы с подобными текстами через
ICU MessageFormat и API react-intl.
Во многих проектах вспомогательные тексты хранятся прямо внутри компонентов:
<input placeholder="Введите email" />
При масштабировании возникают проблемы:
FormatJS решает эту проблему через декларативные сообщения.
npm install react-intl
// locales/ru.js
export default {
'form.email.placeholder': 'Введите email',
}
// locales/en.js
export default {
'form.email.placeholder': 'Enter email',
}
import { IntlProvider } from 'react-intl'
import messages from './locales/ru'
<IntlProvider locale="ru" messages={messages}>
<App />
</IntlProvider>
import { useIntl } from 'react-intl'
function EmailField() {
const intl = useIntl()
return (
<input
placeholder={intl.formatMessage({
id: 'form.email.placeholder',
})}
/>
)
}
Вспомогательные подписи особенно часто используются в формах.
export default {
'password.helper':
'Пароль должен содержать минимум 8 символов',
}
function PasswordHelper() {
const intl = useIntl()
return (
<small>
{intl.formatMessage({
id: 'password.helper',
})}
</small>
)
}
При большом количестве вспомогательных текстов рекомендуется выносить сообщения в отдельные структуры.
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>
</>
)
}
Все строки находятся в одном месте.
FormatJS CLI умеет автоматически извлекать сообщения:
formatjs extract "src/**/*.{js,jsx,ts,tsx}"
Даже при отсутствии перевода интерфейс остаётся работоспособным.
const messages = defineMessages({
searchPlaceholder: {
id: 'search.placeholder',
defaultMessage: 'Search...',
description:
'Placeholder inside global search input',
},
})
Описание помогает переводчикам понимать назначение строки.
Tooltip часто содержит короткие контекстные сообщения.
export default {
'profile.edit.tooltip':
'Редактировать профиль',
}
<button
title={intl.formatMessage({
id: 'profile.edit.tooltip',
})}
>
Edit
</button>
Для вспомогательных текстов обычно применяется
formatMessage, но иногда удобен и компонент
FormattedMessage.
import { FormattedMessage } from 'react-intl'
<small>
<FormattedMessage id="form.required" />
</small>
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,
}
)
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,
}
)
export default {
'notifications.empty':
'Уведомлений пока нет',
}
function EmptyNotifications() {
return (
<div>
<FormattedMessage id="notifications.empty" />
</div>
)
}
export default {
'loading.data': 'Загрузка данных...',
}
<p>
{intl.formatMessage({
id: 'loading.data',
})}
</p>
В крупных приложениях вспомогательные тексты удобно разделять по областям.
src/
locales/
ru/
auth.js
profile.js
validation.js
dashboard.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
<TextField
placeholder={intl.formatMessage({
id: 'search.placeholder',
})}
/>
<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
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>
export default {
'toast.saved':
'Изменения успешно сохранены',
}
toast.success(
intl.formatMessage({
id: 'toast.saved',
})
)
setError(error.message)
Backend-сообщения могут быть:
const errorMessages = {
EMAIL_EXISTS: 'api.email.exists',
INVALID_PASSWORD: 'api.invalid.password',
}
intl.formatMessage({
id: errorMessages[error.code],
})
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 минут назад
'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]
)
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>
Для больших приложений переводы загружаются динамически.
async function loadLocale(locale) {
const messages = await import(
`./locales/${locale}.js`
)
return messages.default
}
type MessageIds =
| 'auth.email.placeholder'
| 'auth.password.placeholder'
| 'validation.required'
function t(id: MessageIds) {
return intl.formatMessage({ id })
}
export function t(intl, id, values) {
return intl.formatMessage(
{ id },
values
)
}
placeholder={t(
intl,
'search.placeholder'
)}
placeholder="Search"
intl.formatMessage({
id: 'Введите email',
})
{
id: 'search.placeholder'
}
{
'profile.title': 'User profile'
}
при русской локали.
Плохой вариант:
placeholder1
placeholder2
message3
Хороший вариант:
auth.email.placeholder
validation.required
profile.tooltip.edit
Полезно разделять:
Во всём проекте должен использоваться одинаковый формат 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
FormatJS валидирует plural-формы и ICU-выражения во время сборки.
MISSING_OTHER_CLAUSE
Ошибка означает отсутствие блока other в
plural-конструкции.
При локализации вспомогательных текстов для арабского или иврита необходимо учитывать:
FormatJS корректно работает с RTL-локалями при правильной настройке интерфейса.