amannn / next-intl

🌐 Internationalization (i18n) for Next.js
https://next-intl-docs.vercel.app
MIT License
2.54k stars 232 forks source link

Opt out of using cookies #454

Closed Kinbaum closed 2 weeks ago

Kinbaum commented 1 year ago

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

Similar to #366, but I would like to have the option to disable the use of cookies for the locale.

Describe the solution you'd like

Adding an option to createMiddleware in next-intl to disable the use of cookies for locale and derive the value only from the Accept-Language header or URL itself could have several benefits:

  1. Enhanced Privacy: Some users or locales may have stringent privacy regulations around the use of cookies and may prefer or require a cookie-less solution. This feature will respect those privacy boundaries.
  2. Flexibility: It gives developers greater control over how locales are determined and how user data is handled. Some applications may prefer to base localization purely on URL structure or Accept-Language headers. This avoids potential discrepancies between cookie locale data and those other factors.
  3. Security: Reducing reliance on cookies can help mitigate certain security risks associated with them, such as Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF).

Describe alternatives you've considered

Attempt to delete the cookie myself in the middleware with each request.

amannn commented 1 year ago

Thanks for raising this!

As you've correctly mentioned, this can currently be implemented by composing the middleware:

import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';

export default async function middleware(request: NextRequest) {
  const handleI18nRouting = createIntlMiddleware({
    locales: ['en', 'de'],
    defaultLocale: 'en'
  });
  const response = handleI18nRouting(request);

  if (response.cookies.get('NEXT_LOCALE')) {
    response.cookies.delete('NEXT_LOCALE');
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next|.*\\..*).*)']
};

In case there's a growing demand for this, I think we could consider built-in support. I haven't yet thought out all implications this might have though (e.g. we need additional guardrails when someone tries to use this together with localePrefix: 'never').

fernandojbf commented 1 year ago

the delete next locale will continue to send the cookie empty header. Would be nice to have the opt out feature.

image

amannn commented 11 months ago

Setting the NEXT_LOCALE cookie on the response will now be disabled if localeDetection: false is set as of next-intl@3.1.3 (see https://github.com/amannn/next-intl/pull/654).

hwhmeikle commented 11 months ago

@amannn I'm using this workaround from your earlier comment:

if (response.cookies.get('NEXT_LOCALE')) {
    response.cookies.delete('NEXT_LOCALE');
  }

This full example seems to work well so far:

import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';

export default async function middleware(request: NextRequest) {
  const handleI18nRouting = createIntlMiddleware({
    locales: ['en', 'de'],
    defaultLocale: 'en',
    localePrefix: 'never'
  });
  const response = handleI18nRouting(request);

  if (response.cookies.get('NEXT_LOCALE')) {
    response.cookies.delete('NEXT_LOCALE');
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next|.*\\..*).*)']
};

I'm also using localePrefix: 'never'. You mention needing additional guard rails when using this option, could you please elaborate on what those are? For context, I just want the accept-language header to determine the correct locale.

sysyamer commented 6 months ago

I needed to disabled the cookie detection as well. You workaround works but it is kind of ugly.
a simple localeCookieDetection: false would be great =)

loudwinston commented 4 months ago

Agreed with @sysyamer here, the change in https://github.com/amannn/next-intl/pull/654 is great if you want to opt out of detection entirely. However it doesn't give an option to continue using locale detection via accept-language header, but not use the NEXT_LOCALE cookie.

The middleware workaround provided works, but feels like a bit of a hack. It would be awesome if there was an option in the config for something like

// Use the browser `accept-language` header to determine locale
localeDetection: true,

// Don't set the `NEXT_LOCALE` cookie
localeCookie: false

@amannn would you be open to a PR of a change like this? I would be willing to submit one if you think this is a potentially useful change.

amannn commented 4 months ago

@loudwinston Can you share what your use case is for disabling the cookie but keeping accept-language?

My main concern currently is that there can be many combinations, so a large question would be the right API:

E.g.:

// Allow for complete customization, but users might have to reimplement parts of the
// `next-intl` middleware if e.g. only the cookie management needs to be adapted
localeDetection(request) {
  return locale;
}

