First Working Translation with Next.js
This guide takes an existing TypeScript Next.js 16 App Router application from one message to one rendered translation. It uses Server Components on the Node runtime. The Next.js package README and API reference cover Client Components, server actions, and production options after this first path works.
Page outline
Before installing the native CLI, check Platform support.
Use Node.js >=22.22 and an application on one of the published targets.
1. Install the packages
pnpm add @palamedes/core @palamedes/react @palamedes/runtime @palamedes/next-plugin
pnpm add -D @palamedes/cli @palamedes/configThe Next integration requires Next.js 16. Keep next, react, and
react-dom from the App Router application itself.
2. Describe the catalogs
# palamedes.yaml
locales: [en, de]
source-locale: en
catalogs:
- path: src/locales/{locale}
include: [app, src]3. Wire the Next plugin
// next.config.mjs
import { withPalamedes } from "@palamedes/next-plugin"
export default withPalamedes({})withPalamedes() transforms macros and loads imported .po catalogs in the
Next build. Keep direct application catalog imports on .po; FCL is supported
as catalog storage but is not an import-loader format.
4. Create a request-local server runtime
Add the .po declaration once:
// src/po.d.ts
declare module "*.po" {
import type { CompiledCatalogMessages } from "@palamedes/core/compiled"
export const messages: CompiledCatalogMessages
}Then add one server-only module. createNextServerI18nScope() follows the App
Router render lifetime, while runWithServerI18n() makes the translated UI
explicitly request-local.
// src/lib/i18n.server.ts
import "server-only"
import { cache } from "react"
import { createI18n } from "@palamedes/core/compiled"
import type { PalamedesI18n } from "@palamedes/core"
import { createNextServerI18nScope } from "@palamedes/next-plugin/server"
import { messages as deMessages } from "../locales/de.po"
import { messages as enMessages } from "../locales/en.po"
export const serverI18nScope = createNextServerI18nScope<PalamedesI18n>()
const loadActiveServerI18n = cache(async () => {
const i18n = createI18n()
i18n.load("en", enMessages)
i18n.load("de", deMessages)
i18n.activate("de")
return i18n
})
export async function createActiveServerI18n() {
const i18n = await loadActiveServerI18n()
serverI18nScope.activate(i18n)
return i18n
}
export function runWithServerI18n<Result>(i18n: PalamedesI18n, callback: () => Result): Result {
return serverI18nScope.run(i18n, callback)
}This smallest path deliberately fixes the locale to de. Replace that choice
with your cookie, route, subdomain, or account policy before adding a locale
switcher; Locale strategies shows the trade-offs.
5. Write and extract one Server Component message
// src/app/page.tsx
import { t } from "@palamedes/core/macro"
import { createActiveServerI18n, runWithServerI18n } from "../lib/i18n.server"
function translateWelcome() {
return t`Welcome to Palamedes`
}
export default async function Page() {
const i18n = await createActiveServerI18n()
return runWithServerI18n(i18n, () => <h1>{translateWelcome()}</h1>)
}Run extraction:
pnpm exec pmds extractIt creates src/locales/en.po and src/locales/de.po.
6. Translate and run
In src/locales/de.po, set the extracted message's translation:
msgid "Welcome to Palamedes"
msgstr "Willkommen bei Palamedes"Then start the application:
pnpm devThe / route renders Willkommen bei Palamedes. That proves macro
transformation, catalog extraction, .po loading, and request-local App Router
rendering work together.
Add client code or server actions next
This first run keeps translated code in Server Components. For translated Client
Components using PO catalogs, enable messageSplitting: true in the second
withPalamedes() options argument. Palamedes then loads only the document
locale's fragments for evaluated client modules; a locale change requires a
document navigation.
Server Functions and Actions run in separate requests. When adding them, expose
initializeServerFunctionI18n() from src/palamedes.server.ts and enable the
plugin's serverFunctions: true option. Follow the complete examples in the
Next.js package README
instead of sharing page-render state with an action.