Quốc tế hóa trong Next.js 13 với các thành phần React Server

theanh

Administrator
Nhân viên
Với sự ra mắt của Next.js 13 và bản phát hành beta của App Router, React Server Components đã có sẵn cho công chúng. Mô hình mới này cho phép các thành phần không yêu cầu các tính năng tương tác của React, chẳng hạn như useStateuseEffect, chỉ duy trì ở phía máy chủ.

Một lĩnh vực được hưởng lợi từ khả năng mới này là quốc tế hóa. Theo truyền thống, quốc tế hóa đòi hỏi phải đánh đổi về hiệu suất vì việc tải bản dịch dẫn đến các gói phía máy khách lớn hơn và việc sử dụng trình phân tích cú pháp tin nhắn sẽ ảnh hưởng đến hiệu suất thời gian chạy máy khách của ứng dụng của bạn.

Lời hứa của React Server Components là chúng ta có thể vừa có bánh vừa ăn bánh. Nếu quốc tế hóa được triển khai hoàn toàn ở phía máy chủ, chúng ta có thể đạt được mức hiệu suất mới cho các ứng dụng của mình, để lại phía máy khách cho các tính năng tương tác. Nhưng làm sao chúng ta có thể làm việc với mô hình này khi chúng ta cần các trạng thái được kiểm soát tương tác và cần được phản ánh trong các thông điệp quốc tế?

Trong bài viết này, chúng ta sẽ khám phá một ứng dụng đa ngôn ngữ hiển thị hình ảnh chụp ảnh đường phố từ Unsplash. Chúng tôi sẽ sử dụng next-intl để triển khai mọi nhu cầu quốc tế hóa của mình trong React Server Components và chúng tôi sẽ xem xét một kỹ thuật để giới thiệu tính tương tác với dấu chân phía máy khách tối giản.


Lấy ảnh từ Unsplash​

Một lợi ích chính của Server Components là khả năng lấy dữ liệu trực tiếp từ bên trong các thành phần thông qua async/await. Chúng ta có thể sử dụng điều này để lấy ảnh từ Unsplash trong thành phần trang của mình.

Nhưng trước tiên, chúng ta cần tạo ứng dụng khách API dựa trên Unsplash SDK chính thức.
Mã:
import {createApi} from 'unsplash-js';export default createApi({ accessKey: process.env.UNSPLASH_ACCESS_KEY});
Sau khi có ứng dụng khách API Unsplash, chúng ta có thể sử dụng nó trong thành phần trang của mình.
Mã:
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 ( 
 );}
Lưu ý: Chúng tôi sử dụng Promise.all để gọi cả hai yêu cầu mà chúng tôi cần thực hiện song song. Bằng cách này, chúng ta tránh được thác nước yêu cầu.

Tại thời điểm này, ứng dụng của chúng ta sẽ hiển thị một lưới ảnh đơn giản.



Ứng dụng hiện đang sử dụng nhãn tiếng Anh được mã hóa cứng và ngày chụp ảnh được hiển thị dưới dạng dấu thời gian, chưa thân thiện với người dùng (hiện tại).

Thêm quốc tế hóa bằng next-intl

Ngoài tiếng Anh, chúng tôi muốn ứng dụng của mình có sẵn bằng tiếng Tây Ban Nha. Hỗ trợ cho Server Components hiện đang trong giai đoạn beta cho next-intl, vì vậy chúng tôi có thể sử dụng hướng dẫn cài đặt cho bản beta mới nhất để thiết lập ứng dụng của chúng tôi cho mục đích quốc tế hóa.

Định dạng ngày tháng​

Ngoài việc thêm ngôn ngữ thứ hai, chúng tôi đã phát hiện ra rằng ứng dụng không thích ứng tốt với người dùng tiếng Anh vì ngày tháng cần phải được định dạng. Để đạt được trải nghiệm người dùng tốt, chúng tôi muốn cho người dùng biết thời gian tương đối khi ảnh được tải lên (ví dụ: "8 ngày trước").