// Isn't quite correct, a prefix or domain will still be taken into account
// (that's arguably already the case with `localeDetection: false`)
localeDetection: 'accept-language-only'

// Allows more customization, but e.g. not a custom cookie name
localeDetection: ['prefix', 'accept-language']

It seems to me like this goes down a rabbit hole, where it's likely that there might still be a use case that's not ideally supported. Currently we take a very high-level approach, where it's basically an all-or-nothing and customization can be added before/after the middleware has run. This gives users the most control, but I do agree it can feel a bit hacky to delete a cookie after it was configured on the response.

Additionally, it seems like there could be changes to the middleware in Next.js. In any case I'd wait a bit to see what changes are coming here before we change things too much.

So for now I'd be happy to hear more about use cases, so please let me know!

damian-balas commented 3 months ago

@loudwinston Can you share what your use case is for disabling the cookie but keeping accept-language?

My main concern currently is that there can be many combinations, so a large question would be the right API:

E.g.:

// Allow for complete customization, but users might have to reimplement parts of the
// `next-intl` middleware if e.g. only the cookie management needs to be adapted
localeDetection(request) {
  return locale;
}

// Isn't quite correct, a prefix or domain will still be taken into account
// (that's arguably already the case with `localeDetection: false`)
localeDetection: 'accept-language-only'

// Allows more customization, but e.g. not a custom cookie name
localeDetection: ['prefix', 'accept-language']

It seems to me like this goes down a rabbit hole, where it's likely that there might still be a use case that's not ideally supported. Currently we take a very high-level approach, where it's basically an all-or-nothing and customization can be added before/after the middleware has run. This gives users the most control, but I do agree it can feel a bit hacky to delete a cookie after it was configured on the response.

Additionally, it seems like there could be changes to the middleware in Next.js. In any case I'd wait a bit to see what changes are coming here before we change things too much.

So for now I'd be happy to hear more about use cases, so please let me know!

@amannn I guess it's the same issue I have. This is how I would imagine the lang detection algorithm to work:

  1. if the path contains a locale prefix eg. /en/some-website /de/some-website /pl/some-website then it should prioritize the language that is inside the pathname.

  2. if the user didn't allow cookies yet and the url doesn't have a lang prefix eg. / /some-website then we should look at the 'Accept-Language' header.

  3. if the user allows for cookies, we first check the pathname, then if there was a cookie with the preferred language and last we look at the 'Accept-Language' header. Checking if user allows cookies or not should be on our side.

it's all about the compliance with GDPR.

amannn commented 3 months ago

it's all about the compliance with GDPR.

@damian-balas As far as I'm informed, a cookie that remembers the language preference of the user for a multilingual site falls into the category of "functionality cookies", which do not require explicit consent.

E.g. imagine this workflow:

  1. A user visits / and is redirected based on the accept-language header to /en
  2. The user wants to change the locale (e.g. because the accept-language header was off due to using a friends computer) and therefore switches to /es
  3. The user opens a second instance of the website (e.g. to compare products) and is redirected from / to /en again

I'd argue this may appear as a broken website, the user's expectation would be that the language preference is remembered.

Cookies that are necessary for a website to function correctly don't need to be asked for opt-in with a cookie banner but should be covered in the privacy policy of your website.

I haven't confirmed this with an expert though, so take this with a grain of salt. Would be happy to discuss if someone has a different opinion!

damian-balas commented 3 months ago

it's all about the compliance with GDPR.

@damian-balas As far as I'm informed, a cookie that remembers the language preference of the user for a multilingual site falls into the category of "functionality cookies", which do not require explicit consent.

E.g. imagine this workflow:

  1. A user visits / and is redirected based on the accept-language header to /en
  2. The user wants to change the locale (e.g. because the accept-language header was off due to using a friends computer) and therefore switches to /es
  3. The user opens a second instance of the website (e.g. to compare products) and is redirected from / to /en again

I'd argue this may appear as a broken website, the user's expectation would be that the language preference is remembered.

Cookies that are necessary for a website to function correctly don't need to be asked for opt-in with a cookie banner but should be covered in the privacy policy of your website.

I haven't confirmed this with an expert though, so take this with a grain of salt. Would be happy to discuss if someone has a different opinion!

