DEV Community

Cover image for 🏝️ i18n translations in Next.js 13's app-dir for server/client components 🌊
Aral Roca
Aral Roca

Posted on • Originally published at aralroca.com

🏝️ i18n translations in Next.js 13's app-dir for server/client components 🌊

Original article: https://aralroca.com/blog/i18n-translations-nextjs-13-app-dir

In this post, I will explain how to easily load and use translations in the pages within the app directory of Next.js 13, a new paradigm for managing your pages that, if mismanaged, can lead to headaches.

Before we begin... A little context

First, let me tell you about Next-translate. It is one of the most widely used libraries for loading and using translations in Next.js pages (~70k weekly downloads) and has existed since Next.js 9.

By default, translations are stored in locales/{lang}/{namespace}.json:

.
β”œβ”€β”€ en
β”‚Β Β  β”œβ”€β”€ common.json
β”‚Β Β  └── home.json
└── es
    β”œβ”€β”€ common.json
    └── home.json
Enter fullscreen mode Exit fullscreen mode

*However, they can also be loaded from a CDN or integrated with an online internationalization platform like localize.

πŸ¦„ One of the library's goals is to be easy to use. With a little configuration in the i18n.js file, you can already consume these translations in your pages and components:

import useTranslation from 'next-translate/useTranslation'

export default function Example() {
  const { t, lang } = useTranslation('common')
  return <h1>{t('example')}</h1>
}
Enter fullscreen mode Exit fullscreen mode

🌍 Another goal of the library is to have all the i18n essentials: interpolation, plurals, formatters, translations with HTML tags, etc.

πŸ“¦ And another goal is to be as lightweight as possible. In version 1.0, we made it occupy ~1kb, but with version 2.0, there have been even more improvements that we will see in this post.

I won't go into further detail on how the library works. I invite you to read the documentation if you want more details.

Next-translate logo

Now that I have introduced the library, let's see if it is still easy to use, lightweight, and has all the i18n essentials in the new paradigm that Next.js 13 introduced in late 2022: the app directory...

What is the app dir?

The app directory in Next.js 13 (beta) introduces a new routing and data fetching system that supports layouts, nested routes, and utilizes React 18 Server Components by default.

Server components are a new type of React component that executes on the server and on the server only. This means that they are never shipped to the client, resulting in a significant reduction in the amount of code that needs to be shipped to the browser. This reduction in the amount of code can improve the performance of React applications, especially those dealing with large amounts of data.

They were originally thought of as a way to solve the "waterfall problem" in React applications. This occurs when a React component requires data from a server, but that data is not available yet. This can result in a "waterfall" of network requests, as each component waits for the data it needs before it can render, causing significant delays in rendering and impacting the user experience.

With Server components allows the server to fetch all the required data in one go, instead of going back and forth with the client. This results in a significant improvement in the performance of React applications.

They also reduce the amount of code that needs to be shipped to the client, which can improve the load time of React applications. They can also improve the maintainability of applications, as they can make it easier to manage data fetching in large applications.

However, if there are parts of your code that require interactivity with the user, you can create client component islands, where the page only uses the kilobytes of JavaScript used in these islands. As less JavaScript is always better, this approach is ideal.

Merch islands example
Image of islands, from Deno post blog

One of the challenges with this paradigm shift of working with server components and client islands is that they function differently and are like two separate worlds. Therefore, the goal is to find a way to make the usage of both as simple and consistent as possible.

Server components headaches

If a programmer works with both Server Components and Client Components at the same time, they may face some challenges in terms of integration and coordination between the two types of components.

One of the main challenges could be the need to maintain coherence between the data handled on the server and on the client. For example, if a server page loads a translation namespace and all page components need to use these translations, island client components must also have access to the same translations without fetching them from the client, which requires coordinating data requests to avoid unnecessary redundancies and ensure that the data is obtained efficiently to avoid delays in site loading and reduce unnecessary bandwidth usage.

Headache

Finally, there may be challenges in integrating libraries used to work with Server Components and Client Components. Ensuring that these tools work well together can be a challenge. So let's see how we have dealt with these problems at Next-translate.

Load and consume translations in Next.js 13 app dir

In Next-translate 2.0, we have struggled with this issue to make the solution as user-friendly as possible. In the end, we achieved this goal, and no additional configuration is necessary in your i18n.js setup. πŸŽ‰

Simply keep in mind 🧠 that the pages defined in the configuration are now used for the pages within the app directory:

// i18n.js
module.exports = {
  locales: ['en', 'ca', 'es'],
  defaultLocale: 'en',
  pages: {
    '*': ['common'],
    '/': ['home'], // app/page.tsx
    '/checkout': ['checkout'], // app/checkout/page.tsx
  },
}
Enter fullscreen mode Exit fullscreen mode

That's all you need to do! 😜

Our plugin, which was already present Next-translate 1.0, now in Next-translate 2.0 takes care of loading translations in both server and client pages, as well as client component islands within a server page. Furthermore, we have reduced the size of our library from 1.8kb to 498B (minified + gzipped).

Bundlephobia Next-translate 2.0


Bundlephobia capture: v1.6 -> 1.8kb - v2.0 -> 498B

Prior to the app directory, the pages always had 1.8kb of translations in the bundle, but now:

  • If you have all translations in server components: 0kb πŸ₯³
  • If you need client component islands that change the text according to user interaction: 498B πŸŽ‰

To consume these translations, use the useTranslation hook or the Trans component:

🌊 Server page (+0kb): app/page.js:

import useTranslation from 'next-translate/useTranslation'

