next-intl
برای پیاده سازی تمام نیازهای بین المللی سازی در React Server Components و به اشتراک گذاری تکنیکی برای معرفی تعامل با یک ردپای حداقلی سمت مشتری.
با معرفی Next.js 13 و انتشار نسخه بتا App Router، React Server Components در دسترس عموم قرار گرفت. این پارادایم جدید به اجزایی اجازه می دهد که به ویژگی های تعاملی React نیازی نداشته باشند، مانند useState
و useEffect
، فقط در سمت سرور باقی بماند.
یکی از حوزه هایی که از این قابلیت جدید سود می برد این است بین المللی شدن. بهطور سنتی، بینالمللیسازی به یک معاوضه در عملکرد نیاز دارد، زیرا بارگیری ترجمهها منجر به بستههای بزرگتری در سمت مشتری میشود و استفاده از تجزیهکنندههای پیام بر عملکرد زمان اجرای برنامه شما تأثیر میگذارد.
وعده از اجزای سرور React این است که ما می توانیم کیک خود را بخوریم و آن را هم بخوریم. اگر بینالمللیسازی بهطور کامل در سمت سرور اجرا شود، میتوانیم به سطوح جدیدی از عملکرد برنامههایمان دست پیدا کنیم و ویژگیهای تعاملی را از سمت کلاینت رها کنیم. اما چگونه میتوانیم با این پارادایم کار کنیم، وقتی به حالتهای کنترلشده تعاملی نیاز داریم که باید در پیامهای بینالمللی منعکس شوند؟
در این مقاله، یک اپلیکیشن چند زبانه را بررسی می کنیم که تصاویر عکاسی خیابانی را از Unsplash نمایش می دهد. استفاده خواهیم کرد next-intl
برای پیادهسازی تمام نیازهای بینالمللیسازی خود در React Server Components، و به تکنیکی برای معرفی تعامل با یک ردپای مینیمالیستی سمت مشتری نگاه خواهیم کرد.

واکشی عکس ها از Unsplash
یکی از مزایای کلیدی کامپوننتهای سرور، توانایی واکشی مستقیم دادهها از داخل اجزای داخلی است async
/await
. میتوانیم از این برای واکشی عکسها از Unsplash در مؤلفه صفحهمان استفاده کنیم.
اما ابتدا باید کلاینت API خود را بر اساس Unsplash SDK رسمی ایجاد کنیم.
import {createApi} from 'unsplash-js';
export default createApi({
accessKey: process.env.UNSPLASH_ACCESS_KEY
});
وقتی مشتری Unsplash API خود را داشتیم، میتوانیم از آن در مؤلفه صفحه خود استفاده کنیم.
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
برای فراخوانی هر دو درخواستی که باید به صورت موازی انجام دهیم. به این ترتیب از درخواست آبشار جلوگیری می کنیم.
در این مرحله، برنامه ما یک شبکه عکس ساده را ارائه می دهد.

این اپلیکیشن در حال حاضر از برچسبهای انگلیسی سختکد شده استفاده میکند و تاریخ عکسها بهعنوان مهر زمانی نمایش داده میشوند که (هنوز) چندان کاربرپسند نیست.
افزودن بین المللی سازی با next-intl
علاوه بر انگلیسی، ما دوست داریم برنامه ما به زبان اسپانیایی نیز در دسترس باشد. پشتیبانی از اجزای سرور در حال حاضر در نسخه بتا است next-intl
، بنابراین ما می توانیم استفاده کنیم دستورالعمل نصب آخرین نسخه بتا برای راه اندازی برنامه ما برای بین المللی شدن.
قالب بندی تاریخ ها
گذشته از افزودن زبان دوم، قبلاً متوجه شدهایم که این برنامه به خوبی با کاربران انگلیسی سازگار نیست زیرا تاریخها باید قالببندی شوند. برای دستیابی به یک تجربه کاربری خوب، میخواهیم زمان نسبی آپلود عکس را به کاربر بگوییم (مثلاً “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>
</div>
</a>
);
}
اکنون تاریخ به روز رسانی یک عکس راحت تر خوانده می شود.

