More libraries to the library god or how I remade i18n [next.js v14]
There are dozens of amazing libraries made for internationalization, such as i18n, react-intl, next-intl. They all do an excellent job of adding translations to an application or website. Most of them are tested, debugged, and consistently supported.
But they are all outdated.
After all, during this time, the react ecosystem has been developing. The latest version of next.js has major updates from react.js — cache, taint, new hooks, and, of course, server components. The React.js team will likely introduce these changes in May.
In this article, I will talk about key changes, personal experience, problems with existing solutions, necessary updates, solutions I came up with, and, of course, answer the questions of “why” and most importantly — “why”?
Changes
The first thing to start with is how the changes in React.js made translation libraries obsolete.
Despite the fact that the latest stable version of React.js was released almost two years ago, it has 2 other channels — canary and experimental, where canary is also considered a stable channel and is recommended for use by libraries.
This is the channel that Next.js uses. Next.js launched server components without additional flags inside the so-called App Router — a new directory as an alternative to pages, which uses its conventions and different sugar (about changes and problems which I wrote in a recent article).
Server components definitely solve a number of problems and are a new milestone for optimizations. Including for translations. Without server components, translations were stored both in the compiled HTML and as a large object in the client script. Now you can get ready-made HTML, which doesn’t need anything on the client.
Next.js paid special attention to this feature.
Personal experience
You can add translations (according to the Next.js documentation) as follows:
// app/[lang]/dictionaries.js
import 'server-only'
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}
export const getDictionary = async (locale) => dictionaries[locale]()
// app/[lang]/page.js
import { getDictionary } from './dictionaries'
export default async function Page({ params: { lang } }) {
const dict = await getDictionary(lang) // en
return <button>{dict.products.cart}</button> // Add to Cart
}
This solution is described as ready and fully optimized. It works entirely on the server, and the client already receives ready-made HTML. However, the Next.js team missed one important detail — how to pass the language deep into server components.
A big problem with server components is that contexts are not available in them. The Next.js team explains the absence of these functions by the fact that Layout is not re-rendered, and everything that depends on props should be client-side.
Perhaps translation libraries were most affected by this. As a temporary solution, they suggest determining the language in the middleware and adding it to cookies. Then when building the page, read it in the necessary places. But reading cookies means enabling server rendering, which is not suitable for everyone.
In general, the main problem with existing solutions is that most of them are not made for server components. Components and functions were developed for runtime, using hooks and synchronicity.
Another inconvenience was caching in Next.js. Namely — it works fully only for GET requests, and if the weight of translations is more than the limit of 2MB — they will not be cached.
Implementation
Goals and tasks:
- The library must have full functionality in both client and server components;
- Usage should be simple (without extra props);
- All complex logic should be moved to the server;
- Translations should be loaded only as necessary and without additional requests;
- Support for updating translations without rebuilding (ISR/SSR);
- Everything should work on a static site (not with switching to SSR);
- Html entities should be supported.
To my surprise, there is not a single library that would satisfy all these requirements.
The first thing you need is functionality. In the standard version, this is a hook that returns the function t
and the Trans
component for more complex translations. However, such functionality is needed in server components, and they have many of their own features.
Functionality
The main functionality is divided into two versions — for client components and for server ones and includes:
useTranslation
, getTranslation
- which return the function t inside the DOM and the language;
import getTranslation from 'next-translation/getTranslation'
export default function ServerComponent() {
const { t } = getTranslation()
return (
<p>{t('intro.title')}</p>
)
}
'use client';
import useTranslation from 'next-translation/useTranslation'
export default function ClientComponent() {
const { t } = useTranslation()
return (
<p>{t('intro.title')}</p>
)
}
The interface turned out to be quite familiar, the functions support namespace
and query
. It is recommended to use it by default, as it is simple to use and in logic. Returns a ready-made string.
For more complex translations, you should use the ClientTranslation
and ServerTranslation
components. They can replace pseudo-components with real ones.
import ServerTranslation from 'next-tranlation/ServerTranslation';
export default function ServerComponent() {
return(
<ServerTranslation
term='intro.description'
components={{
link: <a href='#' />
}}
/>
)
}
"use client";
import ClientTranslation from 'next-tranlation/ClientTranslation';
export default function ClientTranslation() {
return(
<ClientTranslation
term='intro.description'
components={{
link: <a href='#' />
}}
/>
)
}
There are also cases when translations need to be added outside the react tree. For this, you can use createTranslation
anywhere.
import createTranslation from 'next-translation/createTranslation'
// ...
export async function generateMetadata({ params }: { params: { lang: string } }) {
const { t } = await createTranslation(params.lang);
return {
title: t('homePage.meta.title'),
}
}
Page setup
Now about setting up the page. To work with translations, you need to know the language. However, in server components, you cannot use context. For this, an alternative to createContext
was made for server components in the next-impl-getters package - createServerContext
and getServerContext
.
In the package for this, you need to create a NextTranslationProvider
. It is recommended to do this at the page level to avoid problems with Layout re-rendering.
import NextTranlationProvider from 'next-translation/NextTranlationProvider'
export default function HomePage({ params }: { params: { lang: string } }) {
return (
<NextTranlationProvider lang={params.lang} clientTerms={['shared', 'banking.about']}>
{/* ... */}
</NextTranlationProvider>
)
}
It is also necessary to indicate which translations are needed specifically on the client and to pass only them there. To do this, you can pass an array of client keys or groups to NextTranslationProvider
using the clientTerms
prop.
Also, sometimes situations arise when a component needs different translations or different blocks are rendered depending on conditions. In such cases, different translations need to be passed to the client. Condition options can be wrapped in NextTranslationTransmitter
and client terms are passed to it.
import NextTranslationTransmitter from 'next-tranlation/NextTranslationTransmitter';
import ClientComponent from './ClientComponent';
const ServerComponent: React.FC = () => (
<NextTranslationTransmitter terms={['header.nav']}>
<ClientComponent />
</NextTranslationTransmitter>
)
As a result, only those terms that were specified above in NextTranslationProvider
or NextTranslationTransmitter
will be passed to client components.
Package setup
Before working with translations, they need to be loaded. For this, you need to create a configuration file in the root of the project. Its minimum configuration is the load
function, which will return current translations and an array of languages
with permissible languages. The load
function is called in server components, and the necessary keys will be passed to the client.
A very important point was the absence of unnecessary requests, that is, full caching is needed.
Here it is worth digressing a little. Starting from the latest version, Next.js builds the application in parallel in several processes. If each process lived with its own cache — requests would be sent from each. Probably, to avoid this, the Next.js team redesigned fetch — now it works with a common cache.
The package solves the problem in the same way — it creates a common cache and works from each process already with it. For this to work, you need to use withNextTranslation
in next.config.js
.
Conclusion
The solution turned out to be truly tailored to next.js — taking into account all its capabilities and problems. It also includes all the optimization capabilities provided by server components. The package is fully optimized for next.js, their concepts and views, which I fully share.
I faced the problem of translations and I had to make my own solution, which would work exactly as expected.
Despite the significant advantage in optimizations, the package is still inferior to large libraries in terms of translation capabilities. There is a lot of work ahead.
P.S. I will be grateful if you describe what you lacked in existing solutions or what functionality you consider most important.