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
*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>
}
π 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.
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.
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.
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
},
}
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).
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>
}
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>
}
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>
}
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>
}
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 />
</>
}
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)
}
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>
If you need more i18n routing features like automatic locale detection you can follow these steps from the Next.js documentation:
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.