vercel / next.js

The React Framework
https://nextjs.org
MIT License
127.44k stars 27.04k forks source link

[Example needed] i18n with Next.js 13 and `app` directory #41980

Closed jimjamdev closed 1 year ago

jimjamdev commented 2 years ago

Verify canary release

Provide environment information

Operating System: Platform: win32 Arch: x64 Version: Windows 10 Home Binaries: Node: 16.15.0 npm: N/A Yarn: N/A pnpm: N/A Relevant packages: next: 13.0.1-canary.0 eslint-config-next: 13.0.0 react: 18.2.0 react-dom: 18.2.0

What browser are you using? (if relevant)

Chrome

How are you deploying your application? (if relevant)

Local

Describe the Bug

Setting up i18n test in next.config as follows:

experimental: {
    appDir: true
  },
i18n: {
    locales: ['en', 'fr'],
    defaultLocale: 'en',
    localeDetection: true
  },

I've deleted pages and added a few files into the new /app folder to test

Creating a simple component like so:

import { ReactNode } from 'react';

export default function Layout({ locale, children, ...rest }: {
  children: ReactNode;
  locale?: string;
}) {
  console.log('rest', rest);
  return <main className="layout">
    <header><h3 style={{ color: 'tomato' }}>{locale || 'nope'}</h3></header>
    {children}
  </main>;
}

I'm not seeing any locale information provided via the props. I was thinking this would be provided on the server side for rendering locale specific data on the server.

Expected Behavior

I would expect to be passed the current locale for use within sever components layouts/pages.

Link to reproduction

https://github.com/jimmyjamieson/nextjs-13-ts

To Reproduce

npm dev, check output of props, locale in layout

aej11a commented 2 years ago

Hi @jimmyjamieson - it's noted in the beta docs here that they're not planning i18n support in /app

Have you tried something like this? /app/[locale]/<all project files/subfolders here> That should give you a similar system to the old /pages routing behavior.

And you could probably use middleware to enable/disable specific locales.

jimjamdev commented 2 years ago

@aej11a Yeah, I've tried that, but that just gives a 404. I also tried [...locale] and [[...locale]] which works for direct pages. But sub folders will throw an Catch-all must be the last part of the URL. error

I would be of benefit of allowing sub-folders under a catch-all to make internationalization easier. Will have to wait for the v13 docs to see what they suggest regarding middleware.

update: It also seems middleware isn't being loaded

leonbe02 commented 2 years ago

Middleware only seems to work for routes under /pages, anything under /app does not run your middleware. We have the use-case where our default locale (en-us) does not have a subdirectory i.e. /about vs /en-us/about so there's not an easy way to replicate the behavior of the old i18n feature from <= Next 12 Would love to hear from one of the Nextjs devs on how they foresee i18n routing being implemented on Next 13+

johnkahn commented 2 years ago

Middleware seems to execute correctly if you keep the pages directory. My setup has all my pages in the app directory, and then I have a pages folder with just a .gitkeep file in it. That seems to be enough to get Next to run the middleware file for all routes in the app folder.

There's an open issue for middleware not working when the pages folder is removed. There's also a placeholder menu item on the beta docs site for middleware, so I would assume there is probably gonna be a new method in the future for adding middleware in the app folder.

jimjamdev commented 2 years ago

@johnkahn yeah, adding my 404.tsx into the pages directory has middleware working now and can access everything in the request.

siinghd commented 2 years ago

@jimmyjamieson How did you manage to solve the issue? Everytime i'm getting redirected to default locale and can't do nothing with the middleware.

guttenbergovitz commented 2 years ago

actually added [langcode]/test/bunga while having locales de-DE and en-GB and obviously it was 404ing. but to my suprise /en-GB/en-GB/test/bunga and /de-DE/de-DE/bunga worked fine... any ideas?

jimjamdev commented 2 years ago

@guttenbergovitz I would maybe report that as a bug.

jimjamdev commented 2 years ago

@siinghd You can see in my public repo above what I currently have.

minnyww commented 2 years ago

how to solve this issue ?

guttenbergovitz commented 2 years ago

@guttenbergovitz I would maybe report that as a bug.

Well... The issue is I am not 100% convinced it is a bug not the default behaviour I do not understand yet. 🤔

jimjamdev commented 2 years ago