اشاره: در یک برنامه سنتی React که هم در سمت سرور و هم در سمت کلاینت رندر می شود، اطمینان از همگام بودن تاریخ نسبی نمایش داده شده در سرور و کلاینت می تواند کاملاً چالش برانگیز باشد. از آنجایی که اینها محیطهای متفاوتی هستند و ممکن است در مناطق زمانی متفاوت باشند، باید مکانیزمی را برای انتقال زمان سرور به سمت کلاینت پیکربندی کنید. با انجام فرمت فقط در سمت سرور، در وهله اول نگران این مشکل نباشیم.
هولا! 👋 ترجمه برنامه ما به اسپانیایی
در مرحله بعد، میتوانیم برچسبهای ثابت در هدر را با پیامهای محلی جایگزین کنیم. این برچسب ها به عنوان پایه از PhotoViewer
جزء، بنابراین این شانس ما برای معرفی برچسب های پویا از طریق است useTranslations
قلاب.
import {useTranslations} from 'next-intl';
export default function PhotoViewer(/* ... */) {
const t = useTranslations('PhotoViewer');
return (
<>
<Header
title={t('title')}
description={t('description')}
/>
{/* ... */}
</>
);
}
برای هر برچسب بین المللی که اضافه می کنیم، باید مطمئن شویم که یک ورودی مناسب برای همه زبان ها تنظیم شده است.
// en.json
{
"PhotoViewer": {
"title": "Street photography",
"description": "Street photography captures real-life moments and human interactions in public places. It is a way to tell visual stories and freeze fleeting moments of time, turning the ordinary into the extraordinary."
}
}
// es.json
{
"PhotoViewer": {
"title": "Street photography",
"description": "La fotografía callejera capta momentos de la vida real y interacciones humanas en lugares públicos. Es una forma de contar historias visuales y congelar momentos fugaces del tiempo, convirtiendo lo ordinario en lo extraordinario."
}
}
نکته: next-intl
یک ادغام TypeScript را فراهم می کند که به شما کمک می کند مطمئن شوید که فقط به کلیدهای پیام معتبر ارجاع می دهید.
پس از انجام این کار، میتوانیم از نسخه اسپانیایی برنامه در اینجا دیدن کنیم /es
.

تا اینجای کار خیلی خوبه!
افزودن تعامل: ترتیب پویا عکس ها
به طور پیش فرض، Unsplash API محبوب ترین عکس ها را برمی گرداند. ما می خواهیم کاربر بتواند ابتدا ترتیب نمایش جدیدترین عکس ها را تغییر دهد.
در اینجا این سوال مطرح می شود که آیا باید به واکشی داده های سمت مشتری متوسل شویم تا بتوانیم این ویژگی را با useState
. با این حال، این امر مستلزم آن است که همه اجزای خود را به سمت مشتری منتقل کنیم و در نتیجه اندازه بستهای افزایش مییابد.
آیا جایگزینی داریم؟ آره. و این قابلیتی است که برای قرن ها در وب وجود داشته است: پارامترهای جستجو (گاهی اوقات به عنوان پارامترهای پرس و جو). چیزی که پارامترهای جستجو را به یک گزینه عالی برای مورد استفاده ما تبدیل می کند این است که می توان آنها را در سمت سرور خواند.
بنابراین اجازه دهید جزء صفحه خود را برای دریافت تغییر دهیم searchParams
از طریق وسایل
export default async function Index({searchParams}) {
const orderBy = searchParams.orderBy || OrderBy.POPULAR;
const [/* ... */, photosRequest] = await Promise.all([
/* ... */,
UnsplashApiClient.topics.getPhotos({orderBy, /* ... */})
]);
پس از این تغییر، کاربر می تواند به /?orderBy=latest
برای تغییر ترتیب عکس های نمایش داده شده
برای اینکه کاربر به راحتی بتواند مقدار پارامتر جستجو را تغییر دهد، میخواهیم یک تصویر تعاملی ارائه کنیم. select
عنصر از درون یک جزء

ما می توانیم جزء را با علامت گذاری کنیم 'use client';
برای پیوست کردن یک کنترل کننده رویداد و پردازش رویدادهای تغییر از select
عنصر با این وجود، ما می خواهیم نگرانی های بین المللی سازی را در سمت سرور حفظ کنیم تا اندازه بسته نرم افزاری مشتری کاهش یابد.
بیایید به نشانه گذاری مورد نیاز برای ما نگاهی بیندازیم select
عنصر
<select>
<option value="popular">Popular</option>
<option value="latest">Latest</option>
</select>
می توانیم این نشانه گذاری را به دو قسمت تقسیم کنیم:
- رندر کنید
select
عنصر با یک مؤلفه مشتری تعاملی. - بین المللی شده را ارائه دهید
option
عناصر با یک کامپوننت سرور و ارسال آنها به عنوان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
، که رویداد تغییر را در سمت مشتری مدیریت می کند.
نکته: از آنجایی که هنگام تغییر سفارش باید منتظر بمانیم تا نشانهگذاری بهروز شده در سمت سرور ایجاد شود، ممکن است بخواهیم وضعیت بارگیری را به کاربر نشان دهیم. React 18 معرفی شد را useTransition
قلاب، که با اجزای سرور یکپارچه شده است. این به ما این امکان را می دهد که غیرفعال کنیم 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>
);
}
افزودن تعامل بیشتر: کنترل های صفحه
همان الگویی که برای تغییر ترتیب بررسی کردیم را می توان با معرفی a در کنترل های صفحه اعمال کرد page
پارامتر جستجو