Sau khi thiết lập next-intl, chúng tôi có thể sửa định dạng bằng cách sử dụng hàm format.relativeTime trong thành phần hiển thị từng ảnh.
Mã:
nhập {useFormatter} từ 'next-intl';xuất mặc định hàm PhotoGridItem({photo}) { const format = useFormatter(); const updatedAt = new Date(photo.updated_at); trả về (  {/* ... */} 
{format.relativeTime(updatedAt)}
   );}
Giờ đây, ngày ảnh được cập nhật dễ đọc hơn.



Gợi ý: Trong ứng dụng React truyền thống hiển thị trên cả phía máy chủ và phía máy khách, việc đảm bảo ngày tương đối được hiển thị đồng bộ trên cả máy chủ và máy khách có thể là một thách thức khá lớn. Vì đây là những môi trường khác nhau và có thể ở các múi giờ khác nhau, bạn cần cấu hình một cơ chế để chuyển thời gian của máy chủ sang phía máy khách. Bằng cách chỉ thực hiện định dạng ở phía máy chủ, chúng ta không phải lo lắng về vấn đề này ngay từ đầu.

¡Hola! 👋 Dịch ứng dụng của chúng ta sang tiếng Tây Ban Nha​

Tiếp theo, chúng ta có thể thay thế các nhãn tĩnh trong tiêu đề bằng các thông báo được bản địa hóa. Các nhãn này được truyền dưới dạng props từ thành phần PhotoViewer, vì vậy đây là cơ hội để chúng ta giới thiệu các nhãn động thông qua hook useTranslations.
Mã:
import {useTranslations} from 'next-intl';export default function PhotoViewer(/* ... */) { const t = useTranslations('PhotoViewer'); return (   {/* ... */}  );}
Đối với mỗi nhãn quốc tế hóa mà chúng ta thêm vào, chúng ta cần đảm bảo rằng có một mục nhập phù hợp được thiết lập cho tất cả các ngôn ngữ.
Mã:
// en.json{ "PhotoViewer": { "title": "Nhiếp ảnh đường phố", "description": "Nhiếp ảnh đường phố ghi lại những khoảnh khắc trong đời thực và tương tác của con người ở những nơi công cộng. Đây là một cách để kể những câu chuyện trực quan và đóng băng những khoảnh khắc thoáng qua của thời gian, biến những điều bình thường thành phi thường." }
Mã:
// es.json{ "PhotoViewer": { "title": "Nhiếp ảnh đường phố", "description": "La fotografía callejera capta khoảnh khắc de la vida real y interacciones humanas en lugares públicos. Es una forma de contar historys visuales and congelar momentos fugaces del tiempo, convirtiendo lo normalario en lo extra normalario." }}
Mẹo: next-intl cung cấp tích hợp TypeScript giúp bạn đảm bảo rằng bạn chỉ tham chiếu đến các khóa tin nhắn hợp lệ.

Sau khi hoàn tất, chúng ta có thể truy cập phiên bản tiếng Tây Ban Nha của ứng dụng tại /es.



Cho đến giờ thì mọi thứ vẫn ổn!

Thêm tính tương tác: Sắp xếp ảnh động​

Theo mặc định, API Unsplash trả về những bức ảnh phổ biến nhất. Chúng tôi muốn người dùng có thể thay đổi thứ tự để hiển thị những bức ảnh gần đây nhất trước.

Ở đây, câu hỏi đặt ra là liệu chúng ta có nên sử dụng phương pháp truy xuất dữ liệu phía máy khách để có thể triển khai tính năng này bằng useState hay không. Tuy nhiên, điều đó sẽ yêu cầu chúng ta phải di chuyển tất cả các thành phần của mình sang phía máy khách, dẫn đến tăng kích thước gói.

Chúng ta có giải pháp thay thế không? Có. Và đó là một khả năng đã tồn tại trên web trong nhiều thời đại: tham số tìm kiếm (đôi khi được gọi là tham số truy vấn). Điều khiến tham số tìm kiếm trở thành một tùy chọn tuyệt vời cho trường hợp sử dụng của chúng ta là chúng có thể được đọc ở phía máy chủ.