@guttenbergovitz in my repo above it seems to work. My middleware isn't exactly feature complete and messy, but the redirect is working. Links need the locale to work - or possibly wrap next/link. But you can manually add /en/sub and see it gets the correct page

nbouvrette commented 2 years ago

Have you tried something like this? /app/[locale]/<all project files/subfolders here>

I tried this and it works on my end. I also didn't configure the i18n values in the config since its clearly no longer supported. Basically, I created my own config file like this (called it locales.json):

{
  "i18n": {
    "locales": ["en-US", "fr-CA"],
    "defaultLocale": ["en-US"]
  }
}

Then at the root of the app directory I created a [locale] directory where I can decide which locale will be valid, with something like this:

type I18nConfig = {
  i18n: {
    locales: string[];
    defaultLocale: string;
  };
};

export default async function Layout({
  children,
  params,
}: PageProps) {
  const locale = params.locale as string;
  const locales = (i18nConfig as unknown as I18nConfig).i18n.locales.map(
    (locale) => locale.toLowerCase()
  );
  if (!locale || !locales.includes(locale)) {
    return null;
  }
  return (
    <div>
      <div>Locale: {params.locale}</div>
      <div>{children}</div>
    </div>
  );
}

Of course, this is very basic and raw (I was just testing options). I would probably expect more mature packages to be released soon to handle this instead of relying on Next.js.

I myself maintain a Next.js i18n package that relies heavily on pages and the locale config and I suspect it will take a while before I can adapt my package to the app directory because of how many changes there are.

jimjamdev commented 2 years ago

Have you tried something like this? /app/[locale]/<all project files/subfolders here>

I tried this and it works on my end. I also didn't configure the i18n values in the config since its clearly no longer supported. Basically, I created my own config file like this (called it locales.json):

{
  "i18n": {
    "locales": ["en-US", "fr-CA"],
    "defaultLocale": ["en-US"]
  }
}

Then at the root of the app directory I created a [locale] directory where I can decide which locale will be valid, with something like this:

type I18nConfig = {
  i18n: {
    locales: string[];
    defaultLocale: string;
  };
};

export default async function Layout({
  children,
  params,
}: PageProps) {
  const locale = params.locale as string;
  const locales = (i18nConfig as unknown as I18nConfig).i18n.locales.map(
    (locale) => locale.toLowerCase()
  );
  if (!locale || !locales.includes(locale)) {
    return null;
  }
  return (
    <div>
      <div>Locale: {params.locale}</div>
      <div>{children}</div>
    </div>
  );
}

Of course, this is very basic and raw (I was just testing options). I would probably expect more mature packages to be released soon to handle this instead of relying on Next.js.

I myself maintain a Next.js i18n package that relies heavily on pages and the locale config and I suspect it will take a while before I can adapt my package to the app directory because of how many changes there are.

You might be better just doing the check and any redirects in the middleware itself.

aldabil21 commented 2 years ago

Hi @jimmyjamieson

I was testing to implement i18n with Next13, there are some limitations, I've looked into your repo. the limitations I found still exists, please correct me:

  1. This is only handle the case where user hit the home route, what about if a user hit somehwere else like domain.com/some/path ? it will 404. So you would tweak the matcher to run on each route something like matcher: "/:path*", in this case, you will also need to filter requests somehow cuz you wouldn't want to care about chunks & static requests... Have you got into this?

  2. How you would reach the locale inside a nested component? this approach seems encourage prop drilling? Although this approached is somewhat based on 41745#. So as a work around, I tried to wrap a provider in the root app to be able to provide locale with a hook, but that will result in turning most of the app into client components, which is not what Next 13 is about, server component first and by default.

Playground in this repo, any input is appreciated

jimjamdev commented 2 years ago

Yeah you're right, this is only for home. You'd have to call it for all requests and modify the redirect path to include the locale in front of the initial url.

I've still to look into the other issue, but I understand what you mean. I thought about merging the locale data with the params props on page/layout and always returning it there

aldabil21 commented 2 years ago

I've somewhat reached an initial solution, I managed to use i18next with fs-loader, and could keep an instance of i18next in the server. Not sure how good/bad this solution be, anyone can give an input to this repo would appreciate it https://github.com/aldabil21/next13-i18n