image

I don't this a language preference cookie is necessary for a multi lang website. The website would still work without it. It's more a convenience than a requirement. Check out what Cookiebot has to say about this.

amannn commented 3 months ago

Hmm, on a second thought, you might be right. I've asked the StackExchange Webmasters community if someone is familiar with the topic and can provide clarification: Does saving the language preference of the user in a cookie require a cookie banner?

If yes, I agree and we should rather sooner than later support this use case better with the next-intl middleware.

I'll reopen this issue for the time being.

amannn commented 3 months ago

@damian-balas I think I came to a conclusion here.

The Article 29 Working Party opinion 04/2012 provides clear guidelines for this use case (see section 3.6 "UI customization cookies"):

  1. Remembering a language choice in a cookie is ok, as long as an explicit action lead to a language change
  2. The expiration must be "short term" (no longer than a few hours after a browsing session has ended)
  3. An additional note like "uses cookies" can be placed next to a language switcher in order to use a longer expiration period

In contrast, what next-intl currently does is:

  1. A NEXT_LOCALE cookie is always set, even if the negotiated locale matches the accept-language header (the only way to opt-out is localeDetection: false)
  2. The expiration is set to a year (this is not customizable)

I'd really like next-intl to be GDPR-compliant out-of-the-box and clearly state this, so that there's no detective work necessary on the end user side.

An action plan could be:

  1. Only set NEXT_LOCALE when the user requests a localized version that doesn't match the accept-language header (typically when a locale switcher is used).
  2. Reduce the expiration to a few hours

An edge case could be if someone receives a link that would open a site with a locale prefix that doesn't match the accept-language header of the user. Is that an explicit locale change? Not sure. An alternative would be to avoid setting the cookie in general and let the user handle this.

There's also the use case of localePrefix: 'never' with multiple locales being used on the same host. Here, the cookie is really vital and should have a maximum expiration (and might require consent).

I'll add this topic to the list in https://github.com/amannn/next-intl/issues/779 as this will require a breaking change in any case.

As a temporary workaround to modify the cookie, you can use something like this in the middleware:

import {NextRequest} from 'next/server';
import createMiddleware from 'next-intl/middleware';

const handleI18nRouting = createMiddleware(/* ... */);

export default function middleware(request: NextRequest) {
  const response = handleI18nRouting(request);

  if (response.cookies.get('NEXT_LOCALE')) {
    response.cookies.set(
      'NEXT_LOCALE',
      response.cookies.get('NEXT_LOCALE').value,
      {
        // Change cookie options
      }
    );
  }

  return response;
}
loudwinston commented 3 months ago

@amannn Apologies for the late reply here, I've been quite busy getting a new application released (using next-intl successfully!) and haven't had time to respond.

I think my use case similar to other folks in this thread

  1. Anytime we use cookies, we have to consult with our legal department to ensure GDPR compliance. Would prefer to avoid that if not needed.
  2. We want to be able to change the language by adjusting the URL.
  3. During testing, we want folks to be able to change their browser language to test language detection logic, without needing to clear cookies each time.

We don't currently allow users to choose a different language than what we detect from their browser settings. Understood that this is best solved by the use of cookies - when we introduce this feature we'll be ensuring GDPR compliance.

amannn commented 4 weeks ago

Update: A new localeCookie option for the middleware has been added in https://github.com/amannn/next-intl/pull/1414 that should help with the use cases discussed in this thread. The feature is available in the latest canary release.

A bit further down the road, as part of the next major release, I'm planning to decrease the cookie expiration in relation to https://github.com/amannn/next-intl/issues/454#issuecomment-2238885859.

Edit: I noticed there's a bug in the current implementation of the localeCookie feature, need to have another look in https://github.com/amannn/next-intl/pull/1417.

amannn commented 3 weeks ago

Re https://github.com/amannn/next-intl/issues/454#issuecomment-2405449355: Ok, a new canary is out that addresses the bug I've noticed. A small API change was necessary: the localeCookie option was moved from the second argument of the middleware to defineRouting (or just the first parameter of the middleware). However, the previous API is still supported, but now deprecated.

The proposed docs have been updated accordingly.