Skip to main content

Feature deep-dive · SaaSForge Agency

next-intl multilingual boilerplate (EN / FR / ES) for Next.js

next-intl multilingual boilerplate (EN / FR / ES) for Next.js

International marketing sites usually start as a single-language Next.js project and then absorb i18n as a retrofit. That retrofit is almost always painful because routing, metadata, and CMS calls all need to learn about locale at the same time. A boilerplate that ships multilingual from day one bakes the locale context into every layer up front, so adding a language is a content task, not an architecture task.

Three locales, locale-prefixed routing

SaaSForge Agency ships English, French, and Spanish at the URL level: `/en`, `/fr`, `/es`. All three are LTR, the template does not overclaim RTL support. The next-intl middleware handles locale detection, redirects, and the fallback when an unknown locale shows up in a URL.

Locale lives in the route segment, not in a cookie or a query string. That matters for SEO because Google indexes per-URL, not per-cookie. Each locale variant is a real, crawlable page with its own canonical and hreflang alternates.

Message catalogues for UI strings, CMS translations for content

UI strings (nav items, button labels, form errors) live in JSON message catalogues, one per locale, loaded server-side via next-intl. CMS content (services, case studies, blog posts) lives in Directus and uses Directus's native translations layer, so marketers edit French or Spanish copy in the same admin they use for English.

Splitting strings by source, UI in code, content in CMS, keeps the translation workflow sane. Developers do not edit copy in admin, marketers do not edit JSON in git, and both can ship without stepping on each other.

Reading a translated string in a Server Component
import { getTranslations } from "next-intl/server";

export default async function PricingHero() {
  const t = await getTranslations("pricing.hero");
  return (
    <section>
      <h1>{t("title")}</h1>
      <p>{t("description")}</p>
    </section>
  );
}

Per-locale SEO: metadata, sitemap, hreflang

Each locale has its own metadata: title, description, OG tags, canonical URL. The sitemap generator emits every locale variant of every page, and hreflang alternates point each variant at the others so Google understands the language relationships.

Structured data (Organization, FAQ, BreadcrumbList) is generated per-locale where the field values change. The point is that a French visitor on a French URL gets a fully French page including the JSON-LD, not an English page with a French nav.

Adding a fourth language

Adding a new locale is three steps: add the locale code to the next-intl config, create the message catalogue file, and add the language to Directus translations. Routing automatically expands to `/de` (or whatever code you added) on the next deploy.

The template does not ship RTL languages in the default locale set, but next-intl supports them. If you add Arabic or Hebrew you will also want to flip the document `dir` attribute in the locale layout, that is a one-line conditional, not a fork.

Frequently asked

Why next-intl instead of next-i18next?
next-intl was built around the App Router and Server Components from the start. It supports server-side translation loads, formatters for dates and numbers, and message namespacing without client-side hydration costs. next-i18next is mature but originated in the Pages Router era; for a 2026-vintage App Router project, next-intl is the lower-friction choice.
Can I run a non-multilingual site on this template?
Yes, set the locales list to a single locale in config and the locale prefix can be hidden. The CMS still works the same way; you just stop publishing translations.
How are translations kept in sync between code and CMS?
They are not auto-synced because they describe different things. UI strings (button labels) live in code because designers and developers iterate on them together. Content strings (case study body copy) live in Directus because marketers own them. The boundary is enforced by where the data is read from, not by a sync job.
Does the marketing template support locale-specific pricing?
The pricing block is CMS-driven, so per-locale pricing copy (currency labels, plan descriptions) is editable in Directus. The pricing logic itself (Stripe products, currency) is not part of SaaSForge Agency since it is a marketing-site template; pair it with SaaSForge Core or AI if you need a billing layer.
Ships in SaaSForge Agency

See SaaSForge Agency. Skip the deliberation.

Full source code. Lifetime updates. Polar Merchant-of-Record checkout. Private GitHub repo on purchase.