توجه داشته باشید که زبان ها قوانین متفاوتی برای مدیریت جداکننده های اعشاری و هزار دارند. علاوه بر این، زبانها اشکال مختلفی از کثرتسازی دارند: در حالی که انگلیسی فقط بین یک و صفر/چند عنصر تمایز دستوری قائل میشود، برای مثال، کرواتی یک شکل جداگانه برای عناصر «چند» دارد.
next-intl
استفاده می کند نحو آی سی یو که بیان این ظرافت های زبانی را ممکن می سازد.
// en.json
{
"Pagination": {
"info": "Page {page, number} of {totalPages, number} ({totalElements, plural, =1 {one result} other {# results}} in total)",
// ...
}
}
این بار نیازی نیست که یک جزء را با آن علامت گذاری کنیم 'use client';
. در عوض، ما می توانیم این را با تگ های لنگر معمولی پیاده سازی کنیم.
import {ArrowLeftIcon, ArrowRightIcon} from '@heroicons/react/24/solid';
import {Link, useTranslations} from 'next-intl';
export default function Pagination({pageInfo, orderBy}) {
const t = useTranslations('Pagination');
const totalPages = Math.ceil(pageInfo.totalElements / pageInfo.size);
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(pageInfo.page - 1)}>
<ArrowLeftIcon />
</Link>
)}
<p>{t('info', {...pageInfo, totalPages})}</p>
{pageInfo.page < totalPages && (
<Link aria-label={t('prev')} href={getHref(pageInfo.page + 1)}>
<ArrowRightIcon />
</Link>
)}
</>
);
}
نتیجه
اجزای سرور یک تطابق عالی برای بین المللی شدن هستند
بینالمللیسازی بخش مهمی از تجربه کاربر است، چه از چندین زبان پشتیبانی کنید و چه بخواهید از ظرافتهای یک زبان به درستی استفاده کنید. یک کتابخانه مانند next-intl
می تواند در هر دو مورد کمک کند.
پیادهسازی بینالمللیسازی در برنامههای Next.js از لحاظ تاریخی با یک معاوضه عملکرد همراه بوده است، اما در مورد مؤلفههای سرور، دیگر اینطور نیست. با این حال، کاوش و یادگیری الگوهایی که به شما کمک می کند نگرانی های بین المللی خود را در سمت سرور حفظ کنید، ممکن است کمی طول بکشد.
در برنامه نمایشگر عکاسی خیابانی ما، فقط باید یک جزء را به سمت مشتری منتقل کنیم: OrderBySelect
.

جنبه دیگری که باید به آن توجه کنید این است که ممکن است بخواهید اجرای حالت های بارگذاری را در نظر بگیرید زیرا تاخیر شبکه قبل از اینکه کاربران شما نتیجه اقدامات خود را ببینند تاخیر ایجاد می کند.
پارامترهای جستجو یک جایگزین عالی برای useState
پارامترهای جستجو یک راه عالی برای پیاده سازی ویژگی های تعاملی در برنامه های Next.js هستند، زیرا به کاهش اندازه بسته نرم افزاری سمت کلاینت کمک می کنند.
به غیر از عملکرد، موارد دیگری نیز وجود دارد مزایای استفاده از پارامترهای جستجو:
- URL های دارای پارامترهای جستجو را می توان با حفظ وضعیت برنامه به اشتراک گذاشت.
- نشانک ها وضعیت را نیز حفظ می کنند.
- میتوانید بهصورت اختیاری با سابقه مرورگر ادغام شوید و از طریق دکمه برگشت، تغییرات حالت را لغو کنید.
البته توجه داشته باشید که وجود دارند معاوضه هایی که باید در نظر گرفته شوند:
- مقادیر پارامترهای جستجو رشتهها هستند، بنابراین ممکن است لازم باشد انواع دادهها را سریالسازی کنید و سریالسازی کنید.
- URL بخشی از رابط کاربری است، بنابراین استفاده از بسیاری از پارامترهای جستجو ممکن است بر خوانایی تأثیر بگذارد.
شما می توانید نگاهی به کامل داشته باشید کد نمونه در GitHub.
با تشکر فراوان از دلبا دی اولیویرا از Vercel برای ارائه بازخورد برای این مقاله!
مطالعه بیشتر در SmashingMag

(yk, il)