i18n концепции

Интернационализация (i18n) в приложениях на Fresh направлена на разделение контента, языка и логики отображения, сохраняя при этом ключевую особенность фреймворка — серверный рендеринг с минимальным JavaScript на клиенте. Fresh не навязывает встроенную систему i18n, что делает концепции особенно важными: архитектура локализации проектируется на уровне приложения.

i18n в Fresh строится вокруг следующих принципов:

  • язык определяется на сервере;
  • переводимые строки не зависят от компонентов;
  • маршрутизация и локализация тесно связаны;
  • состояние языка не хранится в клиентском состоянии.

Определение текущей локали

Основной источник информации о языке — HTTP-запрос. В Fresh доступны все стандартные механизмы:

Заголовок Accept-Language

HTTP-заголовок Accept-Language позволяет определить предпочтительный язык пользователя:

Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7

На сервере Fresh этот заголовок читается через Request:

const langHeader = req.headers.get("accept-language");

Обычно применяется разбор приоритетов и сопоставление с поддерживаемыми локалями.

Язык в URL

Наиболее распространённый и предсказуемый подход — явное указание языка в маршруте:

/ru/blog
/en/blog
/de/blog

В Fresh это естественно реализуется через файловую структуру:

routes/
 ├─ [lang]/
 │   ├─ index.tsx
 │   ├─ blog.tsx

Параметр lang автоматически доступен в обработчике маршрута.

Cookies и fallback

Cookies используются как вторичный источник, если язык не указан в URL. Типовой порядок определения локали:

  1. параметр маршрута;
  2. cookie;
  3. Accept-Language;
  4. язык по умолчанию.

Архитектура переводов

Статические словари

Наиболее прозрачная модель — хранение переводов в виде статических объектов:

export const ru = {
  title: "Главная страница",
  menu: {
    home: "Главная",
    blog: "Блог",
  },
};

Каждая локаль оформляется как отдельный модуль:

locales/
 ├─ ru.ts
 ├─ en.ts
 ├─ de.ts

Импорт выполняется на сервере, без участия клиента.

Типизация переводов

Fresh активно использует TypeScript, что позволяет строго типизировать словари:

type Dictionary = typeof ru;

const dictionaries: Record<string, Dictionary> = {
  ru,
  en,
};

Это исключает расхождения ключей между языками на этапе компиляции.


Контекст локализации

Для передачи переводов в компоненты используется контекст рендеринга Fresh.

Расширение HandlerContext

В обработчике маршрута определяется язык и словарь:

export const handler = {
  GET(req, ctx) {
    const lang = ctx.params.lang;
    const dict = dictionaries[lang] ?? dictionaries.ru;

    return ctx.render({ lang, dict });
  },
};

Данные передаются в компонент страницы как props.


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

Компоненты Fresh — это чистые функции без клиентского состояния. Переводы передаются явно:

export default function Page(props) {
  const { dict } = props.data;

  return (
    <h1>{dict.title}</h1>
  );
}

Ключевые особенности подхода:

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

Функции-помощники для перевода

Для уменьшения шаблонного кода применяется функция-доступ к ключам:

function t(dict, path: string) {
  return path.split(".").reduce((o, k) => o?.[k], dict);
}

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

<h1>{t(dict, "menu.home")}</h1>

В крупных проектах применяются типобезопасные генераторы ключей или enum-подход.


Параметризация строк

Переводы часто содержат переменные:

welcome: "Добро пожаловать, {name}"

Простая интерполяция:

function format(str: string, vars: Record<string, string>) {
  return str.replace(/\{(\w+)\}/g, (_, k) => vars[k]);
}

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

{format(dict.welcome, { name: user.name })}

Такой подход полностью совместим с серверным рендерингом Fresh.


Форматы дат, чисел и валют

Текст — лишь часть i18n. Форматирование значений выполняется через стандартный Intl API:

new Intl.DateTimeFormat(lang).format(date);
new Intl.NumberFormat(lang).format(number);
new Intl.NumberFormat(lang, {
  style: "currency",
  currency: "EUR",
}).format(price);

Intl работает нативно в Deno и не требует сторонних библиотек.


Локализация маршрутов

Помимо текста, локализуются сами пути:

/ru/o-nas
/en/about

Реализуется через таблицу маршрутов:

const routes = {
  about: {
    ru: "o-nas",
    en: "about",
  },
};

URL генерируются на сервере с учётом языка, что сохраняет SEO и кэшируемость.


SEO и i18n

Fresh отлично подходит для многоязычных сайтов благодаря SSR:

  • <html lang="ru"> задаётся на сервере;
  • <link rel="alternate" hreflang="..."> формируется динамически;
  • каждая локаль имеет собственный URL.

Поисковые системы индексируют страницы как независимые документы.


Производительность и кэширование

Переводы — статические данные, что позволяет:

  • кэшировать словари в памяти;
  • использовать edge-кэширование;
  • избегать дополнительного JavaScript на клиенте.

i18n не увеличивает размер hydration-бандла, поскольку логика полностью серверная.


Типовые ошибки проектирования

Хранение языка в состоянии клиента Нарушает модель Fresh и усложняет рендеринг.

Глобальный singleton-переводчик Приводит к ошибкам при параллельных запросах.

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


Итоговая модель i18n в Fresh

  • язык определяется до рендера;
  • переводы — обычные TypeScript-модули;
  • маршруты и язык связаны структурно;
  • компоненты остаются чистыми функциями.

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