Paragraphs
vs next-intl

next-intl is a runtime. Paragraphs is the backend.

They compose. They don't compete. Keep your next-intl config exactly as it is — and add a translation source.

What next-intl is good at

Where it earns its place.

  • Locale routing and middleware
  • Server-side rendering per locale
  • ICU MessageFormat at runtime
  • Type-safe message keys
  • Date / number / currency formatters
  • next-intl/navigation locale-aware Link

It's a great library. We use it on this very site. We're not trying to replace it.

What it doesn't answer

The questions that pile up.

  • Where do the messages come from? Hand-translated? Machine-translated? Spreadsheet?
  • Who reviews translations before they go live?
  • How do CMS-sourced strings (Sanity, headless WP, Payload) get into messages/?
  • How do you keep brand glossary enforced across 12 locales?
  • How do you stage and merge a campaign's translations atomically?
  • What about translated JSON-LD, sitemaps, llms.txt?
How they compose

One postbuild script.

Paragraphs syncs translations from the graph into ./messages/{locale}.json. next-intl reads them at runtime, exactly as before.

scripts/sync-translations.ts ts
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"],
});

// Run as a postbuild step:
//   "build": "next build && tsx scripts/sync-translations.ts"
// Or on a Paragraphs webhook trigger for instant updates.
Decision matrix

When to add Paragraphs to next-intl.

Your situation next-intl alone + Paragraphs
Tiny site, hand-translated UI strings only Yes — perfect fit Overkill
UI strings + a few static marketing pages Yes, if you have a translator Adds review workflow + memory
UI strings + CMS-sourced content No — messages/ won't cover it Yes — sync CMS units into the graph
10+ locales with frequent updates Slow to maintain Yes — fingerprinted updates
You need AISEO / translated JSON-LD No surface for it Yes — first-class
E-commerce with hundreds of products Out of scope Yes — Translation Memory shines
CMS-sourced content

Where next-intl on its own breaks down.

If your content lives in Sanity, headless WordPress, Payload, or a custom CMS, you can't just stuff it into messages/. Paragraphs' adapters give you locale-aware server components that handle the fetching and substitution for you.

app/[locale]/products/[slug]/page.tsx tsx
import { fetchProduct } from "@paragraphs/next";
import { getTranslations } from "next-intl/server";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>;
}) {
  const { locale, slug } = await params;
  const t = await getTranslations({ locale, namespace: "Product" });

  // Paragraphs fetches the product from your CMS, then
  // substitutes any translatable units for the requested locale.
  const product = await fetchProduct({ slug, locale });

  return (
    <article>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <button>{t("addToCart")}</button>
    </article>
  );
}

Already on next-intl?

Drop in @paragraphs/next as a sync source. Your runtime stays untouched.