Internationalization (i18n)
Introduction​
Use the i18n configuration when a locale changes a route pathname. Blueprint Auth uses the configuration to match protected routes and to create redirect URLs.
Configure i18n for these routing strategies:
- Next.js subpath routing, such as
/fr/dashboard - Translated route segments, such as
/fr/tableau-de-bord - Custom locale routing that changes a pathname
You can omit this configuration when all locales use the same pathnames. You can also omit it for domain routing when only the domain changes.
Before you begin​
Complete the Pages Router setup or the App Router setup.
Configuration​
| Option | Type | Required | Description |
|---|---|---|---|
i18n.localeCookie | string | No | The name of the cookie that stores the locale. The default is NEXT_LOCALE. |
i18n.getLocalizedPathname | (options) => string | Yes | Returns the localized pathname for one configured route. |
getLocalizedPathname receives locale, pathname, params, and url.
params is present for a configured pathname that has dynamic segments.
Always include the required home route in appRoutes:
import { createAuthConfig } from "@krakentech/blueprint-auth";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ pathname }) {
return pathname;
},
},
});
Replace the example callback with one of the routing strategies below.
Run locale middleware first​
Locale middleware can redirect a request before auth runs. Return that redirect without calling the auth middleware. Pass other responses to the auth middleware. This preserves rewrites and response headers.
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import createNextIntlMiddleware from "next-intl/middleware";
import type { NextRequest } from "next/server";
import { routing } from "@/i18n/routing";
import { authConfig } from "@/lib/auth/config";
const handleI18n = createNextIntlMiddleware(routing);
const handleAuth = createAuthMiddleware(authConfig);
export async function proxy(request: NextRequest) {
const response = handleI18n(request);
if (response.headers.has("location")) {
return response;
}
return handleAuth(request, response);
}
export const config = {
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
};
For Next.js 15 or earlier, save this file as middleware.ts and rename proxy
to middleware.
Routing examples​
- Next.js subpath routing
- Translated domain routing
- next-intl
Use this example with the Pages Router subpath routing feature. It does not prefix the default locale.
export const LOCALES = ["en", "fr", "de"] as const;
export const DEFAULT_LOCALE = "en";
export type Locale = (typeof LOCALES)[number];
export function isLocale(value: string | undefined): value is Locale {
return value !== undefined && LOCALES.some((locale) => locale === value);
}
export function resolveLocale(
locale: string | undefined,
url: URL,
): Locale {
if (isLocale(locale)) {
return locale;
}
const pathnameLocale = url.pathname.split("/")[1];
return isLocale(pathnameLocale) ? pathnameLocale : DEFAULT_LOCALE;
}
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { DEFAULT_LOCALE, resolveLocale } from "@/lib/i18n";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, pathname, url }) {
const resolvedLocale = resolveLocale(locale, url);
if (resolvedLocale === DEFAULT_LOCALE) {
return pathname;
}
return pathname === "/"
? `/${resolvedLocale}`
: `/${resolvedLocale}${pathname}`;
},
},
});
Use a host map when the domain selects the locale. Add a translation for every configured app route. This keeps the translation lookup type safe.
export const LOCALES = ["en", "fr", "de"] as const;
export const DEFAULT_LOCALE = "en";
export type Locale = (typeof LOCALES)[number];
const DOMAIN_LOCALES: Readonly<Record<string, Locale>> = {
"example.com": "en",
"example.de": "de",
"example.fr": "fr",
};
export const LOCALIZED_PATHNAMES = {
"/": {
de: "/",
en: "/",
fr: "/",
},
"/dashboard": {
de: "/armaturenbrett",
en: "/dashboard",
fr: "/espace-client",
},
"/login": {
de: "/anmelden",
en: "/login",
fr: "/connexion",
},
} as const;
export type AppPathname = keyof typeof LOCALIZED_PATHNAMES;
function isLocale(value: string | undefined): value is Locale {
return value !== undefined && LOCALES.some((locale) => locale === value);
}
export function resolveDomainLocale(
locale: string | undefined,
url: URL,
): Locale {
if (isLocale(locale)) {
return locale;
}
return DOMAIN_LOCALES[url.hostname] ?? DEFAULT_LOCALE;
}
export function getTranslatedPathname(
pathname: AppPathname,
locale: Locale,
): string {
return LOCALIZED_PATHNAMES[pathname][locale];
}
import { createAuthConfig } from "@krakentech/blueprint-auth";
import {
getTranslatedPathname,
resolveDomainLocale,
} from "@/lib/i18n";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
i18n: {
getLocalizedPathname({ locale, pathname, url }) {
return getTranslatedPathname(
pathname,
resolveDomainLocale(locale, url),
);
},
},
});
Use the getPathname function from your next-intl navigation configuration.
Keep pathname and params together in the href object.
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { hasLocale } from "next-intl";
import { getPathname } from "@/i18n/navigation";
import { routing } from "@/i18n/routing";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, url, ...href }) {
const requestedLocale = locale ?? url.pathname.split("/")[1];
const resolvedLocale = hasLocale(routing.locales, requestedLocale)
? requestedLocale
: routing.defaultLocale;
return getPathname({ href, locale: resolvedLocale });
},
},
});
The rest object is a discriminated union. A dynamic pathname stays paired with
its required parameters. TypeScript can then validate the getPathname call.
See the next-intl documentation for its full setup.
Redirect protected requests​
redirectToLogin uses the same i18n configuration as the middleware. It
localizes the login route. It uses the current request pathname as nextPage by
default.
import { BlueprintAuthErrorCode } from "@krakentech/blueprint-auth";
import type { GetServerSidePropsContext } from "next";
import { getAuth, redirectToLogin } from "@/lib/auth/server";
export async function getServerSideProps(context: GetServerSidePropsContext) {
const auth = await getAuth.user({ context });
if (!auth) {
return redirectToLogin({
context,
errorCode: BlueprintAuthErrorCode.AuthenticationRequired,
});
}
return { props: {} };
}
Pass nextPage: null when the login URL must omit the return destination.
Dynamic routes​
Blueprint Auth passes wildcard values for dynamic segments when it builds route
matchers. A single segment receives "*". A catch-all segment receives
["**"].
This callback supports static routes and dynamic routes:
getLocalizedPathname({ locale, url, ...href }) {
const resolvedLocale = resolveLocale(locale, url);
return getPathname({ href, locale: resolvedLocale });
},
Add every dynamic pathname from appRoutes and allowList to your translated
pathname configuration. If the localization callback throws, Blueprint Auth
falls back to a nonlocalized glob. That fallback might not match translated
segments.
Middleware matchers​
Use the recommended catch-all matcher:
export const config = {
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
};
Localized allowlists​
Blueprint Auth also passes allowList entries to getLocalizedPathname. Use
base pathnames in the configuration:
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { hasLocale } from "next-intl";
import { getPathname } from "@/i18n/navigation";
import { routing } from "@/i18n/routing";
export const authConfig = createAuthConfig({
appRoutes: {
anon: {
pathname: "/feedback/[feedbackId]",
getAnonParams({ url }) {
return { preSignedKey: url.searchParams.get("key") };
},
allowList: ["/feedback/[feedbackId]/public-info"],
},
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
i18n: {
getLocalizedPathname({ locale, url, ...href }) {
const requestedLocale = locale ?? url.pathname.split("/")[1];
const resolvedLocale = hasLocale(routing.locales, requestedLocale)
? requestedLocale
: routing.defaultLocale;
return getPathname({ href, locale: resolvedLocale });
},
},
});
With translated next-intl pathnames, prefer Next.js dynamic segment syntax.
A glob such as /feedback/* is not a pathname key, so getPathname cannot
translate it.
Next steps​
- Kraken OAuth: configure localized OAuth redirects.
- Anonymous authentication: configure localized anonymous routes.
- Masquerade authentication: configure localized staff routes.
- App Router forms: build an App Router login form.