
Internationalization has become a baseline requirement in our projects. Within our applications, we use next-intl to orchestrate language handling, message loading, and user preferences. Below is an overview of the full architecture—from bootstrapping to the translation of reusable components.
1// apps/app/i18n.ts2import { getRequestConfig } from "next-intl/server";34export const locales = ["en"] as const;5export const defaultLocale = "en";6export const localePrefix = "never";78export default getRequestConfig(async ({ requestLocale }) => {9 let locale = await requestLocale;1011 if (!locales.includes(locale)) {12 locale = defaultLocale;13 }1415 return {16 locale,17 messages: (await import(`./messages/${locale}.json`)).default,18 };19});
Key points:
1// apps/app/next.config.js2import createIntlPlugin from "next-intl/plugin";34const withNextIntl = createIntlPlugin("./i18n.ts");56export default withNextIntl({7 output: "standalone",8});
The plugin registers next-intl parameters at build time, enables message extraction, and makes the use of getLocale / getMessages in server components completely transparent.
1// apps/app/middleware.ts2import { type NextRequest } from "next/server";3import createMiddleware from "next-intl/middleware";4import { locales, defaultLocale, localePrefix } from "./i18n";56const intlMiddleware = createMiddleware({7 locales,8 defaultLocale,9 localePrefix,10});1112export default function middleware(req: NextRequest) {13 return intlMiddleware(req);14}
The middleware:
1// apps/app/app/layout.tsx2import { NextIntlClientProvider } from "next-intl";3import { getLocale, getMessages } from "next-intl/server";45export default async function RootLayout({ children }: { children: React.ReactNode }) {6 const locale = await getLocale();7 const messages = await getMessages();89 return (10 <html lang={locale} suppressHydrationWarning>11 <ThemeLayout isMobile={await isMobileDevice()}>12 <body>13 <NextIntlClientProvider14 locale={locale}15 messages={messages}16 >17 <RootLayoutClient>18 {children}19 </RootLayoutClient>20 </NextIntlClientProvider>21 </body>22 </ThemeLayout>23 </html>24 );25}
I retrieve language information directly on the server side and inject it into the provider.
This way, any client component (even those in reusable packages) can call useTranslations without any additional boilerplate.
1// apps/app/messages/en.json2{3 "dashboard": {4 "machinesCount": "{count} machines",5 "dateRange": {6 "last-hour": "Last hour",7 "week-to-date": "Week to date"8 }9 },10 "agentCard": {11 "selectedVariable_one": "1 selected variable",12 "selectedVariable_other": "{count} selected variables"13 }14}
Adopted strategies:
1// apps/app/app/[locale]/(auth)/login/page.tsx2"use client";3import { useTranslations } from "next-intl";45export default function LoginPage() {6 const t = useTranslations("login");78 return (9 <form>10 <label>{t("emailLabel")}</label>11 <input placeholder="name@example.com" />12 <button type="submit">{t("loginButton")}</button>13 </form>14 );15}
useTranslations accepts a namespace (here, "login") and returns a typed resolver (thanks to the literal type of the JSON object). For parameterized messages, you just need to pass an object, e.g. t("machinesCount", { count }).
1// apps/app/app/not-found.tsx2import { getTranslations } from "next-intl/server";34export default async function NotFound() {5 const t = await getTranslations("notFound");6 return (7 <section>8 <h2>{t("title")}</h2>9 <p>{t("description")}</p>10 </section>11 );12}
getTranslations works in any server component or action, allowing me to localize error pages, metadata, or API responses without relying on client-side hooks.
Even a shared UI package (packages/ui/src/user/Component.tsx) can use useTranslations("user"). The provider in the main layout ensures that text resources are available regardless of where the component is mounted—essential in a monorepo with a reusable design system.
The combination of Next.js + next-intl allows centralized locale control, dynamic distribution of translations, and reuse across the entire monorepo. The JSON catalog structure and global provider make it easy to add new languages in the future.