Internationalization (i18n)
Introduction
The i18n configuration enables the auth package to work with localized pathnames and routing strategies.
The auth package's i18n configuration is required when locale influences URL pathnames in your application.
Common scenarios:
- Using Next.js built-in i18n with sub-path routing (
/fr/dashboard) - Translating route segments (
/fr/tableau-de-bordinstead of/fr/dashboard) with libraries likenext-intl - Implementing custom locale-based routing strategies
Skip this configuration if:
- Your app is single-language only
- Routes use identical pathnames across all locales
Getting started
Complete the Getting Started: Pages Router or Getting Started: App Router guide. This i18n configuration builds on top of the base authentication setup.
Configuration
| Option | Type | Required | Description |
|---|---|---|---|
i18n.localeCookie | string | ❌ | Name of the cookie storing user's locale preference |
i18n.getLocalizedPathname | <Pathname extends string>(options: { locale?: string; pathname: Pathname; url: URL }) => string | Promise<string> | ✅ | Callback function to resolve localized pathnames |
The auth middleware needs to operate on the final localized pathname to
accurately match routes and redirect users using localized pathnames from
getLocalizedPathname.
All i18n-related redirects must be handled before the auth middleware processes the request. If your i18n middleware needs to redirect (e.g., sub-path routing, pathnane translation), you need to detect redirect responses and return early.
Learn more
- Proxy (Next.js 15+)
- Middleware (Next.js 14)
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import createNextIntlMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
import { authConfig } from "@/lib/auth/config";
const nextIntlMiddleware = createNextIntlMiddleware(routing);
const authMiddleware = createAuthMiddleware(authConfig);
export function proxy(request: NextRequest) {
const response = nextIntlMiddleware(request);
// If i18n middleware is redirecting, return early
if (!response.ok) return response;
return authMiddleware(request, response);
}
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import createNextIntlMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
import { authConfig } from "@/lib/auth/config";
const nextIntlMiddleware = createNextIntlMiddleware(routing);
const authMiddleware = createAuthMiddleware(authConfig);
export function middleware(request: NextRequest) {
const response = nextIntlMiddleware(request);
// If i18n middleware is redirecting, return early
if (!response.ok) return response;
return authMiddleware(request, response);
}
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
Examples
- Next.js sub-path routing
- Next.js domain routing
- next-intl
If your project uses Next.js built-in i18n features with the default
sub-path routing
strategy, you can define a custom getLocalizedPathname function to emulate
sub-path routing.
export const LOCALES = ["en", "fr", "de"] as const;
export const DEFAULT_LOCALE = "en";
export type Locale = (typeof LOCALES)[number];
export function hasLocale(locale: string | undefined): locale is Locale {
return !!localeCookie && LOCALES.includes(localeCookie as Locale);
}
export function resolveLocale(
localeCookie: string | undefined,
url: URL,
): Locale {
if (hasLocale(cookieLocale)) return cookieLocale;
const pathnameLocale = url.pathname.split("/")[1];
if (hasLocale(pathnameLocale)) return pathnameLocale;
return DEFAULT_LOCALE;
}
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { DEFAULT_LOCALE, resolveLocale } from "@/lib/i18n";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
login: { pathname: "/login" },
home: { pathname: "/" },
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, pathname, url }) {
const resolvedLocale = resolveLocale(locale, url);
// By default, Next.js does not prefix the default locale
// Remove this if you opted-in to prefix the default locale
if (resolvedLocale === DEFAULT_LOCALE) {
return pathname;
}
return `/${resolvedLocale}${pathname}`;
},
},
// ... other config
});
If your project uses Next.js built-in i18n features with the
domain routing
strategy and translatedRoutes, you can define a custom getLocalizedPathname
function to retrieve the locale from the URL.
If your project uses the domain routing without translated routes, you can omit i18n configuration for the auth package.
export const LOCALES = ["en", "fr", "de"] as const;
export const DEFAULT_LOCALE = "en";
export const ROUTE_TRANSLATIONS = {
"/dashboard": {
fr: "/espace-client",
de: "/armaturenbrett",
},
"/dashboard/accounts": {
fr: "/espace-client/comptes",
de: "/armaturenbrett/konten",
},
// other routes...
};
export type Locale = (typeof LOCALES)[number];
export function hasLocale(locale: string | undefined): locale is Locale {
return !!localeCookie && LOCALES.includes(localeCookie as Locale);
}
// sub-domain locale: fr.example.com -> fr, example.com -> en
export function resolveLocale(
cookieLocale: string | undefined,
url: URL,
): Locale {
if (hasLocale(cookieLocale)) return cookieLocale;
const domainLocale = url.host.split(".")[0];
if (hasLocale(domainLocale)) return domainLocale;
return DEFAULT_LOCALE;
}
export const DOMAIN_EXTENSIONS = {
com: "en",
de: "de",
fr: "fr",
};
// domain extension locale: example.fr -> fr, example.com -> en
export function resolveLocale(
cookieLocale: string | undefined,
url: URL,
): Locale {
if (hasLocale(cookieLocale)) return cookieLocale;
const domainLocale = DOMAIN_EXTENSIONS[url.host.split(".").pop()];
if (hasLocale(domainLocale)) return domainLocale;
return DEFAULT_LOCALE;
}
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { resolveLocale, ROUTE_TRANSLATIONS } from "@/lib/i18n";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
login: { pathname: "/login" },
home: { pathname: "/" },
},
i18n: {
getLocalizedPathname({ locale, pathname, url }) {
return (
ROUTE_TRANSLATIONS[pathname]?.[resolveLocale(locale, url)] ?? pathname
);
},
},
// ... other config
});
If you project uses the next-intl middleware, you can use the getPathname
and hasLocale functions.
next-intl without middlewareIf your project uses next-intl without the middleware (e.g. Pages Router), you
most likely rely on Next.js built-in i18n features. Check out the dedicated
examples.
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { getPathname } from "@/i18n/navigation";
import { hasLocale } from "next-intl";
import { routing } from "@/i18n/routing";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: { pathname: "/dashboard" },
login: { pathname: "/login" },
home: { pathname: "/" },
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, pathname, url }) {
const requestedLocale = locale ?? url.pathname.split("/")[1];
return getPathname({
href: { pathname },
locale: hasLocale(routing.locales, requestedLocale)
? requestedLocale
: routing.defaultLocale,
});
},
},
// ... other config
});
See the next-intl documentation for setup instructions.
FAQ
How does the middleware use getLocalizedPathname?
The middleware calls getLocalizedPathname for each configured route in
appRoutes to build a list of localized pathnames to match against:
- Extract locale from cookie (if
localeCookieis configured) - Call
getLocalizedPathnamefor each route:dashboard,login,anon,masquerade - Match incoming request pathname against localized paths
- Apply appropriate authentication logic
For example, with French locale and translated routes:
/dashboard→/fr/tableau-de-bord/login→/fr/connexion- Middleware matches incoming
/fr/tableau-de-bordrequest to dashboard route
Do I need to update my middleware matcher?
Yes, if you use localized route segments. Your middleware matcher should
include all localized variations:
export const config = {
matcher: [
// English routes
"/dashboard/:path*",
"/login",
"/anon/:key",
// French routes
"/fr/tableau-de-bord/:path*",
"/fr/connexion",
"/fr/anon/:key",
// German routes
"/de/armaturenbrett/:path*",
"/de/anmelden",
"/de/anon/:key",
],
};
Alternatively, use a catch-all pattern:
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
How do I use allowList with i18n routing?
The allowList glob patterns are matched against localized pathnames. This
means your globs should include every supported locale or translated pathname.
- Next.js sub-path routing
- Next.js domain routing
- next-intl
Use extglob ?(pattern) syntax to match paths with or without locale prefixes:
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { DEFAULT_LOCALE, resolveLocale } from "@/lib/i18n";
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: {
pathname: "/dashboard",
// Matches: /dashboard/switch-tariff/*, /fr/dashboard/switch-tariff/*, /de/dashboard/switch-tariff/*
allowList: ["/?({fr/,de/})dashboard/switch-tariff/*"],
},
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, pathname, url }) {
const resolvedLocale = resolveLocale(locale, url);
// By default, Next.js does not prefix the default locale
if (resolvedLocale === DEFAULT_LOCALE) {
return pathname;
}
return `/${resolvedLocale}${pathname}`;
},
},
});
Without translated routes:
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: {
pathname: "/dashboard",
allowList: ["/dashboard/switch-tariff/*"],
},
},
});
With translated routes:
function getAllowList(basePath: string, suffix: string): string[] {
return Object.values(ROUTE_TRANSLATIONS[basePath]).map(
(localizedPath) => `${localizedPath}${suffix}`,
);
}
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: {
pathname: "/dashboard",
allowList: getAllowList("/dashboard/switch-tariff", "/*"),
},
},
i18n: {
getLocalizedPathname({ locale, pathname, url }) {
return (
ROUTE_TRANSLATIONS[pathname]?.[resolveLocale(locale, url)] ?? pathname
);
},
},
});
Programmatically derive allowList using getPathname:
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { getPathname } from "@/i18n/navigation";
import { hasLocale } from "next-intl";
import { routing } from "@/i18n/routing";
function getAllowList(basePath: string, suffix: string): string[] {
return routing.locales.map((locale) => {
const localizedPath = getPathname({
href: { pathname: basePath },
locale,
});
return `${localizedPath}${suffix}`;
});
}
export const authConfig = createAuthConfig({
appRoutes: {
dashboard: {
pathname: "/dashboard",
// Matches: /dashboard/switch-tariff/*, /tableau-de-bord/changement-offre/*
allowList: getAllowList("/dashboard/switch-tariff", "/*"),
},
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, pathname, url }) {
const requestedLocale = locale ?? url.pathname.split("/")[1];
return getPathname({
href: { pathname },
locale: hasLocale(routing.locales, requestedLocale)
? requestedLocale
: routing.defaultLocale,
});
},
},
});
See the picomatch docs for all supported glob patterns.
Next Steps
Now that you have i18n configured, explore these related guides:
- Kraken OAuth: Configure OAuth authentication with localized redirect URLs
- Anonymous Auth: Use pre-signed keys with localized anonymous routes
- Masquerade: Staff impersonation with locale-aware redirects
- Building Forms Tutorial: Create localized login forms with i18n support
API Reference
Quick reference to i18n-related configuration and functions:
createAuthConfig: Configure i18n options in your auth configcreateAuthMiddleware: Middleware automatically uses i18n config for route matching and redirectslogin: Server function respects i18n for redirect destinationslogout: Server function respects i18n for redirect destinationscreateLoginHandler: API handler uses i18n for redirectscreateLogoutHandler: API handler uses i18n for redirectscreateKrakenOAuthHandler: OAuth handler uses i18n for redirectsuseKrakenAuthErrorHandler: Client hook uses i18n for error redirects
If the i18n API doesn't meet your specific requirements or doesn't integrate well with your i18n tooling, please reach out to the Blueprint team. We're happy to discuss your use case and potentially extend the API to support additional patterns.