Skip to main content

How to setup Tolgee with Next.js App Router

· 9 min read
Štěpán Granát

With the introduction of Next.js 13 and the app router featuring React server components, there has been a strong push to enable Tolgee to work within this new paradigm.

Considering the app router is still in beta and Next.js API adjustments might occur, we opted for this article (instead of creating an npm package right away) to explain how Tolgee can work with the server.

info

This article is outdated in technical details, read the new documentation.

Server components are a stripped-down version of regular components without React hooks but with async capabilities. Let's see, how we can set them up with Tolgee.

How Tolgee In-Context Works

Tolgee's unique method of enabling users to directly edit translations within the app is based on inserting invisible characters next to the actual translations. This creates a kind of watermark for each translation, making it detectable in the DOM and allowing precise localization. This approach offers a non-intrusive way to empower users to translate in the context of their app.

The General Plan

For server components, the approach involves including a complete key name within the invisible watermark characters. Additionally, the development translation files need to be loaded on the server to facilitate rendering. On the client side, the SDK will recognize the server-rendered watermarks and enable the in-context translation functionality.

In Development mode, data will be fetched directly from Tolgee platform with each request to the server. In Production mode, we'll include the locale data directly into the bundle, so no fetching is necessary.

Setting Up the Configuration

To initiate a new project, we will create a fresh Next.js 13 project (with the app directory enabled):

npx create-next-app@latest

As the i18n support is currently limited for the app directory, we'll use an external library called next-intl to assist with routing and locale management.

For now, we need to install a beta version (updated as of the writing date) and the latest version of @tolgee/react:

npm install next-intl@3.0.0-beta.9 @tolgee/react

Configuring next-intl and Tolgee

The folder structure needs to be adjusted to resemble the following:

├── next.config.js
├── middleware.ts
├── i18n
│ ├── en.json
│ └── de.json
├── tolgee
│ ├── shared.ts
│ ├── client.tsx
│ └── server.tsx
└── app
└── [locale]
├── layout.tsx
└── page.tsx

The app structure and middleware play essential roles in the next-intl setup. The Tolgee folder serves for both client and server configurations with shared properties. The i18n folder contains the static files (exported from the Tolgee platform or left empty for now).

Below is the configuration for the middleware file:

// middleware.ts

import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en',
});

export const config = {
// Skip all paths that should not be internationalized
matcher: ['/((?!api|_next|.*\\..*).*)'],
};

To gain a comprehensive understanding of how next-intl operates, check their docs. We are only utilizing the necessary setup for proper routing, hence not all the listed configurations are required.

Now, let's establish a shared configuration that will apply to both the client and server.

For this to work, create a project in tolgee platform, get the api key in integration section. Also I assume you have your exported language files in i18n folder (as is visible in the file structure above).

// shared.ts

import { DevTools, Tolgee, FormatSimple } from '@tolgee/web';

export const ALL_LOCALES = ['en', 'de'];

export const DEFAULT_LOCALE = 'en';

const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;

export async function getStaticData(languages: string[]) {
const result: Record<string, any> = {};
for (const lang of languages) {
result[lang] = (await import(`../i18n/${lang}.json`)).default;
}
return result;
}

export function TolgeeBase() {
return (
Tolgee()
.use(FormatSimple())
.use(DevTools())
// Preset shared settings
.updateDefaults({
apiKey,
apiUrl,
})
);
}

The client part remains largely unchanged from the pages directory setup. It serves the purpose of translating client components and also enables in-context functionality for server-rendered components.

// client.tsx

'use client';

import { TolgeeBase } from './shared';
import { TolgeeProvider, useTolgeeSSR } from '@tolgee/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

type Props = {
locales: any;
locale: string;
children: React.ReactNode;
};

const tolgee = TolgeeBase().init();

export const TolgeeNextProvider = ({ locale, locales, children }: Props) => {
// This synchronizes Tolgee for the server and client's initial render,
// ensuring proper initialization with language and cache const tolgeeSSR = useTolgeeSSR(tolgee, locale, locales);
const router = useRouter();

useEffect(() => {
const { unsubscribe } = tolgeeSSR.on('permanentChange', () => {
// This ensures that server components refresh after
// translation modifications using in-context
router.refresh();
});

return () => unsubscribe();
}, [tolgeeSSR, router]);

return (
<TolgeeProvider tolgee={tolgeeSSR} options={{ useSuspense: false }}>
{children}
</TolgeeProvider>
);
};

The only significant change is the listener for the permanentChange event. This event triggers when a translation is updated through an in-context dialog, allowing for a server component refresh with router.refresh.

