手把手教你在Next.js 13中利用React Server组件实现国际化


随着 Next.js 13
和 App Router 测试版的推出,React Server Components 开始公开可用。这种新范例允许不需要 React 交互功能的组件(例如useStateuseEffect)仅保留在服务器端。

受益于这一新功能的一个领域是国际化。传统上,国际化需要在性能上进行权衡,因为加载翻译会导致更大的客户端包,而使用消息解析器会影响应用程序的客户端运行时性能。

React Server Components的承诺是我们可以鱼与熊掌兼得。如果国际化完全在服务器端实现,我们可以为我们的应用程序实现新的性能水平,而将交互功能留给客户端。但是,当我们需要应在国际化消息中反映出来的交互式控制状态时,我们如何使用这种范式呢?

在本文中,我们将探索一款显示来自 Unsplash 的街头摄影图像的多语言应用程序。我们将使用next-intl在 React Server 组件中来实现所有国际化需求,并且我们将研究一种通过简约的客户端足迹引入交互性的技术。
请添加图片描述

从 Unsplash 获取照片

服务器组件的一个主要优点是能够通过async/await直接从内部组件获取数据。我们可以使用它从页面组件中的 Unsplash 获取照片。

但首先,我们需要基于官方 Unsplash SDK 创建 API 客户端。

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组件的 props 传递,因此这是我们通过钩子引入动态标签的机会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。然而,这需要我们将所有组件移至客户端,从而导致包大小增加。

我们还有其他选择吗?是的。这是一项在网络上已经存在多年的功能:搜索参数(有时称为查询参数)。搜索参数对于我们的用例来说是一个很好的选择,因为它们可以在服务器端读取。

因此,让我们修改页面组件以通过 props 接收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. 用 Server Component 呈现国际化的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 引入了hook 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>;
}

添加更多交互性:页面控件

通过引入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 应用程序中实现交互功能的好方法,因为它们有助于减少客户端的包大小。

除了性能之外,使用搜索参数还有其他好处:

  • 可以共享带有搜索参数的 URL,同时保留应用程序状态。
  • 书签也保留状态。
  • 您可以选择与浏览器历史记录集成,从而可以通过后退按钮撤销状态更改。

但请注意,还需要考虑权衡

  • 搜索参数值是字符串,因此您可能需要序列化和反序列化数据类型。
  • URL 是用户界面的一部分,因此使用许多搜索参数可能会影响可读性。

您可以在 GitHub 上查看示例的完整代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值