Переключение языка без потери страницы
Сборщик «Параллели» уже создаёт отдельный HTML для каждой опубликованной версии. Теперь нужно дать читателю возможность перейти к переводу. Обычная кнопка English в шапке часто отправляет человека на главную страницу языка, даже если он читает конкретную статью. Такой переход теряет контекст: пользователь вынужден снова искать материал.
Наш переключатель будет строиться для текущего ID статьи. Если английская версия доступна, ссылка поведёт прямо к ней. Если перевода нет, покажем понятное сообщение и оставим оригинал. Основная навигация будет работать без JavaScript: язык статического документа выбирается его адресом.
Ссылки на варианты текущей статьи
Возьмём stable-urls, у которой опубликованы ru и en. На русской странице текущий язык обозначим текстом, а английский — обычной ссылкой. Это облегчает понимание состояния и не предлагает бессмысленный переход на тот же адрес.
Полная функция построения переключателя в build.py использует конфигурацию site и текущую запись article:
from html import escape
def language_navigation(site, article, current_language):
links = []
for language, config in site["languages"].items():
variant = article["variants"].get(language)
label = escape(config["label"])
if language == current_language:
links.append(f'<span aria-current="true">{label}</span>')
elif variant and variant["status"] == "published":
url = escape(variant["url"], quote=True)
lang = escape(language, quote=True)
links.append(f'<a href="{url}" lang="{lang}" '
f'hreflang="{lang}">{label}</a>')
else:
links.append(f'<span>{label}: —</span>')
return ' · '.join(links)
Функция не угадывает путь заменой /ru/ на /en/. Такой алгоритм не работал бы с сохранёнными русскими URL без языкового префикса и с переводами, у которых другой slug. Карта соответствий уже существует в реестре; используем её, чтобы избежать второго независимого источника правды.
Атрибут lang у ссылки описывает язык её подписи, например English внутри русского документа. hreflang в этом месте относится к целевой ссылке. Позднее отдельные элементы link в head опишут набор альтернатив для поисковой системы. Ссылочный переключатель и поисковые метаданные связаны общей картой, но не заменяют друг друга.
Отсутствующая версия
У reading-list английский вариант находится в черновике. Знак тире в переключателе нужен лишь для краткого обозначения состояния. Рядом добавим русскую фразу из словаря: «Перевод этой статьи пока не готов». В английском каталоге такую статью можно представить как доступную на русском, если это соответствует редакционной политике, но ссылка должна вести на русский адрес.
Не делайте неработающую ссылку с href="#": она выглядит как действие, хотя не предоставляет перевод. Не создавайте фиктивную английскую страницу со всеми русскими абзацами и английским меню. Это нарушает ожидание пользователя и затрудняет описание языка основного содержимого.
В некоторых продуктах полезен запасной язык по цепочке: выбранный, затем язык компании, затем основной. Для учебной статьи такая цепочка должна быть видимой. Читатель может согласиться открыть оригинал, но не обязан понимать его. «Перевода нет; открыть русский материал» — ясное действие с ясным результатом.
Сравним договоры:
| Ситуация | Действие переключателя |
|---|---|
| Перевод опубликован | Ссылка на тот же ID в другом языке |
| Перевод черновой | Пояснение без ссылки на черновик |
| Язык уже выбран | Обозначение текущего языка |
| Материал есть только в оригинале | Явная ссылка на оригинал там, где она уместна |
Все эти состояния строятся из реестра. Наличие переведённой подписи English в интерфейсе не доказывает наличие перевода самой статьи.
Предпочтение пользователя и адрес
Можно запомнить язык, выбранный посетителем, но настройка не должна незаметно менять текст уже открытого публичного URL. Если человек перешёл по внешней ссылке на русский урок, показ русского материала соответствует адресу. Вместо автоматической отправки на другой язык предложим доступный перевод через заметную ссылку.
В учебном примере сохранение предпочтения применяется только к ссылке «Каталог» при следующем самостоятельном действии. Хранилище браузера может быть недоступно, поэтому ошибка записи не препятствует навигации. Следующий обработчик является дополнительным улучшением существующих ссылок:
document.querySelectorAll("a[data-language]").forEach(link => {
link.addEventListener("click", () => {
try {
localStorage.setItem("parallel.language", link.dataset.language);
} catch {
// Обычный переход по href остаётся доступным.
}
});
});
Этот код не отменяет стандартное действие ссылки. Открытие в новой вкладке, копирование адреса и работа без скрипта сохраняются. Данные localStorage не отправляются серверу автоматически и не делают страницу персонализированной при статической выдаче. Настройка остаётся удобством конкретного браузера, а не частью публичной карты URL.
Язык браузера является подсказкой
navigator.languages и серверный Accept-Language помогают предложить подходящий язык, но не определяют язык намерения человека в каждом запросе. Настройки браузера могут принадлежать рабочему компьютеру, общему устройству или быть оставлены по умолчанию. Поэтому не скрываем ручной выбор и не запрещаем доступ к другим версиям.
Для корневой страницы нового многоязычного сайта можно сделать явный выбор языка. Для существующей русской библиотеки этот же корень уже имеет содержание и входящий трафик. Переписывать его назначение нужно по отдельной причине, а не автоматически вслед за появлением английских статей. В «Параллели» русский корень и прежние материалы сохраняются.
Переход к той же части статьи
У страницы могут быть фрагменты вида #routing. Обычный переход на английский URL не обязан сохранять такой фрагмент: соответствующего якоря может не существовать или он может обозначать другую часть. В начальном переключателе мы переходим к началу перевода, потому что реестр связывает документы, а не каждый их подзаголовок.
Если требуется сохранить место чтения, добавьте явную карту устойчивых секций. Например, ключ route-model может иметь русский и английский якорь. Переключатель будет искать соответствие для текущей секции и использовать его только при наличии готового варианта. Механическое копирование location.hash в каждый язык создаёт переходы, которые выглядят точными, но ведут к отсутствующему месту.
Другая крайняя ситуация — статья удалена или объединена с соседней. Тогда перевод может тоже менять назначение. Решение нужно оформить в редакционном реестре и правилах выдачи, а не оставлять старую кнопку наугад. Сохранение пользовательского контекста означает понятное соответствие содержания, а не одинаковую строку в адресной строке.
Для «Параллели» достаточно уровня статьи. Такой небольшой договор честно выполняется обычными ссылками и сохраняет доступность без дополнительных скриптов. Более сложное продолжение можно добавить позднее, когда появится реальная задача переносить прогресс чтения между языками.
Теперь переключатель работает с обычными ссылками на реальные версии и не теряет ID статьи. В следующем уроке используем ту же карту для canonical, hreflang и sitemap. Так навигация читателя и описание сайта для поисковых систем будут основаны на одинаковом наборе опубликованных страниц.