export default function HomePage() {
  const { t, lang } = useTranslation('home')

  return <h1>{t('title')}</h1>
}
Enter fullscreen mode Exit fullscreen mode

In this case, we are dealing with a server page, as pages are by default server components. Under the hood, the next-translate-plugin dynamically loads translations directly, without the need for the useEffect hook or context. It also sets the language and namespace in the HTML, enabling other components, such as client-side islands, to hydrate and consume the same translations loaded for the page.

🏝️ Client page (+498B): app/checkout/page.js

"use client"
import useTranslation from 'next-translate/useTranslation'

export default function CheckoutPage() {
  const { t, lang } = useTranslation('checkout')

  return <h1>{t('title')}</h1>
}
Enter fullscreen mode Exit fullscreen mode

For client pages, which are not the default in Next.js, it is necessary to indicate that they are client-side using the "use client" line, which is a new feature in the app directory of Next.js 13. By default, if this line is not present, the page is server-side. Under the hood, the next-translate-plugin loads translations for these pages using the useEffect hook. In this case, there is no need to add anything to the HTML because all components will already have access to the translations without the need for hydration.

🏝️ Client component (+498B): components/ClientIsland

"use client"
import useTranslation from 'next-translate/useTranslation'

export default function ClientIsland() {
  const { t, lang } = useTranslation('common')
  return <h1>{t('island')}</h1>
}
Enter fullscreen mode Exit fullscreen mode

The same logic applies to components: if they do not have the "use client" line, they are server-side by default, but if they include it, they become client component islands that can be used on a server page.

In this case, the next-translate-plugin will check whether hydration is necessary, since it is possible that the translations are already accessible if the parent page or component was also a client.

🌊 Server component (+0kb): components/ServerSea

import useTranslation from 'next-translate/useTranslation'

export default function ServerSea() {
  const { t, lang } = useTranslation('common')
  return <h1>{t('sea')}</h1>
}
Enter fullscreen mode Exit fullscreen mode

For server components, the plugin does not perform any transformation, as translations have already been loaded at the page level, and direct access to the translations is available within the useTranslation hook.

🌊🏝️🏝️ Server page with client islands (+498B): app/page.js

import ServerSea from '@components/ServerSea' // this part 0kb
import ClientIsland from '@components/ClientIsland'
import AnotherClientIsland from '@components/AnotherClientIsland'

export default function HomePage() {
  return (
    <>
      <ServerSea />
      <ClientIsland />
      <AnotherClientIsland />
    </>
}
Enter fullscreen mode Exit fullscreen mode

However, if there is a server page with client components, the client components must hydrate what has been provided from the server page and rerender.

i18n routing with app dir

Next.js 10 introduced i18n routing support, allowing pages to be rendered by navigating to /es/page-name, where the page pages/page-name.js was accessed using the useRouter hook to obtain the locale.

However, since the pages have been moved from the pages dir to the app dir, this i18n routing no longer works correctly.

At Next-translate, we have chosen not to re-implement this functionality, as we aim to be a library for translating pages, rather than routing them. We hope that in the future, this feature will be implemented in the app directory, as it is still in beta and many features still need to be supported.

However, all the support currently available is with the lang parameter. That is, /es/page-name?lang=es renders the page app/page-name/page.js, where we have internal access to the lang parameter, and you do not need to do anything extra other than using the useTranslation hook to consume your translations.

All the same, if you wish to use the language as a subpath /es/page-name without the param, you can utilize middleware to append the lang parameter and perform a rewrite:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import i18n from './i18n'

// /es/page-name -> rewrites to -> /es/page-name?lang=es
export function middleware(request: NextRequest) {
  const locale = request.nextUrl.locale || i18n.defaultLocale
  request.nextUrl.searchParams.set('lang', locale)
  return NextResponse.rewrite(request.nextUrl)
}
Enter fullscreen mode Exit fullscreen mode

Here in the middleware, we are not adding the locale as a subpath, but rather eliminating the need to manually add the lang parameter. By default, the subpath still exists, but you cannot use useRouter from next/router to access the locale within the components of the app directory. Therefore, we still need the parameter, even if we hide it from view.

And to navigate:

<Link href={`/?lang=${locale}`} as={`/${locale}`}>{locale}</Link>
Enter fullscreen mode Exit fullscreen mode

If you need more i18n routing features like automatic locale detection you can follow these steps from the Next.js documentation:

Routes in different languages


Routes in different languages

Demo

To conclude, if you are eager to try an example application using Next.js with the app directory and Next-translate, you can take a look at this example:

Conclusion

This article discusses the Next.js 13 paradigm shift in managing pages, which introduces a new routing and data-fetching system that supports layouts, nested routes, and server components. Server components are a new type of React component that executes only on the server, reducing the amount of code that needs to be shipped to the browser and improving the performance of React applications.

The article also discusses the Next-translate library, which is one of the most widely used libraries for loading and using translations in Next.js pages. It aims to be easy to use, have all the i18n essentials, and be as lightweight as possible. The article evaluates whether Next-translate is still easy to use, lightweight, and has all the i18n essentials in the new paradigm of Next.js 13, and explains how to use it.

Image from unsplash

References

Top comments (2)

Collapse
 
alehano profile image
Alex

Would you please provide middleware that if locale not set in URL uses default locale.
For example:
/de/page uses de
/page uses en

Collapse
 
ndmamadoudv profile image
NdMamadouDv • Edited

Hi, I installed the plugin, and it works very well. My question though is : How can I access to the "lang" parameter or the locale ? In a client component or Server component ?