Больше библиотек богу библиотек или как я пересмотрел i18n
Для интернационализации сделаны десятки по-своему потрясающих библиотек, такие как i18n, react-intl, next-intl. Все они отлично справляются со своей задачей — добавляют переводы в приложение или на сайт. Большинство из них проверены, отлажены и стабильно поддерживаются.
Но все они устарели.
Ведь всё это время развивалось и экосистема реакта. Так, последняя версия next.js включила крупные обновления из react.js — cache, taint, новые хуки и, конечно же, серверные компоненты. Команда самого React.js, вероятно, представит эти изменения уже в мае.
В этой статье я расскажу о ключевых изменениях, личном опыте, проблемах существующих решений, необходимым обновлениях, решениях, к которым я пришёл и, конечно же, отвечу на вопросы зачем, а самое главное — зачем?
Изменения
Первое, с чего стоит начать — как так вышло, что изменения React.js сделали библиотеки переводов устаревшими.
Несмотря на то, что последняя стабильная версия React.js вышла почти два года назад, он имеет и 2 других канала — canary и experimental, где canary также считается стабильным каналом и рекомендуется для использования библиотеками.
Именно этим каналом и пользуется Next.js. Next.js запустил серверные компоненты без дополнительных флагов внутри так называемого App Router-а — это новая директория в альтернативу pages, которая использует свои конвенции и различный сахар (об изменениях и проблемах которого я писал в недавней статье).
Серверные компоненты однозначно решают ряд проблем и являются новой вехой оптимизаций. В том числе и для переводов. Без серверных компонент переводы хранились как в собранном html так и крупным объектом в клиентском скрипте. Теперь же можно сразу получить готовый html, которому ничего не нужно на клиенте.
Next.js уделил этой возможности особое внимание.
Личный опыт
Добавить переводы (по документации Next.js) можно следующим образом:
// app/[lang]/dictionaries.js
import 'server-only'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export const getDictionary = async (locale) => dictionarieslocale
// app/[lang]/page.js
import { getDictionary } from './dictionaries'
export default async function Page({ params: { lang } }) {
const dict = await getDictionary(lang) // en
return // Add to Cart
}
Данное решение описывается как готовое и полностью оптимизированное. Оно работает полностью на сервере, а клиент получает уже готовый HTML. Однако команда Next.js опустила одну важную деталь — как передавать язык в глубокую вложенность в серверных компонентах.
Большая проблема серверных компонент — в них не доступны контексты. Команда Next.js объясняет отсутствие этих функций тем, что Layout не ререндерится, а всё что зависит от пропсов должно быть клиентским.
Пожалуй больше всего это коснулось библиотеки переводов. В качестве временного решения они предлагают определять язык в middleware и добавлять его в куки. Затем при построении страницы читать его в нужных местах. Но чтение куков означает включение серверного рендеринга, что подходит далеко не всем.
В целом же главная проблема существующих решений — большинство из них не сделаны под серверные компоненты. Компоненты и функции разрабатывались под рантайм, с использованием хуков и синхронностью.
Ещё одной неприятностью стало кеширование в Next.js. А именно — оно полноценно работает только для GET запросов, а также если вес переводов больше лимита в 2МБ — они не будут закешированы.
Реализация
Цели и задачи:
Библиотека должна иметь полный функционал как в клиентских комопонентах, так и в серверных;
Использование должно быть простым (без лишних пропсов);
Вся сложная логика должна быть перенесена на сервер;
Переводы должны загружаться только по необходимости и без лишних запросов;
Поддержка обновления переводов без пересборки (ISR/SSR);
Всё должно работать в статическом сайте (а не с переключением в SSR);
Должны поддерживаться html entities.
К моему удивлению нет ни одной библиотеки, которая удовлетворила бы все эти запросы.
Первое, что нужно — функционал. В стандартном варианте это хук, возвращающий функцию t
и компонент Trans
для более сложных переводов. Однако такой функционал нужен и в серверных компонентах, а они имеют множество своих особенностей.
Функционал
Основной функционал делится на две версии — для клиентских компонент и для серверных и включает в себя:
useTranslation
, getTranslation
— которые возвращают функцию t
внутри ДОМ-а и язык;
import getTranslation from 'next-translation/getTranslation'
export default function ServerComponent() {
const { t } = getTranslation()
return (
{t('intro.title')}
)
}
'use client';
import useTranslation from 'next-translation/useTranslation'
export default function ClientComponent() {
const { t } = useTranslation()
return (
{t('intro.title')}
)
}
Получился достаточно привычным интерфейс, функции поддерживают namespace
и query
. Его рекомендуется использовать по умолчанию, так как он прост в использовании и в логике. Возвращает готовую строку.
Для более сложных же переводов нужно использовать компоненты ClientTranslation
и ServerTranslation
. Они умеют заменять псевдо-компоненты реальными.
import ServerTranslation from 'next-tranlation/ServerTranslation';
export default function ServerComponent() {
return(
}}
/>
)
}
"use client";
import ClientTranslation from 'next-tranlation/ClientTranslation';
export default function ClientTranslation() {
return(
}}
/>
)
}
Также есть случаи, когда переводы нужно добавить и вне react-дерева. Для этого в любом месте можно использовать createTranslation
.
import createTranslation from 'next-translation/createTranslation'
// ...
export async function generateMetadata({ params }: { params: { lang: string } }) {
const { t } = await createTranslation(params.lang);
return {
title: t('homePage.meta.title'),
}
}
Настройка страницы
Теперь о настройке страницы. Для работы с переводами нужно знать язык. Однако в серверных компонентах нельзя использовать контекст. Для решения этого была сделана альтернатива createContext
для серверных компонент в пакете next-impl-getters — createServerContext
и getServerContext
.
Чтобы его использовать нужно создать NextTranslationProvider
. Это рекомендуется делать на уровне страницы, чтобы избежать проблем с ререндером Layout.
import NextTranlationProvider from 'next-translation/NextTranlationProvider'
export default function HomePage({ params }: { params: { lang: string } }) {
return (
{/* ... */}
)
}
Также нужно обозначить какие переводы нужны именно на клиенте и передать туда только их. Для этого в NextTranslationProvider
можно передать массив клиентских ключей или групп с помощью пропа clientTerms
.
Также иногда возникают ситуации, когда компоненту нужны разные переводы или рендерятся разные блоки в зависимости от условий. В таких случаях на клиент нужно передать и разные переводы. Варианты условия можно обернуть в NextTranslationTransmitter
и передать в него клиентские термины.
import NextTranslationTransmitter from 'next-tranlation/NextTranslationTransmitter';
import ClientComponent from './ClientComponent';
const ServerComponent: React.FC = () => (
)
Как итог, в клиентские компоненты будут переданы только те термины, которые были указаны выше в NextTranslationProvider
или NextTranslationTransmitter
.
Настройка пакета
Перед работой с переводами их нужно загрузить. Для этого нужно создать конфигурационный файл в корне проекта. Минимальная его конфигурация — функция load
, которая вернёт актуальные переводы и массив languages
с допустимыми языками. Функция load
вызывается в серверных компонентах, а нужные ключи будут переданы на клиент.
Очень важным пунктом было отсутствие лишних запросов, то есть нужно полноценное кеширование.
Здесь стоит немного отойти в сторону. Next.js с последней версии собирает приложение параллельно в несколько процессов. Если бы каждый процесс жил со своим кешем — из каждого послылались бы запросы. Вероятно именно во избежание этого команда Next.js переделала fetch — теперь он работает с общим кешем.
Таким же путём решает проблему и пакет — он создаёт общий кеш и работает из каждого процесса уже с ним. Чтобы это работало нужно использовать withNextTranslation
в next.config.js
.
Заключение
Решение получилось по-настоящему настроенное под next.js — учитывающее все его возможности и проблемы. Также оно включает все возможности оптимизаций, которые дают серверные компоненты. Пакет полностью оптимизирован под next.js, их концепции и взгляды, которые я полностью разделяю.
Я встал перед проблемой переводов и мне пришлось сделать своё решение, которое будет работать именно так, как ожидается. Несмотря на значительное преимущество в оптимизациях, по возможностям самих переводов пакет пока ещё уступает крупным библиотекам. Впереди много работы.
P.S. Буду благодарен, если опишите, чего вам не хватало в существующих решениях или какой функционал считаете самым важным.