Now, let's proceed to the server part. As server components don't support React hooks, we need to recreate similar abstractions to those found in @tolgee/react ourselves. Fortunately, this process is relatively straightforward; we can essentially utilize vanilla Tolgee, with proper configuration and caching of the instance.

// server.tsx

import { cache } from 'react';
import { useLocale } from 'next-intl';

import { TolgeeBase, ALL_LOCALES, getStaticData } from './shared';

// wrapping in `cache` function will ensure
// that we are sharing the instance within a single request
export const getTolgeeInstance = cache(async (locale: string) => {
const tolgee = TolgeeBase().init({
// include all static data on the server, as the bundle size is not a concern here
staticData: await getStaticData(ALL_LOCALES),
observerOptions: {
// include full information about the key into the watermark
// make sure you have newest SDK for this feature
fullKeyEncode: true,
},
// locale is already detected by next-intl package
language: locale,
// providing custom fetch function, which will disable default caching
fetch: async (input, init) => {
return fetch(input, { ...init, next: { revalidate: 0 } });
},
});

await tolgee.run();

return tolgee;
});

export const getTolgee = async () => {
const locale = useLocale();
const tolgee = await getTolgeeInstance(locale);
return tolgee;
};

export const getTranslate = async () => {
const tolgee = await getTolgee();
return tolgee.t;
};

Re-creation of T component is a bit more complicated, because we need to copy some code from @tolgee/react, but you check how to do it in the example repo.

Let's set up the provider

Here is how we apply the TolgeeNextProvider in the layout.tsx

// layout.tsx

import { notFound } from 'next/navigation';
import { useLocale } from 'next-intl';
import { ReactNode } from 'react';
import { TolgeeNextProvider } from 'tolgee/client';
import { getStaticData } from 'tolgee/shared';

type Props = {
children: ReactNode;
params: { locale: string };
};

export default async function LocaleLayout({ children, params }: Props) {
const locale = useLocale();

const locales = await getStaticData(['en', locale]);

// Show a 404 error if the user requests an unknown locale
if (params.locale !== locale) {
notFound();
}

return (
<html lang={locale}>
<body>
<TolgeeNextProvider locale={locale} locales={locales}>
{children}
</TolgeeNextProvider>
</body>
</html>
);
}

This is an equivalent of getInitialProps (or its related methods) because this is the place where we load pick relevant locale already on the backend and supply it to the client component through the props.

If you want to provide each page with different namespace, you can move the provider to the page files, however this example provides the translations globally

How to use it

Let's see how we can localize server components:

// page.tsx

import { getTranslate } from 'tolgee/server';

export default async function IndexPage() {
// because this is server component, use `getTranslate`
// not useTranslate from '@tolgee/react'
const t = await getTranslate();
return (
<main>
<h1>{t('page-example-title')}</h1>
</main>
);
}

If everything is set up correctly, the 'page-example-title' should be alt + clickable. Make sure you've defined your project credentials in the .env.development.local file.

For client components, you can use the regular React integration:

'use client';

import { useTranslate } from '@tolgee/react';

export const ExampleClientComponent = () => {
const { t } = useTranslate();

return (
<section>
<span>{t('example-key-in-client-component')}</span>
</section>
);
};

Switching Languages

For switching languages use the following code:

'use client';

import React, { ChangeEvent, useTransition } from 'react';
import { usePathname, useRouter } from 'next-intl/client';
import { useTolgee } from '@tolgee/react';

export const LangSelector: React.FC = () => {
const tolgee = useTolgee(['language']);
const locale = tolgee.getLanguage();
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();

function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
const nextLocale = event.target.value;
startTransition(() => {
router.replace(pathname, { locale: nextLocale });
});
}
return (
<select className="lang-selector" onChange={onSelectChange} value={locale}>
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
</select>
);
};

If you encounter any issues making it work, you can clone the example app and kickstart your journey from there.

Limitations of Server Components

Although in-context translation works with server components, there are some limitations compared to client components. Since the Tolgee cache on the server is separate, Tolgee can't automatically change the translation when creating a screenshot (unlike with client components, which swap the content if you've modified it in a dialog).

Furthermore, if you're using the Tolgee plugin, it won't affect the server's transition to dev mode. As a result, only the client switches, leaving server components non-editable in this mode.

Conclusion and future steps

Given that server components exist in a completely distinct environment, I might create a separate next.js package in the future.

Moreover, the standard usage of Server components is yet to be fully determined. It's possible they might only be utilized for data fetching, but the future holds the answer.

If you've got any suggestions or ideas for improvements, feel free to share them on our slack or open a Github issue. And if you're fond of Tolgee, don't forget to give us a Github star.

Next.js banner