برنامه نویسی

بین المللی سازی (i18n) در Next.js 13 با 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>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

می توانیم این نشانه گذاری را به دو قسمت تقسیم کنیم:

  1. رندر کنید select عنصر با یک مؤلفه مشتری تعاملی.
  2. بین المللی شده را ارائه دهید 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 از نحو ICU استفاده می کند که بیان این ظرافت های زبان را ممکن می سازد.

// 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 هستند، زیرا به کاهش اندازه بسته نرم افزاری سمت کلاینت کمک می کنند.

به غیر از عملکرد، موارد دیگری نیز وجود دارد مزایای استفاده از پارامترهای جستجو:

  1. URL های دارای پارامترهای جستجو را می توان با حفظ وضعیت برنامه به اشتراک گذاشت.
  2. نشانک ها وضعیت را نیز حفظ می کنند.
  3. می‌توانید به‌صورت اختیاری با سابقه مرورگر ادغام شوید و از طریق دکمه برگشت، تغییرات حالت را لغو کنید.

با این حال توجه داشته باشید که وجود دارند معاوضه هایی که باید در نظر گرفته شوند:

  1. مقادیر پارامترهای جستجو رشته‌ها هستند، بنابراین ممکن است لازم باشد انواع داده‌ها را سریال‌سازی کنید و سریال‌سازی کنید.
  2. URL بخشی از رابط کاربری است، بنابراین استفاده از بسیاری از پارامترهای جستجو ممکن است بر خوانایی تأثیر بگذارد.

می توانید کد کامل نمونه را در GitHub مشاهده کنید.

با تشکر فراوان از دلتای درخت زیتون از Vercel برای ارائه بازخورد برای این مقاله!

عکس روی جلد توسط ناسا در Unsplash

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا