Technical tutorial
How to Build a Multilingual Next.js App with Localess
Step-by-step tutorial, add i18n to a Next.js App Router project using @localess/react. Fetch translations and structured content without redeploying.

Most i18n setups for Next.js share the same frustration: changing a button label or fixing a typo in a translation requires a full rebuild and redeployment. For marketing copy or editorial content, that friction adds up fast.
Localess is a Firebase-backed, open-source headless CMS with a built-in Visual Editor. It gives editors a live preview interface to manage translations and structured content while developers query a clean JSON API. You can explore the project at localess.org or on GitHub.
Localess solves this differently. Translations and structured content live in a Firebase-backed CMS with a JSON API. Your Next.js app fetches them at request time. When an editor updates a key in the Localess UI, it reflects immediately — no code change, no build, no deploy. That makes Localess a strong fit for any nextjs multilingual cms setup where editors need to ship copy changes independently from the dev team.
This localess react tutorial walks through a complete i18n Next.js headless CMS integration: installing the SDK, generating TypeScript types, wiring up locale routing, fetching translation strings and structured content, and understanding how live updates work.
Prerequisites
Before you start, you need:
- A running Localess instance. Deploy one to Firebase following the Localess setup guide, or run it locally for exploration. Note your instance URL (e.g.
https://my-localess.web.app). - A Space, Space ID, and API token. Create a Space in the Localess admin, then generate an API token under Settings → Access Tokens.
- A Next.js 14+ project using the App Router.
- Node.js 20+.
Store your credentials as environment variables — never hard-code them:
# .env.local
LOCALESS_ORIGIN=https://my-localess.web.app
LOCALESS_SPACE_ID=your-space-id
LOCALESS_TOKEN=your-api-tokenInstall and Configure the SDK
Install @localess/react:
npm install @localess/reactChoosing the right export
The package ships three exports. Pick the one that matches your rendering strategy:
| Export | Use case | Live editing |
|---|---|---|
@localess/react | Client-side SPAs | Yes |
@localess/react/ssr | Next.js static export (output: 'export') | No |
@localess/react/rsc | Next.js App Router with React Server Components | Yes |
For a modern Next.js App Router project, use @localess/react/rsc. It supports React Server Components and the Visual Editor's live sync in the same project, with a clean server/client component split.
The @localess/react (default) export is for pure client-side SPAs — importing it in a Server Component will cause a build error because it includes browser-only code. Always use /rsc in App Router projects.
Initialise the SDK in your root layout
Call localessInit once at app startup. The root layout is the right place — it runs server-side, so your token never reaches the browser.
// app/layout.tsx
import { localessInit } from "@localess/react/rsc";
import { HeroSection, NavMenu, Footer } from "@/components";
localessInit({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
enableSync: process.env.NODE_ENV !== "production",
components: {
"hero-section": HeroSection,
"nav-menu": NavMenu,
"footer": Footer,
},
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>{children}</body>
</html>
);
}enableSync: true loads the Localess Visual Editor sync script so editors see live changes in preview mode. Keep it off in production.
Generate TypeScript Types
Localess schemas define the shape of your content. The CLI reads those schemas and generates TypeScript types so your fetches are fully typed.
Install the CLI as a dev dependency:
npm install @localess/cli -DAuthenticate and generate:
npx localess login
npx localess types generate --path types/localess.tsThis creates types/localess.ts in your project root with types for every schema you've defined — Page, HeroSection, NavMenu, and so on.
Run this command any time schemas change in the CMS to keep types in sync.
Setting Up Locale Routing
Before fetching any content, wire up locale routing so the locale parameter flows through every request. Next.js App Router uses a [locale] dynamic segment, paired with middleware that detects the visitor's preferred language.
app/
[locale]/
layout.tsx
page.tsxFetching Translations
Localess stores UI copy — button labels, navigation text, form placeholders — as translation strings: flat key-value pairs per locale. The SDK fetches them via getTranslations, which returns a Record<string, string> map like { "nav.home": "Home", "common.submit": "Submit" }.
This section wires those messages into react-i18next so client components get the standard useTranslation hook.
Install react-i18next
npm install react-i18next i18nextCreate a client-side provider
I18nextProvider is a client component, so create a thin wrapper that accepts the serialisable messages from the server and initialises an i18next instance on the client:
// components/TranslationsProvider.tsx
"use client";
import { I18nextProvider } from "react-i18next";
import { createInstance } from "i18next";
import { initReactI18next } from "react-i18next";
function createI18n(locale: string, messages: Record<string, string>) {
const instance = createInstance();
instance.use(initReactI18next).init({
lng: locale,
resources: { [locale]: { translation: messages } },
interpolation: { escapeValue: false },
});
return instance;
}
export function TranslationsProvider({
locale,
messages,
children,
}: {
locale: string;
messages: Record<string, string>;
children: React.ReactNode;
}) {
return (
<I18nextProvider i18n={createI18n(locale, messages)}>
{children}
</I18nextProvider>
);
}Fetch and seed in the locale layout
Fetch translations from Localess on the server, then pass the plain messages object to the provider. The client never calls Localess directly and your API token stays hidden:
// app/[locale]/layout.tsx
import { getLocalessClient } from "@localess/react/rsc";
import { TranslationsProvider } from "@/components/TranslationsProvider";
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const messages = await getLocalessClient().getTranslations(locale);
return (
<TranslationsProvider locale={locale} messages={messages}>
{children}
</TranslationsProvider>
);
}useTranslation in client components
Any client component in the tree can now use the standard react-i18next hook:
// components/NavBar.tsx
"use client";
import { useTranslation } from "react-i18next";
export function NavBar() {
const { t } = useTranslation();
return (
<nav>
<a href="/">{t("nav.home")}</a>
<a href="/blog">{t("nav.blog")}</a>
<button>{t("common.sign_in")}</button>
</nav>
);
}Server Components
For Server Components that also need translations, use i18next directly — no provider needed:
// app/[locale]/page.tsx
import { getLocalessClient } from "@localess/react/rsc";
import { createInstance } from "i18next";
async function getT(locale: string) {
const messages = await getLocalessClient().getTranslations(locale);
const instance = createInstance();
await instance.init({
lng: locale,
resources: { [locale]: { translation: messages } },
interpolation: { escapeValue: false },
});
return instance.t.bind(instance);
}
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = await getT(locale);
return <h1>{t("home.headline")}</h1>;
}The messages are fetched once per request on the server. The TranslationsProvider reuses the same messages that were already fetched in the layout — no duplicate network calls.
Fetching Structured Content
Beyond flat translation strings, Localess manages schema-driven content: blog posts, landing pages, product listings. Fetch these by slug using the typed API.
// app/[locale]/page.tsx
import { getLocalessClient, LocalessDocument } from "@localess/react/rsc";
import type { Page } from "/types/localess";
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const client = getLocalessClient();
const content = await client.getContentBySlug<Page>("home", { locale });
return (
<LocalessDocument
data={content.data}
links={content.links}
references={content.references}
/>
);
}LocalessDocument maps each content block to the React component you registered in localessInit.
If your home document has a hero-section block and a nav-menu block, those render as your HeroSection and NavMenu components automatically.
Each registered component receives the block data, resolved links, and references:
// components/HeroSection.tsx
import { localessEditable, localessEditableField } from "@localess/react/rsc";
import type { HeroSection } from "@localess/localess";
type Props = { data: HeroSection };
export function HeroSection({ data }: Props) {
return (
<section {...localessEditable(data)}>
<h1 {...localessEditableField<HeroSection>("title")}>{data.title}</h1>
<p {...localessEditableField<HeroSection>("subtitle")}>{data.subtitle}</p>
</section>
);
}localessEditable and localessEditableField add data attributes the Visual Editor uses for inline selection and highlighting.
They are no-ops when enableSync is false, so they have no effect in production.
Fetching by ID or with draft preview
// Fetch a document by its Localess ID, in draft mode for content preview
const content = await client.getContentById<Page>("FRnIT7CUABoRCdSVVGGs", {
locale: "de",
version: "draft", // use "draft" to preview unpublished changes in staging; omit for published content in production
resolveReference: true,
});Publishing Updates Without a Build
This is the core Localess proposition: editors update translations or content in the Localess admin and the change is live immediately — no code change, no build, no deploy.
Here is why it works: the SDK fetches content at request time from Localess's CDN-backed API, with a five-minute in-memory cache by default. When an editor publishes a change:
- The new version is available from the Localess API within seconds.
- On the next cache expiry, Next.js fetches the updated content automatically.
- Done.
For editors working inside the Localess Visual Editor, enableSync: true wires up a sync script that pushes input and change events to the page — changes appear live in the preview iframe without saving.
To tune the cache window:
localessInit({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
cacheTTL: 60_000, // 1-minute cache for near-real-time updates
});For on-demand revalidation when Localess publishes, use revalidatePath or revalidateTag in a Next.js route handler triggered by a Localess webhook.
Summary
Here is what you now have:
- Install:
npm install @localess/react - Export: use
@localess/react/rscin Next.js App Router;@localess/reactis for SPAs only - SDK init:
localessInitinapp/layout.tsxwith server-safe credentials - TypeScript types:
npx localess types generate+ tsconfig alias"@localess/*": [".localess/*"] - Locale routing:
[locale]segment + middleware for language detection - Translations:
client.getTranslations(locale)→TranslationsProvider(i18next) +useTranslation()hook in client components;createInstance().tin server components - Structured content:
client.getContentBySlug<Page>(slug, { locale })+<LocalessDocument> - Zero-build updates: editors publish in Localess; changes go live on next cache refresh
The full SDK source and issue tracker are on GitHub. API and integration docs live at localess.org/docs.
If you are adding Localess to an existing project with SSR already in place, start with translations — it is a single fetch call and gives you immediate value before touching content schemas.