Localisation
Supacharger uses next-intl with the Next.js App Router and without locale-based routing. Language is a user preference inside the SaaS application, so routes have one stable shape in every language:
/account English, French, or another supported language
/en/account Not used
/fr/account Not used
Do not add a top-level [locale] route segment or next-intl routing middleware for the standard Supacharger setup.
Marketing-site language handoff
A separate marketing application can pass its selected language into the authenticated SaaS by adding lang to the destination URL:
https://app.example.com/login?lang=fr
https://app.example.com/login?plan=pro&lang=en
The standard query syntax is ?lang=fr, not lang?=fr. Supacharger validates the value against SUPPORTED_LOCALES, writes an accepted value to the HTTP-only locale cookie, and redirects to the same pathname with only lang removed. Other query parameters, such as plan=pro, remain intact. The query parameter is therefore a one-time handoff and never becomes part of the application's route structure.
An unsupported, malformed, or uppercase value is ignored and removed. Add a language to SUPPORTED_LOCALES before linking to it from the marketing site.
Application-owned files
The starter installs these localisation files:
messages/
en.json
fr.json
src/i18n/
config.ts
request.ts
These files are developer-owned. supacharger coreupdate does not treat changes to them as core conflicts and does not add, overwrite, or delete catalogues when messages/ already exists.
SnapScreen and Wakekeeper use these src/ paths directly. Specdrive follows the same route and preference contract but uses repository-root i18n/config.ts, i18n/request.ts, and messages/. Every consumer supports the transient ?lang=<code> handoff and profile restoration; translation coverage remains application-owned and does not imply that all existing UI copy is translated.
src/i18n/config.ts defines the supported locales, fallback locale, and cookie name:
export const SUPPORTED_LOCALES = ['en', 'fr'] as const;
export const DEFAULT_LOCALE = 'en';
export const LOCALE_COOKIE_NAME = 'supacharger_locale';
Keep a matching messages/<locale>.json file for every entry in SUPPORTED_LOCALES. The request configuration validates the cookie before loading a catalogue, so an unknown or malformed locale falls back to DEFAULT_LOCALE instead of attempting an arbitrary dynamic import.
Request and client configuration
src/i18n/request.ts reads the locale preference and returns the locale and messages to next-intl. The root layout uses getLocale() and getMessages() and passes both to NextIntlClientProvider, which makes translations available to Server and Client Components.
Supacharger enables Next.js Cache Components in every application's merge-managed Next configuration. Since the validated locale cookie determines both <html lang> and the messages provider before the document can render, the developer-owned root layout exports instant = false. Next validates each route segment independently, so pages and nested layouts that directly perform request-time authentication also need an explicit boundary: instant = false for deliberately blocking identity/access checks, or local Suspense for streamable work. Cache Components makes request-time data dynamic by default; do not restore dynamic = 'force-dynamic' or cache user-specific authentication reads with use cache. The shared test:auth-instant contract checks these route segments.
The /api/intl endpoint changes the preference. Send a supported locale as JSON:
await fetch('/api/intl', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({locale: 'fr'})
});
The endpoint validates the locale and stores it in an HTTP-only, same-site cookie. For an authenticated user, it also stores the code in the profile. Reload or refresh the current route after a successful change. The pathname does not change.
Profile persistence and precedence
The canonical profile column is app.profiles.language_code. It accepts lowercase two-letter codes (^[a-z]{2}$). The database constraint is deliberately language-agnostic; SUPPORTED_LOCALES is the application-level allowlist that controls which catalogues can actually be selected and loaded.
Supacharger resolves preferences in this order:
- A valid
?lang=<code>marketing handoff. - A valid
supacharger_localecookie created by an explicit selection. - The signed-in user's saved
language_codewhen the browser has no valid locale cookie. DEFAULT_LOCALE.
When a signed-out visitor arrives with a valid handoff, Supacharger stores the cookie and marks it for profile synchronisation. After the visitor signs in, the preference is written to the profile. On another browser or device with no locale cookie, the supported saved profile value is restored into the cookie.
Supacharger, SnapScreen, Wakekeeper, and Specdrive all persist this contract in app.profiles.language_code. Specdrive also exposes the language choice in Account Settings.
Using translations
Use a namespace from the active application's catalogue:
import {useTranslations} from 'next-intl';
export function SaveNotice() {
const t = useTranslations('GlobalUI');
return <p>{t('buttonSaved')}</p>;
}
Use getTranslations from next-intl/server in async Server Components, Server Actions, metadata, and route handlers.
Authoring and translating copy
Always add or update the English source value in messages/en.json when a localised key or its wording changes. English is the authoring language and must be complete for the affected surface.
Before introducing or changing useTranslations or getTranslations, verify that its namespace and every statically referenced key exist with non-empty values in messages/en.json. Commit the consuming code and English catalogue together; otherwise next-intl raises MISSING_MESSAGE at runtime even when a secondary catalogue contains the namespace.
Do not invent, machine-translate, copy the English wording into, or otherwise fill French or another secondary-language value unless translation into that language is explicitly requested. If catalogue structure or a validation check requires the new key in a secondary catalogue, add the key with an empty string; otherwise leave that catalogue untouched. Empty strings are untranslated placeholders, not completed translations, and should be reported as pending translation.
Where a protected component supports older or incomplete developer-owned catalogues, keep an intentional English fallback so an empty or unavailable value does not expose a raw key or MISSING_MESSAGE error. Do not describe a locale or surface as translation-complete while placeholders remain.
Adding a language
- Add the locale to
SUPPORTED_LOCALES. - Add
messages/<locale>.jsonwith every required namespace and key. - Add the locale to the application's language selector.
- Test signed-out and signed-in screens, number/date formatting, and fallback behaviour.
Because catalogues are not CLI-managed, a core upgrade cannot insert newly required strings. Review the release notes, add the required English values, and add empty secondary placeholders only where catalogue structure requires them. Fill those placeholders only as part of explicitly requested translation work.