What I've learned on the way:

  1. Trying to use generateStaticParams as mentioned here 41745# will not work at all, adding generateStaticParams in the layout will prevent you from using any "next/navigation" or "next/headers" in any nested routes, a vague error message will show: Error: Dynamic server usage: .... Maybe Next team will make some changes about it in future?
  2. There is no way to know the current path on the server. known the current path only available in client-component hooks such as "usePathname".
  3. This solution is server-component only, once you use "use client" it will not work, cuz of the fs-backend for i18next.
leerob commented 2 years ago

Just want to follow up here and say an example for how to use i18n with the app directory is planned, we just haven't made it yet!

We will be providing more guidance on solutions for internationalized routing inside app. With the addition of generateStaticParams, which can be used inside layouts, this will provide improved flexibility for handling routing when paired with Middleware versus the existing i18n routing features in pages/.

aej11a commented 2 years ago

Hey @leerob ! Is there any chance you or the team could briefly describe what that solution will look like, even if the example isn't implemented yet?

The "official word" on i18n is the biggest open question I've found holding back my team from starting incremental adoption of /app - need localization but also want to make sure we don't invest too much in the wrong approach :)

Thanks for everything!

Edit: really glad the new approach h will allow more flexible i18n, I've had a few clients who need more flexible URL structures than the default /pages system allows

timneutkens commented 2 years ago

@aej11a the example will have pretty much the same features but leverages other features to achieve the intended result so that you get much more flexibility, the most common feedback around the i18n feature has been "I want to do x in a different way" so it made more sense to leave it out of the built-in features and instead provide an example to achieve the same.

I can't share the full example yet because there's a few changes needed in order to mirror the incoming locale matching.

The short of it is:

// app/[lang]/layout.ts
export function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'nl' }, { lang: 'de' }]
}

export default function Layout({children, params}) {
  return <html lang={params.lang}>
    <head></head>
    <body>{children}</body>
  </html>
}
// app/[lang]/page.ts
export default function Page({params}) {
  return <h1>Hello from {params.lang}</h1>
}
// app/[lang]/blog/[slug]/page.js
export function generateStaticParams({params}) {
  // generateStaticParams below another generateStaticParams is called once for each value above it
  const lang = params.lang
  const postSlugsByLanguage = getPostSlugsByLanguage(lang)
  return postsByLanguage.map(slug => {
    // Both params have to be returned.
    return {
      lang,
      slug
    }
  })
}

export default function BlogPage({params}) {
  return <h1>Dashboard in Language: {params.lang}</h1>
}
oezi commented 2 years ago

@timneutkens thanks a lot for your example. one thing missing is the actual code for "Reading lang in page/layout:", as it's showing the axact same code block as for the point before ("Root layout with generateStaticParams:"). Most likely just an error when copying your code to this post, but could be confusing for people reading this - maybe you can fix that.

timneutkens commented 2 years ago

@oezi good catch! I copied the wrong block from my notes indeed, fixed!

aej11a commented 2 years ago

Thanks @timneutkens ! This is super helpful

eric-burel commented 2 years ago

Hey folks, I just want to point out that I've spent some time during the last years to formalize a pattern similar to what @timneutkens describes, I name it Segmented Rendering (link points to an article of mine describing it a bit more).

The interesting thing with this pattern is that it extends to many use cases, whenever multiple users should have the same rendered content. That's the case for i18n, all users of the same language should see the same page. In this scenario, you should avoid "headers()", "cookies()", and prefer the middleware+static params combo.

Since it happens server-side, in theory you can also prerender secure content like paid content, as long as you are able to verify the access rights within an edge middleware (easier said than done though).

When using an URL rewrite specifically, the URL parameter doesn't even have to be visible to the end user, for those who don't like the "/fr" in the URL, as if it was a server-side "internal" redirection.

I am a bit bothered by @aldabil21 comment about Error: Dynamic server usage: ..., when using this pattern to get a static layout, you might still want dynamic values for a nested page. For instance, you load the language "statically", but you want to render a block that is user specific. Will it be possible?

One issue I hit is that the root [lang] param seems to hit also public files, so sometimes its value is "favicon.ico" for instance.

robin-gustafsson commented 2 years ago

I've somewhat reached an initial solution, I managed to use i18next with fs-loader, and could keep an instance of i18next in the server. Not sure how good/bad this solution be, anyone can give an input to this repo would appreciate it https://github.com/aldabil21/next13-i18n

