Internationalization (i18n) in Next.js 13 with React Server Components
Tags: nextjs, react, webdev, javascript
Published: 2023-04-12 08:32:00

---
title: Internationalization (i18n) in Next.js 13 with React Server Components
published: true
description: Learn how to implement internationalization (i18n) in a Next.js 13 app by utilizing React Server Components
tags: nextjs, react, webdev, javascript
canonical_url: https://www.smashingmagazine.com/2023/03/internationalization-nextjs-13-react-server-components/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d92iccwnc3mh1ih3wosw.jpg
published_at: 2023-04-12 08:32 +0000
---

With the introduction of [Next.js 13](https://beta.nextjs.org/docs/getting-started) and the beta release of the App Router, React Server Components became publicly available. This new paradigm allows components that don’t require React’s interactive features such as `useState` and `useEffect` to remain server-side only.

One area that benefits from this new capability is **internationalization**.
Traditionally, internationalization requires a tradeoff in performance, as loading translations results in larger client-side bundles and using message parsers impacts the client runtime performance of your app.

The promise of **React Server Components** is that we can have our cake and eat it too. If internationalization is implemented entirely on the server side, we can achieve new levels of performance for our apps, leaving the client side for interactive features. But how can we work with this paradigm when we need interactively-controlled states that should be reflected in internationalized messages?

In this article, we’ll explore a multilingual app that displays street photography images from Unsplash. We’ll use [`next-intl`](https://next-intl-docs.vercel.app/) to implement all our internationalization needs in React Server Components and we’ll look at a technique for introducing interactivity with a minimalistic client-side footprint.

![App final](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r7k604wimumk76rukk73.png)

[Interactive demo](https://street-photography-viewer.vercel.app)

## Fetching photos from Unsplash
A key benefit of Server Components is the ability to fetch data directly from inside components via `async`/`await`. We can use this to fetch the photos from Unsplash in our page component.

But first, we need to create our API client based on the official Unsplash SDK.

```tsx
import {createApi} from 'unsplash-js';

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

Once we have our Unsplash API client, we can use it in our page component.

```tsx
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}
    />
  );
}
```

**Note:** We use `Promise.all` to invoke both requests that we need to make in parallel. This way we avoid a request waterfall.

At this point, our app renders a simple photo grid.

![App basic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6rrjop3cj2o357rqbc4x.png)

The app currently uses hard-coded English labels and the dates of the photos are displayed as timestamps—not very user-friendly (yet).

## Adding internationalization with [`next-intl`](https://next-intl-docs.vercel.app/)
In addition to English, we’d like our app to be available in Spanish. Support for Server Components is currently in beta for `next-intl`, so we can use [the installation instructions for the latest beta](https://next-intl-docs.vercel.app/docs/next-13/server-components) to set up our app for internationalization.

### Formatting dates

Aside from adding a second language, we’ve already found that the app doesn’t adapt well to English users because the dates should be formatted. To achieve a good user experience, we’d like to tell the user the relative time when the photo was uploaded (e.g. “8 days ago”).

Once  `next-intl` is set up, we can fix the formatting by using the `format.relativeTime` function in the component that renders each photo.

```tsx
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>
  );
}
```

Now the date when a photo has been updated is easier to read.

![Date formatted in app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gtnli4cuw5uahy132op5.png)

**Hint:** In a traditional React app that renders on both the server and client side, it can be quite a challenge to ensure that the displayed relative date is in sync across the server and client. Since these are different environments and may be in different time zones, you need to configure a mechanism to transfer the server time to the client side. By performing the formatting only on the server side, we don’t have to worry about this problem in the first place.

### ¡Hola! 👋 Translating our app to Spanish

Next, we can replace the static labels in the header with localized messages. These labels are passed as props from the `PhotoViewer` component, so this is our chance to introduce dynamic labels via the `useTranslations` hook.

```tsx
import {useTranslations} from 'next-intl';

