Больше библиотек богу библиотек или как я пересмотрел i18n [next.js v14]

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Для интернационализации сделаны десятки по-своему потрясающих библиотек, такие как 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 <button>{dict.products.cart}</button> // 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 (
    <p>{t('intro.title')}</p>
  )
}
'use client';

import useTranslation from 'next-translation/useTranslation'

export default function ClientComponent() {
  const { t } = useTranslation()

  return (
    <p>{t('intro.title')}</p>
  )
}

Получился достаточно привычным интерфейс, функции поддерживают namespace и query. Его рекомендуется использовать по умолчанию, так как он прост в использовании и в логике. Возвращает готовую строку.

Для более сложных же переводов нужно использовать компоненты ClientTranslation и ServerTranslation. Они умеют заменять псевдо-компоненты реальными.

import ServerTranslation from 'next-tranlation/ServerTranslation';

export default function ServerComponent() {
  return(
    <ServerTranslation
      term='intro.description'
      components={{
        link: <a href='#' />
      }}
    />
  )
}
"use client";

import ClientTranslation from 'next-tranlation/ClientTranslation';

export default function ClientTranslation() {
  return(
    <ClientTranslation
      term='intro.description'
      components={{
        link: <a href='#' />
      }}
    />
  )
}

Также есть случаи, когда переводы нужно добавить и вне 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 (
    <NextTranlationProvider lang={params.lang} clientTerms={['shared', 'banking.about']}>
      {/* ... */}
    </NextTranlationProvider>
  )
}

Также нужно обозначить какие переводы нужны именно на клиенте и передать туда только их. Для этого в NextTranslationProvider можно передать массив клиентских ключей или групп с помощью пропа clientTerms.

Также иногда возникают ситуации, когда компоненту нужны разные переводы или рендерятся разные блоки в зависимости от условий. В таких случаях на клиент нужно передать и разные переводы. Варианты условия можно обернуть в NextTranslationTransmitter и передать в него клиентские термины.

import NextTranslationTransmitter from 'next-tranlation/NextTranslationTransmitter';
import ClientComponent from './ClientComponent';

const ServerComponent: React.FC = () => (
  <NextTranslationTransmitter terms={['header.nav']}>
    <ClientComponent />
  </NextTranslationTransmitter>
)

Как итог, в клиентские компоненты будут переданы только те термины, которые были указаны выше в NextTranslationProvider или NextTranslationTransmitter.

Настройка пакета

Перед работой с переводами их нужно загрузить. Для этого нужно создать конфигурационный файл в корне проекта. Минимальная его конфигурация - функция load, которая вернёт актуальные переводы и массив languages с допустимыми языками. Функция load вызывается в серверных компонентах, а нужные ключи будут переданы на клиент.

Очень важным пунктом было отсутствие лишних запросов, то есть нужно полноценное кеширование.

Здесь стоит немного отойти в сторону. Next.js с последней версии собирает приложение параллельно в несколько процессов. Если бы каждый процесс жил со своим кешем - из каждого послылались бы запросы. Вероятно именно во избежание этого команда Next.js переделала fetch - теперь он работает с общим кешем.

Таким же путём решает проблему и пакет - он создаёт общий кеш и работает из каждого процесса уже с ним. Чтобы это работало нужно использовать withNextTranslation в next.config.js.

Заключение

Решение получилось по-настоящему настроенное под next.js - учитывающее все его возможности и проблемы. Также оно включает все возможности оптимизаций, которые дают серверные компоненты. Пакет полностью оптимизирован под next.js, их концепции и взгляды, которые я полностью разделяю.

Я встал перед проблемой переводов и мне пришлось сделать своё решение, которое будет работать именно так, как ожидается. Несмотря на значительное преимущество в оптимизациях, по возможностям самих переводов пакет пока ещё уступает крупным библиотекам. Впереди много работы.

P.S. Буду благодарен, если опишите, чего вам не хватало в существующих решениях или какой функционал считаете самым важным.

Источник: https://habr.com/ru/articles/793266/


Интересные статьи

Интересные статьи

Такого принципа я придерживался всю карьеру, но есть нюансы. Поначалу я приносил пользу потенциальным клиентам ещё до того, как они заплатят денег. Консультировал, составлял функциональные требования ...
Здравствуй, дорогой читатель. Каждому Android-разработчику во время разработки своего приложения необходимо сталкиваться с созданием пользовательского интерфейса. Для этого существует несколько способ...
Сервис связан с постоянными административными затратами, нести которые приходится поставщику сервиса. Правильно спроектированная библиотека переносит все эти затраты на её пользовател...
В Авито колоссальное количество интерфейсов и экранов — около 1000 на десктопе и в мобильных приложениях. Чтобы интерфейсы были консистентными и корректно работали, а соз...
Продолжаем серию (1, 2, 3) статей о сюрпризах, спрятанных в разнообразных музыкальных записях. В прошлый раз мы начали говорить о посланиях, скрытых намеках и зашифрованных образах. Сегодня ре...