Feeds next-intl. Doesn't replace it.
Paragraphs is the backend. next-intl stays your runtime. Install in two minutes; works with App Router and server components.
Two minutes.
pnpm add @paragraphs/next next-intl
# or: npm install / yarn add
# Get your API key from the dashboard
echo "PARAGRAPHS_API_KEY=pg_live_..." >> .env.local
echo "PARAGRAPHS_PROJECT_ID=prj_..." >> .env.local Pull translations into messages/.
import { syncToNextIntl } from "@paragraphs/next";
await syncToNextIntl({
apiKey: process.env.PARAGRAPHS_API_KEY!,
projectId: process.env.PARAGRAPHS_PROJECT_ID!,
outputDir: "./messages",
locales: ["en-GB", "es-ES", "fr-FR", "de-DE"],
// Optional: only export approved translations
status: "approved",
}); Wire it into package.json:
{
"scripts": {
"build": "next build && tsx scripts/sync-translations.ts",
"translations:sync": "tsx scripts/sync-translations.ts"
}
} For dynamic content, use the server fetcher.
If your content lives in Sanity, headless WP, or Payload, `messages/` won't cover it. The Paragraphs server fetcher handles locale-aware reads and substitutes translatable fields inline.
import { fetchPost } from "@paragraphs/next/sanity";
import { getTranslations } from "next-intl/server";
export default async function Post({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
const t = await getTranslations({ locale, namespace: "Post" });
const post = await fetchPost({ slug, locale });
return (
<article>
<h1>{post.title}</h1>
<PortableText value={post.body} />
<p>{t("publishedAt", { date: post.publishedAt })}</p>
</article>
);
} Real-time without a rebuild.
Paragraphs can POST to your app whenever translations are approved. Pair with Next.js on-demand revalidation to keep static pages fresh without a full rebuild.
import { verifyWebhook } from "@paragraphs/next";
import { revalidateTag } from "next/cache";
export async function POST(req: Request) {
const event = await verifyWebhook(req, {
secret: process.env.PARAGRAPHS_WEBHOOK_SECRET!,
});
if (event.type === "translation.approved") {
revalidateTag(`post:${event.data.unitId}`);
}
return Response.json({ ok: true });
} No build step, no postbuild.
If you prefer to skip the sync step entirely, the @paragraphs/next middleware can fetch the latest approved translations at request time from Cloudflare KV. Great for content that changes often.
import createMiddleware from "next-intl/middleware";
import { paragraphsEdge } from "@paragraphs/next/edge";
export default paragraphsEdge(
createMiddleware({
locales: ["en-GB", "es-ES", "fr-FR"],
defaultLocale: "en-GB",
})
);
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)"],
}; Want a working example?
The docs include a complete Next.js + next-intl starter on GitHub.