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.
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.