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

Using a custom middleware instead of the supplied #609

Closed
cbodin opened this issue Nov 8, 2023 · 3 comments
Closed

Using a custom middleware instead of the supplied #609

cbodin opened this issue Nov 8, 2023 · 3 comments
Labels
enhancement New feature or request has-workaround

Comments

@cbodin
Copy link

cbodin commented Nov 8, 2023

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

The default middleware has some limitations to what can be configured. It would be good to have an official way to skip the supplied middleware and write a custom one.

I'm currently on [email protected] and [email protected] when writing this.

Some of the issues i've discovered with the official middleware:

  • Multi-domain with english on all domains can not use "en" as path prefix. Let's say that we have 2 domains with a total of 4 locales: sv-se, en-se, nb-no and en-no.

    • example.se/* should be sv-se
    • example.se/en/* should be en-se
    • example.no/* should be nb-no
    • example.no/en/* should be en-no.
  • Turning off locale redirects. Using the same example as above:

    • example.se/nb-no/swedish-content should use the sv-se locale and skip redirects. Currently a 301 to example.no/swedish-content will be issued that will result in a 404 after the redirect. This is not optimal as we're telling robots that example.se/nb-no/swedish-content do indeed exist, but then immediately redirect to a 404 page.
    • example.se/sv-se/invalid-path will also issue a 301 to example.se/invalid-path that will result in a 404 (if prefix is not set to always).
  • Impossible to set a default locale based on domain for paths inside _next/ as these are not recommended to be handled by the middleware. This is possible using a custom middleware.

  • There are issues with basePath that the middleware shouldn't have to care for if redirects can be completely disabled.

Describe the solution you'd like

Some official way to set the next-intl locale in a custom middleware. Maybe something like setRequestLocale(request: NextRequest).

If cookies would be set that should be an option as cookies are not always a requirement for the implementation.

Describe alternatives you've considered

I've currently implemented a middleware that works like what i described above. However, the header to set the locale is not exposed so it feels a bit unstable and requires testing for every update.

If anyones interested heres the full middleware (that works with or without basePath set in next.config):

import { NextRequest, NextResponse } from "next/server";

/** The header next-intl uses to determine the current locale. */
const nextIntlLocaleHeaderName = "X-NEXT-INTL-LOCALE";

interface LocaleConfig<T> {
  /** The url prefix to use. */
  prefix: string;
  /** The locale to set based on the prefix. */
  locale: T;
}

interface DomainConfig<T> {
  /** The host of the domain to match. */
  host: string;
  /** Available locales for this domain, excluding the default. */
  locales: LocaleConfig<T>[];
  /** The default locale to use when none of the prefixed locales match. */
  defaultLocale: T;
}

/**
 * Create a middleware to handle i18n rewrites and configure next-intl locale.
 *
 * @param domains The domain configuration. Uses first if none matches.
 */
export function createI18nMiddleware<T extends string>(
  domains: DomainConfig<T>[],
) {
  return function middleware(request: NextRequest) {
    if (!domains.length) {
      throw Error(
        "At least one domain must be specified in the i18n middleware.",
      );
    }

    // Match host against domain configuration
    const host = request.headers.get("host");
    const domain = domains.find((d) => d.host === host) ?? domains[0];

    // Set default domain locale for api and static files
    if (
      /^\/(api\/.+|_next\/.+|icon.png|robots.txt)$/.test(
        request.nextUrl.pathname,
      )
    ) {
      request.headers.set(nextIntlLocaleHeaderName, domain.defaultLocale);
      return NextResponse.next({ request });
    }

    // Check if we can find a locale in the url based on the prefix
    const locale = domain.locales.find(
      (l) =>
        request.nextUrl.pathname === `/${l.prefix}` ||
        request.nextUrl.pathname.startsWith(`/${l.prefix}/`),
    );

    request.headers.set(
      nextIntlLocaleHeaderName,
      locale?.locale ?? domain.defaultLocale,
    );

    const url = request.nextUrl.clone();
    if (locale) {
      // Remove our prefix from the path and add the full locale.
      const path = url.pathname.substring(locale.prefix.length + 1);

      url.pathname = `/${locale.locale}${path}`;
    } else {
      // Add default locale as we have no prefix in the url.
      url.pathname = `/${domain.defaultLocale}${url.pathname}`;
    }

    return NextResponse.rewrite(url, { request });
  };
}

Usage:

import { createI18nMiddleware } from "@/i18n/middleware";
import type { NextRequest } from "next/server";

type Locale = "sv-se" | "en-se" | "nb-no" | "en-no" | "fi-fi" | "sv-fi" | "en-fi";

const i18nMiddleware = createI18nMiddleware<Locale>([
  {
    host: "example.se",
    locales: [{ prefix: "en", locale: "en-se" }],
    defaultLocale: "sv-se",
  },
  {
    host: "example.no",
    locales: [{ prefix: "en", locale: "en-no" }],
    defaultLocale: "nb-no",
  },
  {
    host: "example.fi",
    locales: [{ prefix: "sv", locale: "sv-fi" }, { prefix: "en", locale: "en-fi" }],
    defaultLocale: "fi-fi",
  },
]);

export function middleware(request: NextRequest) {
  return i18nMiddleware(request);
}

This will totally break the next-intl <Link> component. I've created a custom Link component that creates absolute url's including the domain for locales not included in the current domain. But that's a different feature request :)

Many thanks for a great library!

@amannn
Copy link
Owner

amannn commented Nov 22, 2023

Thank you very much for this detailed feature request!

I agree there's a piece missing here currently. I've opened #653 to collect this issue together with similar ones, also to share solutions that can be used today.

If cookies would be set that should be an option as cookies are not always a requirement for the implementation.

That's a good point. I've addressed this in #654.

I've currently implemented a middleware that works like what i described above. However, the header to set the locale is not exposed so it feels a bit unstable and requires testing for every update.

The header will definitely remain there for at least the 3.x version range. If anything changes in regard to it, this will be announced as part of a major version.

@amannn
Copy link
Owner

amannn commented Jan 23, 2024

@cbodin I've updated #653 with a proposal that I think could support your use case. Let me know in the linked issue in case you have feedback or a better idea in mind!

@amannn
Copy link
Owner

amannn commented Mar 28, 2024

I'll close this now in favor of #653. This issue is mentioned there and will be taken care of.

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

No branches or pull requests

2 participants