Jan-11, 2025 ยท 10min
change languageComplete guide: Integrate lingui localization solution in Next.js project, including SWC plugin configuration, PO file management, multi-language routing, and translation workflow.
Related articles
Implementation

pnpm add @lingui/core @lingui/react
pnpm add @lingui/cli @lingui/loader @lingui/swc-plugin -D.po fileconst nextConfig: NextConfig = {
output: "export",
+ webpack: (config) => {
+ config.module.rules.push({
+ test: /\.po$/,
+ use: {
+ loader: "@lingui/loader",
+ },
+ });
+ return config;
+ },
+ experimental: {
+ swcPlugins: [["@lingui/swc-plugin", {}]],
+ },
transpilePackages: ["three"],
};import { defineConfig } from '@lingui/cli'
export default defineConfig({
sourceLocale: 'en',
locales: ['zh', 'en'],
catalogs: [
{
path: '<rootDir>/src/locales/{locale}/messages',
include: ['src'],
},
],
})Parse the message.po file, provide a global context
import { setupI18n } from '@lingui/core'
import linguiConfig from '../lingui.config'
import 'server-only'
import type { I18n, Messages } from '@lingui/core'
export const { locales, sourceLocale } = linguiConfig
type SupportedLocales = string
async function loadCatalog(locale: SupportedLocales): Promise<{
[k: string]: Messages
}> {
const { messages } = await import(`./locales/${locale}/messages.po`)
return {
[locale]: messages,
}
}
const catalogs = await Promise.all(locales.map(loadCatalog))
export const allMessages = catalogs.reduce((acc, oneCatalog) => {
return { ...acc, ...oneCatalog }
}, {})
type AllI18nInstances = { [K in SupportedLocales]: I18n }
export const allI18nInstances: AllI18nInstances = locales.reduce(
(acc, locale) => {
const messages = allMessages[locale] ?? {}
const i18n = setupI18n({
locale,
messages: { [locale]: messages },
})
return { ...acc, [locale]: i18n }
},
{},
)
export const getI18nInstance = (locale: SupportedLocales): I18n => {
if (!allI18nInstances[locale]) {
console.warn(`No i18n instance found for locale "${locale}"`)
}
return allI18nInstances[locale]! || allI18nInstances.en!
}provider
"use client";
import { setupI18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { useState } from "react";
import type { Messages } from "@lingui/core";
export function LinguiClientProvider({
children,
initialLocale,
initialMessages,
}: {
children: React.ReactNode;
initialLocale: string;
initialMessages: Messages;
}) {
const [i18n] = useState(() => {
return setupI18n({
locale: initialLocale,
messages: { [initialLocale]: initialMessages },
});
});
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}Global configuration
import { setI18n } from "@lingui/react/server";
import { Inter } from "next/font/google";
import { Toaster } from "react-hot-toast";
+import { getI18nInstance } from "~/i18n";
+import { LinguiClientProvider } from "~/providers/LinguiClientProvider";
import { Providers } from "../providers";
import "./globals.css";
import type { Metadata } from "next";
import type { ReactNode } from "react";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Magic tools",
description: "a magic tools for developers",
icons: ["/favicon.ico"],
};
export function getRootLayout(lang: string) {
function RootLayout({ children }: { children: ReactNode }) {
+ const i18n = getI18nInstance(lang);
+ setI18n(i18n);
return (
<html lang={lang}>
<body className={inter.className}>
<Toaster />
+ <LinguiClientProvider
+ initialLocale={lang}
+ initialMessages={i18n.messages}
+ >
<Providers>{children}</Providers>
+ </LinguiClientProvider>
</body>
</html>
);
}
return RootLayout;
}The effect to be achieved is that there are two en and zh, and the en path can be hidden, the specific idea is to implement two RootLayout

Then in the lang path, import the corresponding component
export { default, metadata } from '../../../(main)/base64/layout'export { default } from '../../../(main)/base64/page'The default setting of the main route is en, the lang route obtains the corresponding lang according to generateStaticParams, and here a getRootLayout function is extracted to obtain the specific RootLayout
export function getRootLayout(lang: string) {
function RootLayout({ children }: { children: ReactNode }) {
const i18n = getI18nInstance(lang)
setI18n(i18n)
return (
<html lang={lang}>
<body className={inter.className}>
<Toaster />
<LinguiClientProvider
initialLocale={lang}
initialMessages={i18n.messages}
>
<Providers>{children}</Providers>
</LinguiClientProvider>
</body>
</html>
)
}
return RootLayout
}import { getRootLayout } from '../_layout'
export { metadata } from '../_layout'
const RootLayout = getRootLayout('en')
export default RootLayoutimport { locales } from '~/i18n'
import { getRootLayout } from '../../_layout'
export { metadata } from '../../_layout'
interface Props {
params: Promise<{
lang: string
}>
children: React.ReactNode
}
export async function generateStaticParams() {
return locales.map((locale) => ({ lang: locale }))
}
export default async function Layout({ params, children }: Props) {
const { lang } = await params
const RootLayout = getRootLayout(lang)
return <RootLayout>{children}</RootLayout>
}After adding en zh, some route links may not jump to the current language, useI18nHelper is implemented as a helper
import { useLingui } from '@lingui/react/macro'
import { usePathname, useRouter } from 'next/navigation'
export function useI18nHelper() {
const router = useRouter()
const pathname = usePathname()
const { i18n } = useLingui()
const sourceLocale = 'en'
const locales = ['en', 'zh']
const switchLocale = () => {
const newLocale =
locales.find((locale) => locale !== i18n.locale) || sourceLocale
const realPathname = pathname.split('/').filter((i, index) => {
if (index === 1 && i === i18n.locale) return false
return Boolean(i)
})
const newPathname =
newLocale === sourceLocale
? `/${realPathname.join('/')}`
: `/${newLocale}/${realPathname.join('/')}`
router.push(newPathname)
}
const getRealPathname = (path: string) => {
const isLocalePath = locales.includes(pathname.split('/')[1])
return isLocalePath
? [i18n.locale, ...path.split('/').filter(Boolean)].join('/')
: path
}
return {
// FIXME import from config file
sourceLocale,
locales,
switchLocale,
getRealPathname,
}
}Add extraction command
{
"scripts": {
"extract": "lingui extract",
"compile": "lingui compile"
}
}Manual translation or translation with tools
