When it comes to managing app routes for an i18n application, I initially relied on reading the request headers (cookies and accept-language) to determine the locale. The experimental typedRoutes
feature was working perfectly fine in this setup. However, I recently received advice to move the locale information into the pathname itself. Even though I can still construct paths without directly including the locale segment, the middleware is now responsible for redirecting to the correct path based on the same lookup method as before. While this approach eliminates a round-trip and reduces computational load, it might be more efficient to include the current locale in the path from the beginning. For reference, I am using i18next
.
// src/i18n/settings.ts
import { InitOptions } from "i18next";
import type { Route } from "next";
import * as config from "@/services/config-service/service-public.mjs";
export const SUPPORTED_LOCALES = ["en-US"] as const;
export const FALLBACK_LOCALE = SUPPORTED_LOCALES[0];
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
export const cookieName = config.i18nName;
export const getOptions = () =>
({
debug: config.debugI18n,
fallbackLng: FALLBACK_LOCALE,
supportedLngs: SUPPORTED_LOCALES
}) satisfies InitOptions as InitOptions;
export const i18nPathname = <
Locale extends SupportedLocale,
LogicalPathname = string extends Route<`/${Locale}${infer Pathname}`>
? Pathname
: never
>(
locale: SupportedLocale,
logicalPathname: LogicalPathname
) => (logicalPathname === "/" ? `/${locale}` : `/${locale}${logicalPathname}`);
// some random page or action
redirect(i18nPathname(locale, "/profile/welcome"));
redirect(`/${locale}/profile/welcome`);
Although I haven't encountered any type errors with this setup, the return type of i18nPathname()
remains as string
. Furthermore, there seems to be no assistance from autocomplete. At this point, the distinction between my function and direct interpolation is unclear.
- Is there a way to achieve correct pathname typing (including autocomplete and typo checks)?
- Are there alternative approaches that could help me reach my desired outcome?
I have observed the path generation in TanStack Router and would love to implement something similar with Next!
Edit:
Below is the structure of my app
folder, which should contain nothing unexpected.
t --only-dirs src/app
src/app/
├── [locale]/
│ ├── _actions/
│ ├── about/
│ ├── design-system/
│ │ ├── cards/
│ │ ├── elevation/
│ │ ├── input/
│ │ ├── interactive/
│ │ ├── palette/
│ │ ├── table/
│ │ └── typography/
│ ├── profile/
│ │ ├── verification/
│ │ │ └── _actions/
│ │ ├── verification-prompt/
│ │ │ └── _actions/
│ │ └── welcome/
│ ├── profiles/
│ │ └── new/
│ │ └── _actions/
│ └── sessions/
│ ├── assistance/
│ │ └── password/
│ │ ├── _actions/
│ │ ├── new/
│ │ │ └── _actions/
│ │ └── prompt/
│ └── new/
│ └── _actions/
└── api/
└── cron/
├── password-resets/
└── sessions/
Edit 2:
This is how Route
is defined by Next:
export type Route<T extends string = string> =
__next_route_internal_types__.RouteImpl<T>
In the same file (.next/types/link.d.ts
), here's an excerpt of __next_route_internal_types__
. Keep in mind that StaticRoutes
and DynamicRoutes
may vary from one application to another.
type SafeSlug<S extends string> = S extends `${string}/${string}`
? never
: S extends `${string}${SearchOrHash}`
? never
: S extends ''
? never
: S
// ...
type StaticRoutes =
| `/api/cron/password-resets`
| `/api/cron/sessions`
type DynamicRoutes<T extends string = string> =
| `/${SafeSlug<T>}`
| `/${SafeSlug<T>}/about`
| `/${SafeSlug<T>}/profile/verification`
| `/${SafeSlug<T>}/profile/verification-prompt`
| `/${SafeSlug<T>}/sessions/assistance`
| `/${SafeSlug<T>}/sessions/assistance/password/new`
| `/${SafeSlug<T>}/profile`
| `/${SafeSlug<T>}/profile/welcome`
| `/${SafeSlug<T>}/sessions/assistance/password`
| `/${SafeSlug<T>}/sessions/new`
| `/${SafeSlug<T>}/sessions/assistance/password/prompt`
| `/${SafeSlug<T>}/sessions`
| `/${SafeSlug<T>}/profiles/new`
| `/${SafeSlug<T>}/design-system/input`
| `/${SafeSlug<T>}/design-system`
| `/${SafeSlug<T>}/design-system/palette`
| `/${SafeSlug<T>}/design-system/table`
| `/${SafeSlug<T>}/design-system/cards`
| `/${SafeSlug<T>}/design-system/interactive`
| `/${SafeSlug<T>}/design-system/typography`
| `/${SafeSlug<T>}/design-system/elevation`
type RouteImpl<T> =
| StaticRoutes
| SearchOrHash
| WithProtocol
| `${StaticRoutes}${SearchOrHash}`
| (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)
I do plan on incorporating additional dynamic path parameters besides [locale]
in due time. Currently, these functionalities are yet to be developed.