Словарь интерфейса и устойчивые ключи
В предыдущем уроке мы связали языковые версии статьи через один ID и сохранили русский URL. Теперь займёмся оболочкой страницы. В библиотеке «Параллель» повторяются подписи каталога, кнопки сохранения и сообщения о доступных переводах. Если записывать эти строки прямо в каждом шаблоне, изменение одного названия потребует обхода множества файлов.
Выделим словарь интерфейса: соответствие устойчивого ключа и текста на выбранном языке. Ключ будет описывать смысл сообщения, а перевод — его представление. На этом этапе используем обычные JavaScript-объекты без внешней библиотеки. Пример достаточно мал, чтобы увидеть механизм, а ограничения будут обозначены явно.
Ключ описывает действие
Не используйте русский текст как единственный идентификатор перевода. Строка «Сохранить» может относиться к статье, настройкам или изменению профиля, а в другом языке эти случаи потребуют разных выражений. Ключ bookmark.add задаёт конкретное действие с закладкой и остаётся прежним после редакционной правки подписи.
Полный начальный файл web/messages.mjs:
export const messages = {
ru: {
"nav.catalog": "Каталог",
"bookmark.add": "Сохранить статью",
"bookmark.saved": "Статья «{title}» сохранена",
"translation.missing": "Перевод этой статьи пока не готов"
},
en: {
"nav.catalog": "Catalogue",
"bookmark.add": "Save article",
"bookmark.saved": "“{title}” was saved",
"translation.missing": "This article has not been translated yet"
}
};
export function message(language, key, values = {}) {
const dictionary = messages[language];
if (!dictionary || !Object.hasOwn(dictionary, key)) {
throw new Error(`Missing message: ${language}/${key}`);
}
return dictionary[key].replace(/\{([a-zA-Z][a-zA-Z0-9_]*)\}/g,
(_, name) => {
if (!Object.hasOwn(values, name)) {
throw new Error(`Missing value: ${name}`);
}
return String(values[name]);
});
}
Функция выбирает словарь только из перечисленных языков. Проверка собственного свойства не позволяет случайно принять унаследованное имя за перевод. Если редактор ошибся в ключе, мы получим понятное сообщение с языком и именем. Такое поведение полезнее молчаливой пустой подписи, которую можно заметить лишь после публикации.
Шаблон сообщения содержит параметр {title}. Значение передаётся отдельно, поэтому переводчик может переставить его в естественное место фразы. Мы не склеиваем «Статья», название и «сохранена» из независимых переведённых кусочков: порядок слов и пунктуация принадлежат целому сообщению.
Подстановка не создаёт HTML
Статья может называться HTML <dialog>. Это корректное название с угловыми скобками, но при ошибочном вставлении через innerHTML часть строки превращается в разметку. Словарь не делает пользовательские данные безопасными сам по себе. Безопасность определяется тем, куда вставлен готовый результат.
В обработчике сохранения используем textContent. Следующий фрагмент относится к web/page.mjs; элементы с заданными ID находятся в HTML страницы:
import { message } from "./messages.mjs";
const language = document.documentElement.lang;
const button = document.querySelector("#save-article");
const status = document.querySelector("#save-status");
const title = document.querySelector("h1").textContent;
button.textContent = message(language, "bookmark.add");
button.addEventListener("click", () => {
status.textContent = message(language, "bookmark.saved", { title });
});
Ожидаемый русский текст статуса — Статья «HTML <dialog>» сохранена. Символы названия остаются текстом. Статус размещаем в элементе с role="status", чтобы средство чтения могло сообщить изменение без переноса фокуса. В этом фрагменте сохранение имитируется: настоящее хранилище закладок не является результатом урока.
Если нужен перевод со ссылкой, не разрешайте произвольный HTML в обычном сообщении. Создайте ссылку средствами DOM или шаблона, а подпись возьмите из словаря. Для сложных текстов существуют библиотеки сообщений с управляемыми вставками компонентов. Наш маленький заменитель параметров их не имитирует и не поддерживает вложенные выражения.
Пропущенный перевод и запасной язык
Можно было бы автоматически брать любую недостающую английскую подпись из русского словаря. Иногда это оправдано при разработке, но в публичном интерфейсе такая политика скрывает незавершённую работу: рядом оказываются Catalogue и «Сохранить статью». Поэтому основная функция бросает ошибку. Решение о запасном языке принимается явно на уровне страницы или редакционного режима.
Добавим отдельную функцию для списка недостающих ключей. Она не запускает тесты приложения, а описывает полезный редакционный отчёт:
export function missingKeys(language) {
const reference = Object.keys(messages.ru);
const dictionary = messages[language] ?? {};
return reference.filter(key => !Object.hasOwn(dictionary, key));
}
Если удалить bookmark.saved из английского словаря, ожидаемый список содержит именно этот ключ. Пустая строка требует отдельной политики: наличие ключа ещё не доказывает качество текста. Для нашего выпуска пустые обязательные сообщения будем считать незавершёнными. Позднее редакционный отчёт объединит пропущенные подписи и состояние перевода статей.
Ключи нужно удалять осознанно. Сначала найдите все места использования, затем уберите запись из каждого словаря. Старый ключ иногда ещё нужен на прежней странице или в сохранённом выпуске. Переименование ключа не затрагивает URL статьи, но может нарушить интерфейс, поэтому оно отличается от обычной правки формулировки.
Контекст для переводчика
Словарь должен сопровождаться пояснениями: где появляется сообщение, что означает параметр и насколько ограничено место. Для bookmark.saved укажем, что {title} — название учебной статьи, сообщение появляется после действия пользователя и должно оставаться законченным предложением. Переводчик не должен угадывать назначение параметра по английскому имени переменной.
Нельзя считать длину русского текста максимальной для всех языков. Кнопка должна допускать перенос или разумное увеличение ширины. В дальнейшем мы проверим интерфейс длинными подписями и рассмотрим направление текста. Сейчас важно, что перевод строки не требует редактирования обработчика действия.
Изменение ключа и изменение формулировки
Рассмотрим практическую правку. Редактор хочет заменить «Сохранить статью» на «Добавить в закладки». Действие осталось прежним, поэтому ключ bookmark.add сохраняется. Меняется значение русского словаря, а обработчик кнопки продолжает вызывать тот же смысловой идентификатор. Английская подпись может остаться прежней, если уже правильно выражает действие. Не каждое редакционное изменение требует синхронно менять все формулировки.
Если же кнопка теперь сохраняет весь курс для чтения без сети, прежний ключ становится неверным. Нужно новое сообщение и описание действия: время ожидания, объём загрузки и возможный отказ отличаются. Переименование нельзя выполнить одной правкой словаря, потому что пользовательская операция тоже изменилась. Это хороший пример границы ответственности между переводом и логикой интерфейса.
Параметры сообщения заслуживают такой же аккуратности. Удаление {title} из перевода может быть допустимой редакционной перестройкой, но добавление {author} требует данных вызывающего кода. Если переводчик ввёл новый параметр без изменения договора, наша функция обнаружит отсутствие значения. В крупной системе полезен отчёт по наборам параметров каждого ключа. Он помогает найти техническое расхождение, но не оценивает естественность фразы и не заменяет языковую редактуру.
Такой словарь подходит для небольшой библиотеки с двумя языками и простыми параметрами. При росте проекта нужно будет выбрать готовую систему сообщений, поддерживающую необходимые правила и работу редакторов. Сам принцип сохранится: устойчивый смысловой ключ, целая фраза, явный контекст и безопасный вывод. В следующем уроке добавим множественные формы, где простой подстановки числа уже недостаточно.