Vì vậy, hãy sửa đổi thành phần trang của chúng ta để nhận searchParams thông qua props.
Mã:
export default async function Index({searchParams}) { const orderBy = searchParams.orderBy || OrderBy.POPULAR; const [/* ... */, photosRequest] = await Promise.all([ /* ... */, UnsplashApiClient.topics.getPhotos({orderBy, /* ... */}) ]);
Sau khi thay đổi này, người dùng có thể điều hướng đến /?orderBy=latest để thay đổi thứ tự của các ảnh được hiển thị.

Để người dùng dễ dàng thay đổi giá trị của tham số tìm kiếm, chúng tôi muốn hiển thị một phần tử select tương tác từ bên trong một thành phần.



Chúng ta có thể đánh dấu thành phần bằng 'use client'; để đính kèm trình xử lý sự kiện và xử lý các sự kiện thay đổi từ phần tử select. Tuy nhiên, chúng tôi muốn giữ lại các mối quan tâm về quốc tế hóa ở phía máy chủ để giảm kích thước của gói máy khách.

Chúng ta hãy xem xét đánh dấu bắt buộc cho phần tử select của chúng ta.
Mã:
 Popular Latest
Chúng ta có thể chia đánh dấu này thành hai phần:
  1. Kết xuất phần tử select bằng một Thành phần Máy khách tương tác.
  2. Kết xuất các phần tử option được quốc tế hóa bằng một Thành phần Máy chủ và truyền chúng dưới dạng children cho phần tử select.
Chúng ta hãy triển khai phần tử select cho phía máy khách.
Mã:
'use client';import {useRouter} from 'next-intl/client';export default function OrderBySelect({orderBy, children}) { const router = useRouter(); function onChange(event) { // Móc `useRouter` từ `next-intl` tự động // xem xét tiền tố ngôn ngữ tiềm năng của tên đường dẫn. router.replace('/?orderBy=' + event.target.value); } return (  {children}  );}
Bây giờ, hãy sử dụng thành phần của chúng ta trong PhotoViewer và cung cấp các phần tử option được bản địa hóa dưới dạng children.
Mã:
import {useTranslations} from 'next-intl';import OrderBySelect from './OrderBySelect';export default function PhotoViewer({orderBy, /* ... */}) { const t = useTranslations('PhotoViewer'); return (  {/* ... */}  {t('orderBy.popular')} {t('orderBy.latest')}   );}
Với mẫu này, đánh dấu cho các phần tử option hiện được tạo ở phía máy chủ và được chuyển đến OrderBySelect, xử lý sự kiện thay đổi ở phía máy khách.

Mẹo: Vì chúng ta phải đợi đánh dấu cập nhật được tạo ở phía máy chủ khi đơn hàng thay đổi, chúng ta có thể muốn hiển thị trạng thái đang tải cho người dùng. React 18 đã giới thiệu móc useTransition, được tích hợp với Server Components. Điều này cho phép chúng ta vô hiệu hóa phần tử select trong khi chờ phản hồi từ máy chủ.
Mã:
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 (  {children}  );}

Thêm nhiều tính tương tác hơn: Điều khiển trang​

Có thể áp dụng cùng một mẫu mà chúng tôi đã khám phá để thay đổi thứ tự cho các điều khiển trang bằng cách giới thiệu tham số tìm kiếm trang.



Lưu ý rằng các ngôn ngữ có các quy tắc khác nhau để xử lý dấu phân cách thập phân và dấu phân cách phần nghìn. Hơn nữa, các ngôn ngữ có các dạng số nhiều khác nhau: trong khi tiếng Anh chỉ phân biệt ngữ pháp giữa một và không/nhiều phần tử, ví dụ, tiếng Croatia có một dạng riêng cho 'một vài' phần tử.

next-intl sử dụng cú pháp ICU giúp thể hiện các nét tinh tế của ngôn ngữ này.
Mã:
// en.json{ "Phân trang": { "info": "Trang {page, number} trong tổng số {totalPages, number} ({totalElements, plural, =1 {one result} other {# results}} in total)", // ... }}
Lần này chúng ta không cần đánh dấu một thành phần bằng 'use client';. Thay vào đó, chúng ta có thể triển khai điều này bằng các thẻ neo thông thường.
Mã:
import {ArrowLeftIcon, ArrowRightIcon} from '@heroicons/react/24/solid';import {Link, useTranslations} from 'next-intl';export default function Pagination({pageInfo, orderBy}) { const t = useTranslations('Phân trang'); const totalPages = Math.ceil(pageInfo.totalElements / pageInfo.size); function getHref(page) { return { // Vì chúng ta đang sử dụng `Link` từ next-intl, nên tiền tố ngôn ngữ tiềm năng của pathname sẽ được tự động xem xét. pathname: '/', // Giữ tham số `orderBy` có thể tồn tại. query: {orderBy, page} }; } trả về (  {pageInfo.page > 1 && ( 
[*]    )} 
{t('info', {...pageInfo, totalPages})}
 {pageInfo.page < totalPages && (    )}  );}

Kết luận​

Thành phần máy chủ rất phù hợp với quốc tế hóa​

Quốc tế hóa là một phần quan trọng của trải nghiệm người dùng, cho dù bạn hỗ trợ nhiều ngôn ngữ hay muốn nắm bắt đúng những nét tinh tế của một ngôn ngữ duy nhất. Một thư viện như next-intl có thể giúp ích cho cả hai trường hợp.

Việc triển khai quốc tế hóa trong các ứng dụng Next.js trước đây đi kèm với sự đánh đổi về hiệu suất, nhưng với Thành phần máy chủ, điều này không còn đúng nữa. Tuy nhiên, có thể mất một thời gian để khám phá và tìm hiểu các mẫu giúp bạn giữ mối quan tâm quốc tế hóa của mình ở phía máy chủ.

Trong ứng dụng xem ảnh đường phố của chúng tôi, chúng tôi chỉ cần di chuyển một thành phần duy nhất sang phía máy khách: OrderBySelect.



Một khía cạnh khác cần lưu ý là bạn có thể muốn cân nhắc triển khai trạng thái tải vì độ trễ mạng gây ra sự chậm trễ trước khi người dùng của bạn thấy kết quả của hành động.

Tham số tìm kiếm là một giải pháp thay thế tuyệt vời cho useState

Tham số tìm kiếm là một cách tuyệt vời để triển khai các tính năng tương tác trong ứng dụng Next.js vì chúng giúp giảm kích thước gói của phía máy khách.

Ngoài hiệu suất, còn có lợi ích khác khi sử dụng tham số tìm kiếm:
  • URL có tham số tìm kiếm có thể được chia sẻ trong khi vẫn giữ nguyên trạng thái ứng dụng.
  • Dấu trang cũng giữ nguyên trạng thái.
  • Tùy chọn, bạn có thể tích hợp với lịch sử trình duyệt, cho phép hoàn tác các thay đổi trạng thái thông qua nút quay lại.
Tuy nhiên, lưu ý rằng cũng có sự đánh đổi cần cân nhắc:
  • Giá trị tham số tìm kiếm là chuỗi, do đó bạn có thể cần tuần tự hóa và hủy tuần tự hóa các kiểu dữ liệu.
  • URL là một phần của giao diện người dùng, do đó, việc sử dụng nhiều tham số tìm kiếm có thể ảnh hưởng đến khả năng đọc.
Bạn có thể xem mã đầy đủ của ví dụ trên GitHub.

Xin chân thành cảm ơn Delba de Oliveira từ Vercel đã cung cấp phản hồi cho bài viết này!

Đọc thêm trên SmashingMag​

  • “Hiểu kiến trúc thư mục ứng dụng trong Next.js”, Atila Fassina
  • “Thiết kế cho người dùng trên nhiều nền văn hóa: Phỏng vấn Jenny Shen”, Rachel Andrew
  • “Lấy dữ liệu động trong ứng dụng Next.js đã xác thực”, Caleb Olojo
  • “Cách triển khai chức năng tìm kiếm trong ứng dụng Nuxt của bạn bằng Algolia InstantSearch”, Miracle Onyenma
 
Back
Bên trên