Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to not have the locale anywhere in the URL #366

Closed
boris-arkenaar opened this issue Jun 29, 2023 · 13 comments · Fixed by #388
Closed

Option to not have the locale anywhere in the URL #366

boris-arkenaar opened this issue Jun 29, 2023 · 13 comments · Fixed by #388
Labels
enhancement New feature or request

Comments

@boris-arkenaar
Copy link
Contributor

boris-arkenaar commented Jun 29, 2023

Is your feature request related to a problem? Please describe.

As I understand the only two options next-intl currently provides is having the locale either as a prefix of the path, or as a subdomain. I'd like to use this library for a large application with (almost) everything behind authentication. The end user will have the option to choose their preferred language to use the application in, but I don't like and don't need the locale to show up in the URL.

Describe the solution you'd like

I would like to:

  • not have to put my pages inside a [locale] directory
  • have the middleware not determine the locale based on the current URL and/or not redirect/rewrite based on the locale.
  • be able to retrieve the current locale in my app/layout.tsx, so I can pass it on to NextIntlClientProvider, or maybe NextIntlClientProvider can retrieve the locale internally.

Describe alternatives you've considered

I've actually written my own middleware. Copy and pasting the bits of code from this library that I needed, leaving out the parts that didn't suit my use case. At the end, I both save the locale in a cookie and in a custom response header.
In the app/layout.tsx I retrieve the current locale from the custom response header (couldn't make it work to read the cookie), and passed the value on to the NextIntlClientProvider.

I am using this library in a Next13 app with the app router. So far I've only got it set up for client components. I have looked at the setup needed for server components, but couldn't figure out how to avoid having the locale in the URL.

So, I can use your library now, with my middleware setup myself. I would love to see this option appear and be able to set things up more properly and start to make use of server components as well.

Thanks for this beautiful library!

@boris-arkenaar boris-arkenaar added enhancement New feature or request unconfirmed Needs triage. labels Jun 29, 2023
@boris-arkenaar boris-arkenaar changed the title Option to **not** have the locale anywhere in the URL Option to not have the locale anywhere in the URL Jun 29, 2023
@amannn
Copy link
Owner

amannn commented Jun 29, 2023

Thank you for the feature proposal!

I do think for internal apps where SEO is not a concern, there's an opportunity for a localePrefix: 'never' config option for the middleware. It would always use the cookie value to detect the locale and then use a rewrite for every locale. If no cookie is present (yet), the locale can be negotiated based on the accept-language header.

Moving routes into a [locale] folder is still required though, but the end user would never notice it.

Are you interested in contributing this feature?

The relevant code is here:

Would be happy to review a PR!

@amannn amannn added good first issue and removed unconfirmed Needs triage. labels Jun 29, 2023
@boris-arkenaar
Copy link
Contributor Author

Thanks for the quick response @amannn

localePrefix: 'never' sounds good. Still making use of cookie and accept-language is what I would do as well.

Why is it still necessary to have the routes in a [locale] directory (and do a redirect based on locale)?

Let me see if I can indeed contribute this feature. Thanks for pointing to the relevant code.

@raphaelbadia
Copy link

raphaelbadia commented Jul 4, 2023

@boris-arkenaar would you mind sharing your custom middleware?

I'm encountering the same issues:

  • I don't want to have a [locale] directory
  • because I don't want the middleware to do all these redirects/rewrites

Currently, I'm using another library for a full-authenticated app with a lot of middleware conditions (for authorization, old browsers detections and other stuff) and to make i18n work I simply have to add a language cookie based of Accept-Language.

The current middleware implementation is difficult to extend.

In my case in my middleware I'd like to:

  1. run a custom middleware related to my business that redirects to somewhere else under certain conditions
  2. set the language for the app
  3. check for authorization, rewriting to a forbidden page if necessary

But the example with createIntlMiddleware() supposes that createIntlMiddleware() is the last middleware in the chain

Having the [locale] folder mandatory is worrying me, is that required to have it working with RSC?

@raphaelbadia
Copy link

Soooo actually it's super easy, I just set a cookie NEXT_LOCALE=en for instance and it works 🥰

@amannn
Copy link
Owner

amannn commented Jul 5, 2023

Why is it still necessary to have the routes in a [locale] directory (and do a redirect based on locale)?

Having the locale available as a segment in the URL enables static rendering of different language versions of your app (works in the latest stable version for Client Components and is on the roadmap for the RSC beta). Furthermore, the locale that is received in layouts and pages can also be used for the Metadata & Route Handler API.

I understand that it might feel unnecessary to require the [locale] folder if you intend to never have it be part of the routing. I think localePrefix: 'never' would be really useful though because then it's just a matter of flipping a config option to choose between different routing patterns and static rendering is supported everywhere.

Soooo actually it's super easy, I just set a cookie NEXT_LOCALE=en for instance and it works 🥰

That's true, this workaround will work currently, because next-intl first checks the presence of a cookie in components. There might be some updates to this part of the library in the future when support for static rendering is added though (there are some details on this here: #149).

There will likely be an upgrade path if the cookie solution works for you currently, but localePrefix: 'never' would be the cleanest option IMO, without having to rely on internals.

Hope this helps!

Let me see if I can indeed contribute this feature. Thanks for pointing to the relevant code.

Fantastic, let me know if I can help with something! I've just migrated the test runner to vitest this week, should be more fun to work with tests now!

@boris-arkenaar
Copy link
Contributor Author

boris-arkenaar commented Jul 5, 2023

@raphaelbadia

@boris-arkenaar would you mind sharing your custom middleware?

Most of it is copy-paste from this library.

middleware.ts

import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
import { NextResponse } from 'next/server';
import Negotiator from 'negotiator';
import { match } from '@formatjs/intl-localematcher';

import type { NextRequest } from 'next/server';

const COOKIE_LOCALE_NAME = 'NEXT_LOCALE';
const defaultLocale = 'en';
const locales = ['en', 'nl'];

function getAcceptLanguageLocale(
  requestHeaders: Headers,
  locales: Array<string>,
  defaultLocale: string
) {
  let locale;

  const languages = new Negotiator({
    headers: {
      'accept-language': requestHeaders.get('accept-language') || undefined,
    },
  }).languages();
  try {
    locale = match(languages, locales, defaultLocale);
  } catch (e) {
    // Invalid language
  }

  return locale;
}

function resolveLocale(
  locales: Array<string>,
  defaultLocale: string,
  requestHeaders: Headers,
  requestCookies: RequestCookies
) {
  let locale;

  // Prio 1: Use existing cookie
  if (requestCookies) {
    if (requestCookies.has(COOKIE_LOCALE_NAME)) {
      const value = requestCookies.get(COOKIE_LOCALE_NAME)?.value;
      if (value && locales.includes(value)) {
        locale = value;
      }
    }
  }

  // Prio 2: Use the `accept-language` header
  if (!locale && requestHeaders) {
    locale = getAcceptLanguageLocale(requestHeaders, locales, defaultLocale);
  }

  // Prio 3: Use default locale
  if (!locale) {
    locale = defaultLocale;
  }

  return locale;
}

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();

  const locale = resolveLocale(
    locales,
    defaultLocale,
    req.headers,
    req.cookies
  );

  const hasOutdatedCookie =
    req.cookies.get(COOKIE_LOCALE_NAME)?.value !== locale;

  if (hasOutdatedCookie) {
    res.cookies.set(COOKIE_LOCALE_NAME, locale, {
      sameSite: 'strict',
    });
  }

  res.headers.set('x-my-locale', locale);

  return res;
}

export const config = {
  /*
   * Match all request paths except for the ones starting with:
   * - api (API routes)
   * - _next/static (static files)
   * - _next/image (image optimization files)
   * - favicon.ico (favicon file)
   */
  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
};

layout.tsx

import { NextIntlClientProvider } from 'next-intl';
import { headers } from 'next/headers';
import { notFound } from 'next/navigation';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const locale = headers().get('x-my-locale') || 'en';
  let messages;
  try {
    messages = (await import(`../messages/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

@kylemorena
Copy link

kylemorena commented Jul 12, 2023

@amannn Hello,
I'm having this issue, how can I handle errors and not-found pages, in order to use notFound function?
All the locale contexts are missing, so I have a not-found page localized without a anguage.

Thanks in advance!

@amannn
Copy link
Owner

amannn commented Jul 12, 2023

@kylemorena I'd kindly ask you to not hijack issues for usage questions. This question was asked here before, hope this helps: #329. I might include docs for this in the future, as the question has come up a few times before (but is ultimately more related to Next.js itself than next-intl).

@kylemorena
Copy link

@kylemorena I'd kindly ask you to not hijack issues for usage questions. This question was asked here before, hope this helps: #329. I might include docs for this in the future, as the question has come up a few times before (but is ultimately more related to Next.js itself than next-intl).

@amannn Thank you for your prompt response.

amannn added a commit that referenced this issue Jul 18, 2023
@V1RE
Copy link

V1RE commented Jul 19, 2023

Thank you @amannn and @boris-arkenaar for the amazing work! Would love if this option was available in #149! Can I help in any way?

@amannn
Copy link
Owner

amannn commented Jul 19, 2023

@V1RE I'll rebase the RSC branch and publish a new beta—should be ready in a bit!

@amannn
Copy link
Owner

amannn commented Jul 19, 2023

[email protected] is out!

@amannn
Copy link
Owner

amannn commented May 22, 2024

[email protected] brings enhanced support for this use case: announcement.

Maybe this can be helpful to some of you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants