Paragraphs
Astro

Feeds Astro's i18n. Doesn't replace it.

An integration that plugs into content collections. Translations live in the graph; Astro renders them at build time or on demand.

Install

One config block.

astro.config.ts ts
import { defineConfig } from "astro/config";
import paragraphs from "@paragraphs/astro";

export default defineConfig({
  integrations: [
    paragraphs({
      apiKey: process.env.PARAGRAPHS_API_KEY,
      projectId: process.env.PARAGRAPHS_PROJECT_ID,
      locales: ["en-GB", "es-ES", "fr-FR"],
      defaultLocale: "en-GB",
      // Sync mode: at-build or on-demand
      mode: "at-build",
    }),
  ],
  i18n: {
    defaultLocale: "en-GB",
    locales: ["en-GB", "es-ES", "fr-FR"],
    routing: { prefixDefaultLocale: false },
  },
});
Content collections

Locale-aware reads, no boilerplate.

src/pages/[locale]/blog/[slug].astro astro
---
import { getCollection } from "astro:content";
import { getEntry } from "@paragraphs/astro";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.flatMap((post) =>
    ["en-GB", "es-ES", "fr-FR"].map((locale) => ({
      params: { locale, slug: post.slug },
      props: { post },
    }))
  );
}

const { post } = Astro.props;
const { locale } = Astro.params;
const translated = await getEntry(post, locale);
---

<h1>{translated.data.title}</h1>
<article set:html={translated.body} />
Build performance

Incremental, fingerprint-aware.

The integration caches translations by fingerprint between builds. Edit a single blog post, only the affected fingerprints get re-fetched. A 500-post site re-translates incrementally — a build that took 4 minutes still takes 4 minutes after adding 8 locales.

Astro 5 starter on GitHub.

No credit card. 100,000 words on the free tier. Self-serve onboarding.