T
TanStack4mo ago
eager-peach

i18n urls and locales

Hi everyone, I'm using TanStack Router for my application, and I'm trying to implement localization for both the app content and the URLs. For example: myapp.com → English myapp.com/fr → French myapp.com/es → Spanish myapp.com/about → English myapp.com/fr/a-propos → French myapp.com/es/acerca-de → Spanish What I'd like to achieve is the ability to localise each each url without creating a file for each locale and be able to use <Link to="/about">About</Link> without having to manually specify the locale in the link props. I'm a bit stuck on how to handle the localized URLs part. I couldn’t find clear documentation or examples on how to structure routes or links to support this behavior.
15 Replies
genetic-orange
genetic-orange4mo ago
we dont have optional path params yet but are working on that this would most likely solve your issue
continuing-cyan
continuing-cyan4mo ago
How did you solve this for now? Have the exact same use-case, kind of a blocking factor for us to switch from Next to TSS Is there a way with the current types to create a custom version of <Link> for example where $lang is already filled as part of the type?
genetic-orange
genetic-orange4mo ago
we have Route.Link
eager-peach
eager-peachOP4mo ago
I was able to achieve the routing with the local as an optionnal parameter following this exemple https://github.com/IgorSzymanski/tanstack-start-paraglide But i couldn't achieve having multiple localized urls without duplicating my routes with tanstack start. I could do it just using the router and using the code based routing but not with the file based routing. And it seems that tanstack start doesn't allow using code based routing for now. That's whats keeping me from migrating from next js too :/
continuing-cyan
continuing-cyan4mo ago
Im not sure I follow. Was hoping we could possibly do something like this:
type MyCustomLinkProps<
TComp = "a",
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends string = string,
TTo extends string | undefined = ".",
TMaskFrom extends string = TFrom,
TMaskTo extends string = ".",
> = TanStackLinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
params: TanStackLinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>["params"] & {
locale?: AvailableLocale
}
}
type MyCustomLinkProps<
TComp = "a",
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends string = string,
TTo extends string | undefined = ".",
TMaskFrom extends string = TFrom,
TMaskTo extends string = ".",
> = TanStackLinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
params: TanStackLinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>["params"] & {
locale?: AvailableLocale
}
}
So that I can just pre-pass the locale as part of each link in the custom component
genetic-orange
genetic-orange4mo ago
you could build that yes but you should not need that if you use Route.Link of the $lang route then you don't have to supply the $lang param as it is taken over from the previous route
continuing-cyan
continuing-cyan4mo ago
Ahh okay, I see now. Thanks!! That'll do
foreign-sapphire
foreign-sapphire2mo ago
@Manuel Schiller Am I right to assume that it is now kind of supported with optional params (https://tanstack.com/router/latest/docs/framework/react/guide/path-params#complex-i18n-patterns), under the condition we define values matching a specific url Exemple for the following url: /rooms /fr/chambres Have a /{-$locale}/{-$rooms}.tsx route and validate both locale (fr or en) and rooms (rooms or chambres)?
Path Params | TanStack Router React Docs
Path params are used to match a single segment (the text until the next /) and provide its value back to you as a named variable. They are defined by using the $ character prefix in the path, followed...
genetic-orange
genetic-orange2mo ago
wouldn't make the second param optional
foreign-sapphire
foreign-sapphire2mo ago
Ok nevermind, I'm tired, makes sense. This setup doesn't work unfortunately:
export const Route = createFileRoute("/{-$locale}/$rooms")({
component: RoomsComponent,
beforeLoad: async ({ params }) => {
const locale = params.locale || defaultLocale; // defaultLocale = "fr"
const urlSegment = params.rooms;
console.log("BEFORELOAD", params);

// Validate locale
if (locale && !availableLocales.includes(locale as Locale)) {
console.log("Invalid locale");
throw new Error("Invalid locale");
}
// Validate url segment
// Here I check if the url segement for the locale is valid
// roomsPathSegments: { fr: "chambres", en: "rooms" }
if (urlSegment && !isUrlSegmentValid("rooms", urlSegment, locale)) {
console.log("Invalid segment");
throw new Error("Invalid url segment");
}

return { locale, rooms: urlSegment };
},
});
export const Route = createFileRoute("/{-$locale}/$rooms")({
component: RoomsComponent,
beforeLoad: async ({ params }) => {
const locale = params.locale || defaultLocale; // defaultLocale = "fr"
const urlSegment = params.rooms;
console.log("BEFORELOAD", params);

// Validate locale
if (locale && !availableLocales.includes(locale as Locale)) {
console.log("Invalid locale");
throw new Error("Invalid locale");
}
// Validate url segment
// Here I check if the url segement for the locale is valid
// roomsPathSegments: { fr: "chambres", en: "rooms" }
if (urlSegment && !isUrlSegmentValid("rooms", urlSegment, locale)) {
console.log("Invalid segment");
throw new Error("Invalid url segment");
}

return { locale, rooms: urlSegment };
},
});
When I navigate to /en/rooms, the log output is:
BEFORELOAD { locale: 'en', rooms: 'rooms' } BEFORELOAD { locale: '.well-known', rooms: 'appspecific', '**': 'com.chrome.devtools.json' } .well-known Invalid locale
I don't know why there is a second beforeLoad event firing, but the page displays correctly. However if I navigate to /chambres, the log output is:
BEFORELOAD { locale: '.well-known', rooms: 'appspecific', '**': 'com.chrome.devtools.json' } .well-known Invalid locale
And I get a not found error. It should detect that locale is undefined, thus fallback to the defaultLocale (fr), and the rooms param should be "chambres".
genetic-orange
genetic-orange2mo ago
the .wellknown is something that chrome seems to request you will get errors on the server console but you can just ignore those aside from that, a complete minimal example by forking one of the existing router examples on stackblitz (this is really just a router topic, not start) would help debug this
foreign-sapphire
foreign-sapphire2mo ago
genetic-orange
genetic-orange2mo ago
could very well be a bug can you please create a github issue for this?
foreign-sapphire
foreign-sapphire2mo ago
GitHub
i18n with optional path parameters broken when optional path not se...
Which project does this relate to? Router Describe the bug Hi, I&#39;m trying to get the i18n with optional path parameters work in order to have localized urls. According to the docs, it should be...
genetic-orange
genetic-orange2mo ago
GitHub
Release v1.130.9 · TanStack/router
Version 1.130.9 - 7/30/25, 10:01 PM Changes Fix correctly match routes with optional path params followed by req… (#4838) (ce9e373) by Manuel Schiller Ci apply automated fixes (14ad18f) by autof...

Did you find this page helpful?