What I've learned on the way:

  1. Trying to use generateStaticParams as mentioned here 41745# will not work at all, adding generateStaticParams in the layout will prevent you from using any "next/navigation" or "next/headers" in any nested routes, a vague error message will show: Error: Dynamic server usage: .... Maybe Next team will make some changes about it in future?
  2. There is no way to know the current path on the server. known the current path only available in client-component hooks such as "usePathname".
  3. This solution is server-component only, once you use "use client" it will not work, cuz of the fs-backend for i18next.

Pretty cool. I've tried your solution for Storyblok with field level translation nextjs13-i18n-storyblok and it's working pretty well until you want to use client. Looking forward to find a way to use server components with i18n 👀

KurtGokhan commented 2 years ago

Pretty cool. I've tried your solution for Storyblok with field level translation nextjs13-i18n-storyblok and it's working pretty well until you want to use client. Looking forward to find a way to use server components with i18n 👀

You can load all needed namespaces as described here and pass it to a I18NextProvider marked with 'use client'. You need to create the client i18n instance in another file. For example:

// provider.tsx
'use client';

import { ReactNode, useMemo } from 'react';
import { I18nextProvider } from 'react-i18next';
import { initializeClientI18n } from 'src/i18n/client';

export interface Props {
  language: string;
  namespaces: any;
  children: ReactNode;
}

export function I18nClientProvider({ children, language, namespaces }: Props) {
  const instance = useMemo(() => initializeClientI18n(language, namespaces), [language, namespaces]);

  return <I18nextProvider i18n={instance}>{children}</I18nextProvider>;
}
// src/i18n/client.ts

import i18n from 'i18next';

export function initializeClientI18n(language: string, namespaces: any) {
  i18n.init({
    fallbackLng: language,
    supportedLngs: ['tr', 'en'],
    fallbackNS: 'common',
    ns: ['common'],
    initImmediate: false,
    resources: {
      [language]: namespaces,
    },
  });

  return i18n;
}

One problem is that now we have to actively think about whether to call i18next instance directly (server side), or use useTranslation hook (client side). react now exports something called createServerContext so theoretically it should be possible to use useTranslation hook everywhere if react-i18next implements it I guess.

eric-burel commented 2 years ago

@KurtGokhan createServerContext seems undocumented, link I've found: https://stackoverflow.com/a/74311552/5513532 I've tried to check how Next.js internally implement headers and cookies (because this is technically request-level context), it's using async_hooks from Node it seems, but it's not yet clear to me.

leerob commented 2 years ago

Hey y'all – we've built an example here: https://app-dir-i18n.vercel.app/en

Open to any feedback!

danilobuerger commented 2 years ago

@leerob nice, how do you localize a route? e.g. https://app-dir-i18n.vercel.app/en/hello vs https://app-dir-i18n.vercel.app/de/hallo (hello vs hallo)

iljamulders commented 2 years ago

@leerob Awesome! Can't wait to try it out!

Could you give us an example on how we can create a default locale without a prefix? Example: https://ahrefs.com/backlink-checker https://ahrefs.com/de/backlink-checker https://ahrefs.com/es/backlink-checker

Could we create a rewrite, keeping in mind to not break our existing pages (that are localized) while we incrementally adopt from our existing pages/ directory?

max-ch9i commented 2 years ago

@leerob @iljamulders The only option I see is to use a catch-all segment [[...path]]. In the component, the first segment from params.slug is matched against the list of locales. If the first segment is not a valid locale, treat the segment as part of the pathname for the default locale.

davidhellmann commented 2 years ago

@maximus8891 yes did it this way with astro. Bit not sure if I like the approach. Feels a bit dirty 😅

reiv commented 2 years ago

@leerob A few thoughts:

eric-burel commented 2 years ago

@leerob for spreading good practices, you may want to add a line showing sanitization of the lang param. Last time I've checked (Next 13.0.4 I think?) for instance static files that are not found in public folder will fallback to checking existing routes in app folder, leading to a language that can be "favicon.ico" literally (to reproduce remove favicon.ico from your demo folder, unless the bug as been fixed since). So only valid languages should be accepted.

