Назад

Інтернаціоналізація у Next.js 13 із React Server Components

html code

Джерело фото: http://surl.li/gufiv

Словничок:

Інтернаціоналізація - процес мовної адаптації продукту, як-от програмне забезпечення, відеоігри, веб-сайти, кінофільми тощо, до мови конкретної країни чи регіону.

Вступ (короткий підсумок): Спираючись на приклад багатомовного додатку, який висвітлює вуличні фотографії з Unsplash, Jan Amann досліджує чи можна за допомогою next-intl задовольнити усі потреби з інтернаціоналізації у React Server Components та ділиться технікою із впровадження інтерактивності із найменшим впливом на стороні клієнта.

Із впровадженням Next.js 13 і бета-релізом App Router, стало доступним використовувати також React Server Components. Ця нова парадигма дозволяє компонентам, які не потребують інтерактивних можливостей React, такі як useState та useEffect, залишатися лише серверними.

Одна зі сфер, що отримала перевагу від цієї якості, це інтернаціоналізація. Зазвичай, через інтернаціоналізацію доводиться жертвувати продуктивністю, оскільки завантаження результатів перекладу у більші бандли на стороні клієнта та використання парсерів впливає на продуктивність під час роботи вашого додатку.

React Server Components обіцяє нам вирішити цю проблему. Якщо інтернаціоналізація впроваджується цілковито на серверній стороні, ми можемо досягти покращення продуктивності нашого додатку, залишаючи клієнта працювати з інтерактивністю. Але яким чином ми можемо працювати із цією парадигмою, коли нам потрібні інтерактивно-контрольовані стани, які мають бути задіяні у інтернаціоналізованому контенті?

У цій статті, ми дослідимо багатомовний додаток, який висвітлює вуличні фотографії з Unsplash. Ми використаємо next-intl аби реалізувати усі наші потреби із висвітлення контенту різними мовами у React Server Components, а також подивимось на спосіб впровадження інтерактивності із мінімальним впливом на роботу клієнта.

стартап
Джерело: http://surl.li/grurc

Демо: https://street-photography-viewer.vercel.app

Fetch фотографій з Unsplash

Ключовою перевагою Server Components є спроможність фетчити дані безпосередньо із компонентів через async/await. Ми можемо скористатися цим аби зафетчити фотографії з Unsplash у наш page-компонент.

Але спершу нам потрібно створити API client згідно з офіційною Unsplash SDK.

                    
import {createApi} from 'unsplash-js';

export default createApi({
  accessKey: process.env.UNSPLASH_ACCESS_KEY
});
                    
                 

Як тільки ми зробили власний Unsplash API client, ми можемо використовувати його у page-компоненті.

                    
import {OrderBy} from 'unsplash-js';
import UnsplashApiClient from './UnsplashApiClient';
                        
export default async function Index() {
    const topicSlug = 'street-photography';
                        
    const [topicRequest, photosRequest] = await Promise.all([
      UnsplashApiClient.topics.get({topicIdOrSlug: topicSlug}),
      UnsplashApiClient.topics.getPhotos({
        topicIdOrSlug: topicSlug,
        perPage: 4
    })
  ]);
                        
return (
    < PhotoViewer 
      coverPhoto={topicRequest.response.cover_photo}
      photos={photosRequest.response.results} 
    />
  );
}
                    
                 

Зверніть увагу: ми використовуємо Promise.all аби зробити запити паралельно. Таким чином ми уникаємо їх послідовності.

Наразі наш додаток рендерить простий фото-grid.

стартап
Джерело: http://surl.li/grvjt

Зараз додаток використовує hard-coded лейбли англійською, а дати на фотографіях - це часові мітки (timestamps), які насправді не є user-friendly (поки що).

Додавання інтернаціоналізації із next-intl

Додатково до англійської, ми хочемо додати іспанську. Підтримка Server Components наразі в beta для next-intl, тож ми можемо використовувати інструкцію зі встановлення останньої бета-версії аби налаштувати наш додаток для інтернаціоналізації.

Форматування дат

Окрім додавання другої мови, ми також виявили, що додаток не дуже добре адаптується під англомовних користувачів, оскільки дати мають бути відформатовані належним чином. Аби досягти гарного user experience, ми хочемо показати користувачу відносний час, коли фото було завантажено (наприклад, “8 днів тому”).

Отже коли next-intl встановлено, ми можемо змінити форматування, використовуючи функцію format.relativeTime у компоненті, що рендерить кожне фото.

                    
import {useFormatter} from 'next-intl';
                        
export default function PhotoGridItem({photo}) {
    const format = useFormatter();
    const updatedAt = new Date(photo.updated_at);
                        
    return (
        < a href={photo.links.html}>
            {/* ... */}
            < p>{format.relativeTime(updatedAt)}< /p>
        < / a>
    );
}
                    
                

Тепер час, коли фото було завантажено, значно простіше читати.

стартап
Джерело: http://surl.li/grvqm

Порада: у традиційному React додатку, який рендериться на сервері та на клієнті, може бути важко забезпечити синхронізацію дат між сервером та клієнтом. Оскільки це два різні середовища і можуть знаходитися у різних часових зонах, потрібно розробити механізм передачі серверного часу на клієнта. Але, виконуючи форматування лише на серверній стороні, про це можна не хвилюватися.

¡HOLA! 👋 Переклад додатку іспанською

Далі, ми можемо замінити статичні лейбли у header на локалізовані дані. Ці лейбли були передані як пропси з компоненту PhotoViewer, тож аби впровадити динамічні лейбли ми можемо застосувати useTranslations хук.

                        
import {useTranslations} from 'next-intl';
                            
export default function PhotoViewer(/* ... */) {
    const t = useTranslations('PhotoViewer');
                            
    return (
        <>
            < Header title={t('title')} 
             description={t('description')} 
            />
             {/* ... */}
        </>
  );
}
                        
                    

Для кожного інтернаціоналізованого лейблу мають бути відповідні entries, налаштовані для всіх мов.

стартап

Порада: next-intl надає TypeScript інтеграцію, яка допомагає переконатися, що ви вказуєте лише валідні ключі.

Тільки-но ви це зробили, то ми вже можемо відвідати іспанську версію вашого додатку додавши у кінці посилання /es.

стартап
Джерело: http://surl.li/gsvil

Вже непогано!

Додаємо інтерактивність: динамічний порядок фотографій

За замовчуванням, Unsplash API показує найпопулярніші фотографії. Але ми хочемо, аби користувач мав змогу змінити порядок та отримати спочатку найновіші фотографії.

Тут постає питання чи слід нам вдаватися до фетча даних на стороні клієнта, щоб ми могли реалізувати цю фічу за допомогою useState. Однак, це потребуватиме переносу усіх наших компонентів на сторону клієнта, що збільшить розмір нашого бандлу.

Чи маємо ми альтернативу? Так. І ця можливість вже існує на просторах вебу достатньо довго - search parameters (іноді називають query parameters). Нам підходить ця опція, оскільки параметри можна прочитати на сервері.

Тож давайте трохи змінимо наш page-компонент, щоб отримати searchParams через пропси.

                            
export default async function Index({searchParams}) {
const orderBy = searchParams.orderBy || OrderBy.POPULAR;
                                
const [/* ... */, photosRequest] = await Promise.all([
/* ... */,
UnsplashApiClient.topics.getPhotos({orderBy, /* ... */})
]);
                            
                        

Після цього, користувач може перейти до /?orderBy=latest та змінити порядок відображення фотографій.

Аби спростити вибір значення search parameter для користувача, ми хочемо додати інтерактивний select елемент.

стартап
Джерело: http://surl.li/gswin

Ми можемо відмітити компонент як 'use client'; додати слухач подій та оброблювати подію “change” у select елементі. Тим не менш, ми хочемо залишити інтернаціоналізацію на стороні сервера, аби зменшити розмір бандла на клієнті.

Подивимось на розмітку для нашого select елементу.

                            
<select›
    <option value="popular"›Popular</option›
    <option value="latest"›Latest</option›
</select
                        

Ми можем розділити цю розмітку на дві частини:

01 - рендер select елемент за допомогою інтерактивного Client Component

02 - рендер інтернаціоналізованих option елементів за допомогою Server Component і передаємо їх як children до select елемента.

Впровадимо select елемент на стороні клієнта.

                            
'use client';
                                
import {useRouter} from 'next-intl/client';
                                
export default function OrderBySelect({orderBy, children}) {
    const router = useRouter();
                                
    function onChange(event) {
        // The `useRouter` hook from `next-intl` automatically
        // considers a potential locale prefix of the pathname.
        router.replace('/?orderBy=' + event.target.value);
    }
                                
    return (
    <select defaultValue={orderBy} onChange={onChange}>
        {children}
    </select>
  );
}
                            
                        

Тепер, використаємо наш компонент у PhotoViewer і надамо локалізовані option елементи як children.

                            
import {useTranslations} from 'next-intl';
import OrderBySelect from './OrderBySelect';
                                
export default function PhotoViewer({orderBy, /* ... */}) {
const t = useTranslations('PhotoViewer');
                                
return (
  <>
    {/* ... */}
    <OrderBySelect orderBy={orderBy}>
        <option value="popular">{t('orderBy.popular')</option>
        <option value="latest">{t('orderBy.latest')</option>
    </OrderBySelect>
  </>
 );
}
                            
                        

За допомогою цього паттерну розмітка для option елементів тепер генерується на сервері и передається до OrderBySelect, який оброблює подію “change” на стороні клієнта.

Порада: Оскільки ми маємо зачекати на оновлену розмітку, яка генерується на сервері тільки но порядок змінюється, ми хочемо показати користувачу стан завантаження. React 18 має useTransition хук, який інтегрується з Server Components. Це дозволить зробити недоступним (disable) select елемент доки чекаємо на відповідь з серверу.

                        
import {useRouter} from 'next-intl/client';
import {useTransition} from 'react';
                            
export default function OrderBySelect({orderBy, children}) {
    const [isTransitioning, startTransition] = useTransition();
    const router = useRouter();
                            
    function onChange(event) {
        startTransition(() => {
            router.replace('/?orderBy=' + event.target.value);
        });
    }
                            
    return (
        <select disabled={isTransitioning} /* ... * />
            {children}
        </select>
    );
}
                        
                    

Додаємо більше інтерактивності: пагінація

Той самий паттерн може бути використаний для пагінації за допомогою параметру пошуку page.

стартап
Джерело: http://surl.li/gtldz

Зверніть увагу, що мови мають різні правила для використання десяткових і тисячних розділових знаків. Більш того, мови мають різні форми множини: якщо в англійській мові є лише граматична різниця між одним та нулем/багатьма елементами, то хорватська має окрему форму для «декількох» елементів.

next-intl використовує ICU syntax який надає можливість зберегти усі тонкощі мови.

стартап

На цей раз нам не треба відмічати компонент 'use client';. Замість того ми можемо впровадити звичайні a теги.

                        
import {ArrowLeftIcon, ArrowRightIcon} from '@heroicons/resolid';
import {Link, useTranslations} from 'next-intl';
                            
export default function Pagination({pageInfo, orderBy}) {
    const t = useTranslations('Pagination');
    const totalPages = Math.ceil(pageInfo.totalElements / pasize);
                            
    function getHref(page) {
        return {
        // Since we're using `Link` from next-intl, a potential locale
        // prefix of the pathname is automatically considered.
        pathname: '/',
        // Keep a potentially existing `orderBy` parameter.
        query: {orderBy, page}
      };
    }
                            
    return (
        <>
            {pageInfo.page > 1 && (
            <Link aria-label={t('prev')} href={getHref(papage - 1)}>
                <ArrowLeftIcon />
            </Link>
        )}
        <p>{t('info', {...pageInfo, totalPages})}</p>
        {pageInfo.page < totalPages && ( ̸aria-label={t('prev')} href={getHref(pageInfo.page + 1)}>
            <ArrowRightIcon />
            </Link>
            )}
        </>
      );
    }
                        
                    

Підведемо підсумки

SERVER COMPONENTS вдалий засіб для інтернаціоналізації

Інтернаціоналізація є важливою частиною досвіду користувача: чи ваш додаток підтримує декілька мов чи ви хочете отримати вірне відображення особливостей однієї мови. Бібліотека next-intl може зарадити в обох випадках.

Через впровадження інтернаціоналізації у додатках Next.js завжди доводилося жертвувати продуктивністю, але з Server Components цей недолік нівелюється. Однак, вам знадобиться деякий час, аби вивчити паттерни, які допоможуть перенести інтернаціоналізацію на сервер.

В нашому додатку, нам знадобилося перенести лише один компонент на клієнт: OrderBySelect.

стартап
Джерело: http://surl.li/gtnln

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

SEARCH PARAMETERS як гарна альтернатива useState

Search parameters - добрий спосіб впровадити інтерактивні фічі у додатки Next.js, оскільки вони допомагають зменшити розмір бандлу на стороні клієнта.

Окремо від продуктивності, є ще декілька переваг використання search parameters:

  • - URL з search parameters можна поширити зі збереженням стану додатку
  • - Закладки також зберігають стан
  • - Ви можете за бажанням інтегруватися з історією браузера та повертатися до попередніх станів за допомогою кнопки “Назад”.

Варто зауважити, що також існують деякі компроміси:

  • - Значення Search parameter це строки, тож може виникнути потреба серіалізації та десеріалізації типів даних
  • - URL - це частина інтерфейсу користувача, тож використання великої кількості параметрів може негативно вплинути на читабельність.

Повний код можна подивитися за посиланням на GitHub.