Internationalization (i18n)
Leave Localhost ships with full internationalization support powered by next-international. The product app includes three locales out of the box — English, French, and Spanish — and adding more is straightforward.
Leave Localhost ships with full internationalization support powered by next-international. The product app includes three locales out of the box — English, French, and Spanish — and adding more is straightforward.
Architecture
apps/app/src/
├── locales/
│ ├── config.ts # locale list, types, and path helpers
│ ├── client.ts # client-side hooks and provider
│ ├── server.ts # server-side getI18n / getScopedI18n
│ ├── en.ts # English translations (default)
│ ├── fr.ts # French translations
│ └── es.ts # Spanish translations
├── proxy.ts # locale detection and URL rewriting (Next 16 convention)
└── app/
└── [locale]/ # all routes are nested under a locale segmentHow Locale Routing Works
The proxy at apps/app/src/proxy.ts uses createI18nMiddleware with
the rewrite URL mapping strategy (the file is named proxy.ts because that is
the Next.js 16 convention; middleware.ts is the deprecated name):
const I18nMiddleware = createI18nMiddleware({
locales,
defaultLocale,
urlMappingStrategy: "rewrite",
});- Rewrite strategy — the default locale (
en) is served without a prefix in the URL. Non-default locales are prefixed (e.g./fr/settings). The[locale]dynamic segment catches both cases. - Detection — on first visit,
next-internationalchecks theAccept-Languageheader and sets a cookie. Subsequent visits use the cookie.
Translation Files
Each locale file exports a flat-ish object with nested namespaces:
// locales/en.ts
export default {
navigation: {
dashboard: "Dashboard",
settings: "Settings",
// ...
},
dashboard: {
workspace: {
roleLabel: "Your role",
// ...
},
},
// ...
} as const;All locale files must share the same shape. TypeScript enforces this — if you
add a key to en.ts, the other locale files will produce type errors until you
add the corresponding translations.
Namespaces
Translations are organized by feature area:
| Namespace | Content |
|---|---|
navigation | Sidebar labels, page titles, user menu items |
dashboard | Workspace info card, records table, record dialog |
organization | Workspace switcher, create/rename/delete workspace |
members | Member list, invitations, invite dialog |
settings | Account settings, avatar, username, delete account |
security | Sensitive action verification dialog |
billing | Plan display, checkout, subscription management |
login | Sign-in page, magic link, social auth, password auth |
verify2fa | Two-factor verification challenge |
onboarding | Username setup flow |
acceptInvitation | Invitation acceptance flow |
errors | Generic error boundary messages |
Using Translations
In Client Components
"use client";
import { useI18n, useScopedI18n } from "@/locales/client";
function MyComponent() {
// Full namespace access
const t = useI18n();
t("navigation.dashboard"); // "Dashboard"
// Scoped to a namespace for cleaner calls
const tNav = useScopedI18n("navigation");
tNav("dashboard"); // "Dashboard"
}In Server Components
import { getI18n, getScopedI18n } from "@/locales/server";
export default async function Page() {
const t = await getI18n();
const tNav = await getScopedI18n("navigation");
return <h1>{tNav("dashboard")}</h1>;
}String Interpolation
Some translations include placeholders wrapped in {braces}:
// en.ts
readyTitle: "Join {workspace}",Pass parameters as the second argument:
t("acceptInvitation.readyTitle", { workspace: "Acme Team" });
// → "Join Acme Team"Getting the Current Locale
import { useCurrentLocale } from "@/locales/client";
const locale = useCurrentLocale(); // "en" | "fr" | "es"Changing the Locale
import { useChangeLocale } from "@/locales/client";
const changeLocale = useChangeLocale();
changeLocale("fr"); // navigates to the French versionThe Language Switcher
The built-in language switcher lives in the user dropdown menu in the sidebar
footer. It is implemented in
apps/app/src/app/[locale]/(dashboard)/_components/language-switcher.tsx as a
DropdownMenuSub with radio items for each locale.
Adding a language to the switcher requires adding it to the langs array in
that file:
const langs = [
{ text: "English", value: "en" },
{ text: "French", value: "fr" },
{ text: "Spanish", value: "es" },
{ text: "German", value: "de" }, // ← new
];Adding a New Locale
-
Create the translation file — duplicate
en.tsas your new locale file (e.g.de.ts) and translate all strings. -
Register the locale in three places:
// locales/config.ts export const locales = ["en", "fr", "es", "de"] as const;// locales/client.ts export const { useI18n, ... } = createI18nClient({ en: () => import("./en"), fr: () => import("./fr"), es: () => import("./es"), de: () => import("./de"), // ← new });// locales/server.ts export const { getI18n, ... } = createI18nServer({ en: () => import("./en"), es: () => import("./es"), fr: () => import("./fr"), de: () => import("./de"), // ← new }); -
Add to the language switcher — update the
langsarray inlanguage-switcher.tsxas shown above. -
Typecheck — run
bun run typecheckto verify the new file matches the expected shape.
Locale-Aware Route Helpers
The getAppPathname() function in locales/config.ts strips the locale
segment from a pathname so that route matching works against locale-agnostic
paths:
getAppPathname("/fr/settings"); // → "/settings"
getAppPathname("/settings"); // → "/settings"This is used by the sidebar and site header to determine the active navigation item regardless of the current locale prefix.
I18n Provider Placement
The I18nProviderClient wraps the dashboard layout, not the entire app.
This is intentional — public routes (login, invitation acceptance) that do not
use the dashboard layout still get i18n through the proxy's locale
detection and server-side getI18n.
// apps/app/src/app/[locale]/(dashboard)/layout.tsx
<I18nProviderClient locale={locale}>
{/* dashboard content */}
</I18nProviderClient>Next Reads
- Customizing the Dashboard — modifying navigation labels and the language switcher.
- UI Overview — how the UI package fits into the monorepo.
Customizing the Dashboard
The dashboard is the authenticated shell that signed-in users see. It includes a collapsible sidebar, a contextual header, an organization switcher, and a user menu. All of these are customizable.
Blog
Publish a blog post on the marketing site by adding a validated MDX file with the right frontmatter.