Next.js i18n with next-intl: How prompts.chat Handles Translation Management

The prompts.chat repository implements full-stack internationalization by combining Next.js App Router server components with the next-intl library, using getLocale() and getMessages() for server-side detection and NextIntlClientProvider with useTranslations() for client-side consumption.

The open-source prompts.chat project demonstrates a production-ready approach to Next.js i18n that leverages Next.js 12+ App Router capabilities alongside the next-intl library. This architecture delivers SEO-friendly, server-rendered translations while maintaining reactive client-side language switching capabilities. By examining the actual implementation in src/app/layout.tsx and supporting files, we can see how the application manages translation namespaces and handles hydration across the component tree.

Server-Side Locale Detection in the Root Layout

The internationalization flow begins in src/app/layout.tsx, where the application detects the user's locale and loads corresponding translation messages before rendering. The code imports getLocale and getMessages from next-intl/server to handle these operations asynchronously during the server render cycle.

According to the prompts.chat source code, the root layout calls await getLocale() to determine the current locale (e.g., "en", "es") and await getMessages() to load the compiled JSON translation file for that specific language. These values are then passed as props to the Providers component, ensuring the entire React tree has access to localization data from the initial HTML payload.

// src/app/layout.tsx
import { getMessages, getLocale } from "next-intl/server";
import { Providers } from "@/components/providers";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = await getLocale();          // → "en" | "es" | …
  const messages = await getMessages();     // loads messages/<locale>.json

  return (
    <html lang={locale}>
      <body>
        <Providers
          locale={locale}
          messages={messages}
          // ... other props
        >
          {children}
        </Providers>
      </body>
    </html>
  );
}

Source: layout.tsx#L147-L199

Wiring NextIntlClientProvider in the Provider Stack

Inside src/components/providers/index.tsx, the application wraps the children with NextIntlClientProvider from next-intl. This client-side provider receives the locale and messages props from the server, injecting them into the React context tree.

The NextIntlClientProvider component makes the translation data available to all descendant client components without prop-drilling. As implemented in prompts.chat, this provider sits alongside other context providers like SessionProvider and ThemeProvider, ensuring internationalization data persists across authentication and theme state changes.

// src/components/providers/index.tsx
"use client";

import { NextIntlClientProvider, AbstractIntlMessages } from "next-intl";

export function Providers({
  children,
  locale,
  messages,
  // ... other props
}: {
  children: React.ReactNode;
  locale: string;
  messages: AbstractIntlMessages;
}) {
  return (
    <SessionProvider>
      <NextIntlClientProvider locale={locale} messages={messages}>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </NextIntlClientProvider>
    </SessionProvider>
  );
}

Source: providers/index.tsx#L34-L38

Client-Side Translation Consumption with useTranslations

Once the provider is mounted, any client component can import the useTranslations hook from next-intl to access localized strings. The prompts.chat implementation in src/components/prompts/prompt-card.tsx demonstrates how to scope translations to specific namespaces.

The hook returns a function t(key) that looks up the appropriate string in the loaded messages object. By calling useTranslations("prompts"), components access only the "prompts" namespace, while useTranslations("common") provides shared UI strings like button labels and navigation elements.

// src/components/prompts/prompt-card.tsx
import { useTranslations } from "next-intl";

export function PromptCard(/* props */) {
  const t = useTranslations("prompts");       // namespace = "prompts"
  const tCommon = useTranslations("common"); // shared UI strings

  return (
    <div>
      <h2>{t("title")}</h2>
      <button>{tCommon("edit")}</button>
    </div>
  );
}

Source: prompt-card.tsx#L90-L91

Server Component Rendering with getTranslations

For server-only pages in the Next.js App Router, prompts.chat uses getTranslations from next-intl/server instead of the client hook. This function returns a translation function that can be awaited during the server render phase, ensuring the correct language is baked into the HTML before reaching the client.

In src/app/page.tsx, the homepage implementation calls await getTranslations("homepage") to acquire a translator function for the homepage namespace. This approach eliminates client-side hydration mismatches for static content while improving initial page load performance.

// src/app/page.tsx (homepage)
import { getTranslations } from "next-intl/server";

export default async function HomePage() {
  const tHomepage = await getTranslations("homepage");
  const tNav = await getTranslations("nav");

  return (
    <main>
      <h1>{tHomepage("welcome")}</h1>
      <nav>{tNav("explore")}</nav>
    </main>
  );
}

Source: app/page.tsx#L22-L23

Translation File Structure and Namespaces

All translation data in prompts.chat resides in the messages/ directory as JSON files named by locale (e.g., messages/en.json, messages/es.json). Each file follows a flat object structure organized by namespaces, where each namespace contains key-value pairs for specific UI sections.

The server loads the matching JSON file based on the detected locale and supplies it to NextIntlClientProvider. This creates a single source of truth for translations that both server components (via getTranslations) and client components (via useTranslations) access through their respective APIs.

End-to-End Request Flow

The complete Next.js i18n implementation in prompts.chat follows this lifecycle:

  1. An HTTP request arrives with a locale indicator (e.g., /es or header-based detection)
  2. getLocale() determines the active language in src/app/layout.tsx
  3. getMessages() reads the corresponding messages/<locale>.json file
  4. The Providers component wraps the application with NextIntlClientProvider, injecting locale data
  5. Server components use getTranslations() to render localized HTML immediately
  6. Client components hydrate and access the same translations via useTranslations() without additional network requests

Because the provider mounts once at the root level, every component—regardless of depth—shares the same locale context without prop-drilling or re-fetching translation files.

Summary

  • Server-side detection: getLocale() and getMessages() from next-intl/server run in src/app/layout.tsx to detect language and load JSON translations during the initial render
  • Provider architecture: NextIntlClientProvider in src/components/providers/index.tsx hydrates the React tree with locale data, enabling client-side access
  • Client hooks: useTranslations() allows client components like prompt-card.tsx to access namespaced strings with full type safety
  • Server rendering: getTranslations() provides asynchronous translation functions for server components, ensuring SEO-friendly markup
  • File organization: All translations live in messages/*.json files, organized by namespace for maintainable localization management

Frequently Asked Questions

What is the difference between getTranslations and useTranslations in next-intl?

getTranslations is an asynchronous server-side function imported from next-intl/server that returns a translation function during the React Server Component render phase. It ensures translations are embedded in the initial HTML. useTranslations is a client-side React hook that accesses the context provided by NextIntlClientProvider and is used exclusively in client components marked with the "use client" directive.

How does prompts.chat detect the user's locale without middleware?

The repository leverages Next.js 12+ App Router's built-in internationalization routing capabilities combined with next-intl's getLocale() function. In src/app/layout.tsx, the locale is determined automatically based on the URL path or domain configuration, then passed to the provider stack without requiring custom middleware for basic locale detection.

Where are translation files stored in the prompts.chat repository?

All translation files are stored in the messages/ directory at the project root. Each supported language has its own JSON file (e.g., messages/en.json, messages/es.json) containing flat objects organized by namespaces like "prompts", "common", "homepage", and "nav".

Can I use next-intl with the Next.js Pages Router, or is it App Router only?

While the prompts.chat implementation uses the App Router, next-intl supports both App Router and Pages Router architectures. In the Pages Router, you would typically use getStaticProps or getServerSideProps to load messages and pass them to NextIntlClientProvider in your custom _app.tsx file, rather than using the async getTranslations function available only in Server Components.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →