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 | undefined; pathname: Pathname; params?: Record<string, string | string[]>; url: URL }) => 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, url, ...href }) {
const requestedLocale = locale ?? url.pathname.split("/")[1];
return getPathname({
href,
locale: hasLocale(routing.locales, requestedLocale)
? requestedLocale
: routing.defaultLocale,
});
},
},
// ... other config
});
getLocalizedPathname receives a discriminated union: each pathname
literal is paired with its exact params shape (or no params for static
routes). By destructuring { locale, url, ...href }, the rest object href
naturally contains { pathname, params } for dynamic routes or just
{ pathname } for static routes, preserving the discriminant so TypeScript
can verify the pathname/params pairing without any type assertions.
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",
],
};
Maintaining per-locale matchers gets cumbersome as routes and translations grow. A catch-all pattern is simpler:
export const config = {
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
};
How do dynamic segments work with localized routing?
When a route uses
dynamic segment syntax
(e.g. /dashboard/[accountNumber]), the middleware passes wildcard params to
getLocalizedPathname: "*" for [param], ["**"] for catch-all segments.
The next-intl example above already handles this. The spread
pattern { locale, url, ...href } preserves the pathname/params pairing:
getLocalizedPathname({ locale, url, ...href }) {
return getPathname({ ...href, locale: resolveLocale(locale, url) });
},
next-intl substitutes the params, producing a translated glob like
/fr/tableau-de-bord/* that matches /fr/tableau-de-bord/A-12345.
How do I use allowList with i18n routing?
The allowList patterns are resolved through getLocalizedPathname
automatically, just like route pathnames. Write your allowList entries using
untranslated base paths. The middleware translates them for the current locale
before matching:
appRoutes: {
anon: {
pathname: "/anon/[preSignedKey]",
getAnonParams({ url }) { /* ... */ },
allowList: [
"/anon/[preSignedKey]/public-info", // Next.js dynamic segment
"/anon/dashboard/*/change-plan/**", // picomatch glob
"/anon/dashboard/public-profile", // static path
],
},
}
For a French user, /anon/[preSignedKey]/public-info is resolved to
/fr/anon/*/info-publique before matching.
If you use translated pathnames, prefer
dynamic segment syntax over globs. getPathname performs a key-based
lookup, so a glob like /anon/* won't match any configured route key.
It won't be translated, and won't match the request URL.
This doesn't apply if you use next-intl without translated pathnames.
What happens if my dynamic segment route is not in my next-intl config?
If the dynamic route segment is not in your next-intl routing config and use the
recommended implementation, you will get a TypeScript error, and getPathname
will throw an error at runtime. When getLocalizedPathname throws, the
middleware falls back to glob-only matching: it converts the dynamic segments
to picomatch wildcards (e.g. /dashboard/[accountNumber] becomes
/dashboard/*) and matches against that pattern instead.
This means the route is still protected, but without locale-aware path translation. The glob fallback works reliably for the default locale (where pathnames are typically untranslated), but may fail to match requests in other locales if your app uses translated route segments.
Make sure all dynamic segment pathnames in your appRoutes and allowList are
also defined in your next-intl routing configuration to get correct localized
matching across all locales.
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.