Skip to main content

Internationalization (i18n)

Introduction

The i18n configuration enables the auth package to work with localized pathnames and routing strategies.

Do you need i18n configuration?

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-bord instead of /fr/dashboard) with libraries like next-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

Before you begin

Complete the Getting Started: Pages Router or Getting Started: App Router guide. This i18n configuration builds on top of the base authentication setup.

Configuration

OptionTypeRequiredDescription
i18n.localeCookiestringName 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 }) => stringCallback function to resolve localized pathnames
i18n-related redirects

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.ts
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|.*\\..*).*)"],
};

Examples

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;
}
lib/auth/config.ts
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
});

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:

  1. Extract locale from cookie (if localeCookie is configured)
  2. Call getLocalizedPathname for each route: dashboard, login, anon, masquerade
  3. Match incoming request pathname against localized paths
  4. 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-bord request 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:

middleware.ts
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:

lib/auth/config.ts
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.

Translated pathnames with next-intl

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:

API Reference

Quick reference to i18n-related configuration and functions:

Need Help?

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.