export default function PhotoViewer(/* ... */) {
  const t = useTranslations('PhotoViewer');

  return (
    <>
      <Header
        title={t('title')}
        description={t('description')}
      />
      {/* ... */}
    </>
  );
}
```

For each internationalized label we add, we need to make sure that there is an appropriate entry set up for all languages.

```js
// 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."
  }
}
```

```js
// 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."
  }
}
```

**Tip:** [`next-intl` provides a TypeScript integration](https://next-intl-docs.vercel.app/docs/usage/typescript) that helps you ensure that you’re only referencing valid message keys.

Once this is done, we can visit the Spanish version of the app at `/es`.

![App translated to Spanish](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pmc6fep8153cuzee2llg.png)

So far, so good!

## Adding interactivity: Dynamic ordering of photos
By default, the Unsplash API returns the most popular photos. We want the user to be able to change the order to show the most recent photos first.

Here, the question arises whether we should resort to client-side data fetching so that we can implement this feature with `useState`. However, that would require us to move all of our components to the client side, resulting in an increased bundle size.

Do we have an alternative? Yes. And it’s a capability that has been around on the web for ages: [search parameters](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) (sometimes referred to as *query parameters*). What makes search parameters a great option for our use case is that they can be read on the server side.

So let’s modify our page component to receive `searchParams` via props.

```tsx
export default async function Index({searchParams}) {
  const orderBy = searchParams.orderBy || OrderBy.POPULAR;

  const [/* ... */, photosRequest] = await Promise.all([
    /* ... */,
    UnsplashApiClient.topics.getPhotos({orderBy, /* ... */})
  ]);
```

After this change, the user can navigate to `/?orderBy=latest` to change the order of the displayed photos.

To make it easy for the user to change the value of the search parameter, we’d like to render an interactive [`select` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) from within a component.

![App order select](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t9etnmxgf0ot3j2jqaez.png)

We can mark the component with `'use client';` to attach an event handler and process change events from the `select` element. Nevertheless, we would like to keep the internationalization concerns on the server side to reduce the size of the client bundle.

Let’s have a look at the required markup for our `select` element.

```html
<select>
  <option value=“popular”>Popular</option>
  <option value="latest">Latest</option>
</select>
```

We can split this markup into two parts:

1. Render the `select` element with an interactive Client Component.
2. Render the internationalized `option` elements with a Server Component and pass them as `children` to the `select` element.

Let’s implement the `select` element for the client side.

```tsx
'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>
  );
}
```

Now, let’s use our component in `PhotoViewer` and provide the localized `option` elements as  `children`.

```tsx
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>
    </>
  );
}
```

With this pattern, the markup for the  `option` elements is now generated on the server side and passed to the `OrderBySelect`, which handles the change event on the client side.

**Tip:** Since we have to wait for the updated markup to be generated on the server side when the order is changed, we may want to show the user a loading state. React 18 introduced [the `useTransition` hook](https://beta.reactjs.org/reference/react/useTransition)  which is integrated with Server Components. This allows us to disable the `select` element while waiting for a response from the server.

```tsx
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>
  );
}
```

## Adding more interactivity: Page controls
The same pattern that we’ve explored for changing the order can be applied to page controls by introducing a `page` search parameter.

![Pagination](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51dh6xgy4ndq15g9dah0.png)

Note that languages have different rules for handling decimal and thousand separators. Furthermore, languages have different forms of pluralization: While English only makes a grammatical distinction between one and zero/many elements, for example Croatian has a separate form for “few” elements.

`next-intl` uses the [ICU syntax](https://next-intl-docs.vercel.app/docs/usage/messages#rendering-of-messages) which makes it possible to express these language subtleties.

```js
// en.json
{
  "Pagination": {
    "info": "Page {page, number} of {totalPages, number} ({totalElements, plural, =1 {one result} other {# results}} in total)",
    // ...
  }
}
```

This time we don’t need to mark a component with `'use client';`. Instead, we can implement this with regular anchor tags.

```tsx
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>
      )}
    </>
  );
}
```

- - - -

## Conclusion

### Server Components are a great match for internationalization

Internationalization is an important part of the user experience, whether you support multiple languages or you want to get the subtleties of a single language right. A library like [`next-intl`](https://next-intl-docs.vercel.app/) can help with both cases.

Implementing internationalization in Next.js apps has historically come with a performance tradeoff, but with Server Components this is no longer the case. However, it might take some time to explore and learn patterns that will help you keep your internationalization concerns on the server side.

In our street photography viewer app, we only needed to move a single component to the client side: `OrderBySelect`.

![Components diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfs49abbfh4435jsyo0u.png)

Another aspect to consider is that you might want to consider implementing loading states since the network latency introduces a delay before your users see the result of their actions.

### Search parameters are a great alternative to `useState`

Search parameters are a great way to implement interactive features in Next.js apps, as they help to reduce the bundle size of the client side.

Apart from performance, there are other **benefits of using search parameters**:

1. URLs with search parameters can be shared while preserving the application state.
2. Bookmarks preserve the state as well.
3. You can optionally integrate with the browser history, enabling undoing state changes via the back button.

Note however that there are also **tradeoffs to consider**:

1. Search parameter values are strings, so you may need to serialize and deserialize data types.
2. The URL is part of the user interface, so using many search parameters may affect readability.

- - - -

You can have a look at the complete [code of the example on GitHub](https://github.com/amannn/street-photography-viewer).

Many thanks to [Delba de Oliveira](https://twitter.com/delba_oliveira) from Vercel for providing feedback for this article!

Cover photo by [NASA](https://unsplash.com/de/@nasa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/de/fotos/Q1p7bh3SHj8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)