@danilobuerger this localization of route question is coming a lot in Next.js Discord. The solution is to play around with middlewares and redirection: 0) Say you have "[lang]/hello/page.tsx" 1) user hits "/de/hallo" directly => rewrite URL to "/de/hello" so Next finds "[lang]/hello/page.tsx" (user won't see the rewrite) 2) use hits "/hello" => there are multiple possibilities, you can redirect to "/de/hello" (as shown in the example) and then client-side change the displayed URL manually OR you can redirect to "/de/hallo" which in turns will rewrite to "/de/hello" via step 1)

But an official example could be interesting

fernandojbf commented 2 years ago

I'm testing the app folder features in an existent app and it seems that the locale is messing the routing.

using i18n in pages folder and [lang]/test in app folder:

http://localhost:3000/pt/test -> 404 not found http://localhost:3000/pt/pt/test -> lang = pt http://localhost:3000/not_a_next_locale/test -> lang = not_a_next_locale

It seems that the configuration from i18n of next is messing with the app folder routing. If the i18n feature of pages folder mess the new app structure will be really dificult to gradualy migrate to it

eric-burel commented 2 years ago

This branch is showing our setup at Devographics (work in progress and very experimental) for the State of JS/CSS/GraphQL surveys:

https://github.com/Devographics/Monorepo/tree/feature/next-13-react-18/surveyform

nbouvrette commented 2 years ago

@leerob few comments on your example:

sinar93 commented 1 year ago

@leerob Thanks for the example.

goncy commented 1 year ago

@leerob Thanks for the example.

  • Could you please suggest the best practice to set a default locale in order to remove the prefix for it: /test instead of /en/test/ ?
  • Is there a way to set dir on the html of the root layout? (for switching between RTL and LTR) [in the example [lang] is inside of root layout]
  • Is it a bad practice to break down dictionaries into smaller files and import every file in its place instead of one big file?

Hey @sinar93 ! You can use middleware to rewrite to the correct path, ie (given the simple example above):

import { NextResponse } from 'next/server'

const LOCALES = ['en', 'es', 'de']

export const config = {
  matcher: ['/', '/:lang'],
}

export default function middleware(req) {
  const locale = '...' // Determine the locale you want to show

  // If the url doesn't start with a locale, append one to it
  if (!LOCALES.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`))) {
    req.nextUrl.pathname = `/${locale}` + req.nextUrl.pathname
  }

  // Rewrite to the correct path
  return NextResponse.rewrite(req.nextUrl)
}

For nested paths you only have to tweak the middleware to work with multiple segments.

For adding dir I would add a new param, like /[dir]/[lang]/ and move the root layout inside of it (as @reiv said) and then add it to the html tag, ie:

export default async function RootLayout({ children, params: { lang, dir } }) {
  return (
    <html lang={lang} dir={dir}>

Regarding translations practices, its not a bad practice to split it. In my experience, large applications and services usually divide files in namespaces. Whatever works for you (while keeping performance in mind) will be ok.

sinar93 commented 1 year ago

@goncy Many thanks, This is very helpful for me. 🌷

eric-burel commented 1 year ago

@leerob as asked by @aldabil21 earlier, it's not clear whether we can use SSR with this approach. For instance say I have /fr-FR/home which is static and /fr-FR/account/[userId] which I want to be SSRed.

I will hit this error:

error - Error: A required parameter (userId) was not provided as a string in generateStaticParams for /[lang]/account/[userId]

Unless I add this in "app/[lang]/account/[userId]/layout.tsx" (or "page.tsx"):

export async function generateStaticParams() {
  return [];
}

Which is however still static rendering and not dynamic rendering, this solution works but will consume a lot of memory for nothing if I have 1 million users.

The alternative is to remove generateStaticParams() from the root layout, but then /fr-FR/home will be dynamic which is not efficient either.

Using export const dynamic = "force-dynamic" in the "[userId]" page doesn't seem to solve the issue.

adrai commented 1 year ago

fyi: seems next-i18next is not necessary anymore with the new app dir setup...

I've started a little example showing how this could work directly by using i18next and react-i18next on server side and on client side: https://github.com/i18next/next-13-app-dir-i18next-example

...and here the corresponding blog post: https://locize.com/blog/next-13-app-dir-i18n/

feel free to check that out

filipesmedeiros commented 1 year ago

@adrai although that does seem to work (I have used it myself), this does not seem like it would be supported by the official docs.

As stated here https://beta.nextjs.org/docs/routing/pages-and-layouts:

The top-most layout is called the Root Layout. This is a required layout that is shared across all pages in an application. It must contain the html and body tags.

So the root layout (above [lang]) is mandatory.

I don't know if the fact that it works is good or not 😅

Also note that (in my view) the only real problem with this is that we need access to the html tag to change the lang attribute. If it wasn't for that, we wouldn't need to have the root layout inside the route, right?

EDIT: CC @leerob. Also, sometimes I see Next tries to automagically creates the root page and root layout for us? We wouldn't want that, in this case. I think it's only if you have a page.tsx at the root though

KurtGokhan commented 1 year ago

So the root layout (above [lang]) is mandatory.

According to this, the app can have multiple root layouts (although it is talking about Route Groups). I think this works with the same logic. For any path, the top-most layout is the root layout. It does not have to be directly under app dir, unless there is a page there. (I am making a guess and this needs to be confirmed)

adrai commented 1 year ago

So the root layout (above [lang]) is mandatory.

According to this, the app can have multiple root layouts (although it is talking about Route Groups). I think this works with the same logic. For any path, the top-most layout is the root layout. It does not have to be directly under app dir, unless there is a page there. (I am making a guess and this needs to be confirmed)

Yes, that's why I've moved the layout.js to the [lng] folder 😁

b3nten commented 1 year ago

Thanks for the examples folks, it's been helpful.

However, one issue I see with using dynamic route segments is that Links must add the locale to the href in order to respect a users choice. For example, if I want my users to have the option to pick a language, Link components need to append this choice to the beginning of URLs. I suppose this isn't a big deal with a custom component, but it does mean passing params.lang to each link in server components, and parsing the URL in client components.

Another option I can see is using cookies instead of a dynamic [lang] root. Middleware would redirect "/es/my-page" to "/my-page" and append a cookie with "es". The root server component can read this cookie and then follow the example from there.

Is there a reason why this approach was not chosen for the example? It seems cleaner and more flexible, but perhaps there is a downside or issue I'm not seeing.

Edit: One downside would be translations not working for users who disable cookies. I suppose you could use both, keep the dynamic lang segment but have an optional cookie override everything.

KurtGokhan commented 1 year ago

Another option I can see is using cookies instead of a dynamic [lang] root. Middleware would redirect "/es/my-page" to "/my-page" and append a cookie with "es". The root server component can read this cookie and then follow the example from there.

SEO takes a hit if two languages use the same URL. If you meant rewrite instead of redirect, that could work better. But I would not prefer it because I can't predict what could go wrong.

For links, I have a wrapper for Link that gets the locale from current path (retrieved with usePathname) and prepend that to paths without any locale. The wrapper must be a client component, but I don't have a problem with that. Better than manually passing down the locale everywhere (This could improve with server contexts).

b3nten commented 1 year ago

Another option I can see is using cookies instead of a dynamic [lang] root. Middleware would redirect "/es/my-page" to "/my-page" and append a cookie with "es". The root server component can read this cookie and then follow the example from there.

SEO takes a hit if two languages use the same URL. If you meant rewrite instead of redirect, that could work better. But I would not prefer it because I can't predict what could go wrong.

For links, I have a wrapper for Link that gets the locale from current path (retrieved with usePathname) and prepend that to paths without any locale. The wrapper must be a client component, but I don't have a problem with that. Better than manually passing down the locale everywhere (This could improve with server contexts).

Good point about the SEO, a rewrite would be fine. I'm curious what makes it impossible to predict what could go wrong?

KurtGokhan commented 1 year ago

Good point about the SEO, a rewrite would be fine. I'm curious what makes it impossible to predict what could go wrong?

I didn't mean to say it is impossible to predict. I meant I am incapable of it. But my instincts say it is bad😄

There may be some edge cases. For example, if you change the language in another tab, then change the route with a soft reload, you could end up in a situation where there are translations from multiple languages in the same page.

There may be problems with SEO too. Let's say, you are in /es/page1. The links in this page will be like /page2 and /page3, right? If crawler doesn't keep cookies, they would fail to render /page2 in es locale. Also need to think of "Open in new tab" behavior (rewrite or redirect?).

b3nten commented 1 year ago

Yeah, good thinking. Looks like it's crucial to maintain the locale in the URL. However it should be fine to leave it out if it's the default, correct? As middleware would "correct" the mistake.