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.

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

Install and Configure the SDK

Install @localess/react:

npm install @localess/react

Choosing the right export

The package ships three exports. Pick the one that matches your rendering strategy:

ExportUse caseLive editing
@localess/reactClient-side SPAsYes
@localess/react/ssrNext.js static export (output: 'export')No
@localess/react/rscNext.js App Router with React Server ComponentsYes

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

Authenticate and generate:

npx localess login
npx localess types generate --path types/localess.ts

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

Fetching 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 i18next

Create 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:

  1. The new version is available from the Localess API within seconds.
  2. On the next cache expiry, Next.js fetches the updated content automatically.
  3. 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/rsc in Next.js App Router; @localess/react is for SPAs only
  • SDK init: localessInit in app/layout.tsx with 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().t in 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.