Skip to content

Commit

Permalink
feat(v2): improved middleware (#372)
Browse files Browse the repository at this point in the history
  • Loading branch information
QuiiBz authored Mar 3, 2024
1 parent 63618f1 commit 1c69c87
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 151 deletions.
9 changes: 7 additions & 2 deletions examples/app/app/[locale]/[...notFound]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { notFound } from 'next/navigation';
import React from 'react';

export default function NotFound() {
notFound();
return (
<div>
<h2>Custom Not Found page</h2>
<p>Could not find requested resource</p>
</div>
);
}
8 changes: 7 additions & 1 deletion examples/app/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@ export default function RootLayout({
children: React.ReactNode;
params: { locale: string };
}>) {
return <I18nProvider locale={locale}>{children}</I18nProvider>;
return (
<html lang={locale}>
<body>
<I18nProvider locale={locale}>{children}</I18nProvider>
</body>
</html>
);
}
13 changes: 0 additions & 13 deletions examples/app/app/layout.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions examples/app/app/not-found.tsx

This file was deleted.

16 changes: 12 additions & 4 deletions examples/app/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { NextResponse } from 'next/server';
import { createI18nMiddleware } from 'next-international/middleware';

export const middleware = createI18nMiddleware(request => {
return NextResponse.next();
});
export const middleware = createI18nMiddleware(
request => {
console.log('User middleware:', request.url);
return NextResponse.next();
},
{
locales: ['en', 'fr'],
defaultLocale: 'en',
// urlMappingStrategy: 'rewrite',
},
);

export const config = {
matcher: ['/', '/:locale'],
matcher: ['/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)'],
};
5 changes: 4 additions & 1 deletion packages/next-international/src/app/app/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ export function createI18n<Locales extends LocalesObject, Locale extends LocaleT
const router = useRouter();

return locale => {
const finalLocale = locale as string;
document.cookie = `locale=${finalLocale};`;

// TODO: preserve URL & search params
router.push(`/${locale as string}`);
router.push(`/${finalLocale}`);
};
};

Expand Down
17 changes: 16 additions & 1 deletion packages/next-international/src/app/app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,22 @@ const useLocaleCache = cache(() => {
throw new Error('Invariant: urlPathname is not a string: ' + JSON.stringify(store, null, 2));
}

return url.split('/')[1].split('?')[0];
let locale = url.split('/')[1].split('?')[0];

if (locale === '') {
const cookie = (store?.incrementalCache?.requestHeaders?.['cookie'] as string)
?.split(';')
.find(c => c.trim().startsWith('locale='))
?.split('=')[1];

if (!cookie) {
throw new Error('Invariant: locale cookie not found');
}

locale = cookie;
}

return locale;
});

export function createI18n<Locales extends LocalesObject, Locale extends LocaleType = GetLocale<Locales>>(
Expand Down
204 changes: 85 additions & 119 deletions packages/next-international/src/app/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,88 @@
// import type { NextRequest } from 'next/server';
// import { NextResponse } from 'next/server';
//
// import { LOCALE_COOKIE, LOCALE_HEADER } from '../../common/constants';
// import { warn } from '../../helpers/log';
// import type { I18nMiddlewareConfig } from '../../types';
//
// const DEFAULT_STRATEGY: NonNullable<I18nMiddlewareConfig<[]>['urlMappingStrategy']> = 'redirect';
//
// export function createI18nMiddleware<const Locales extends readonly string[]>(config: I18nMiddlewareConfig<Locales>) {
// return function I18nMiddleware(request: NextRequest) {
// const locale = localeFromRequest(config.locales, request, config.resolveLocaleFromRequest) ?? config.defaultLocale;
// const nextUrl = request.nextUrl;
//
// // If the locale from the request is not an handled locale, then redirect to the same URL with the default locale
// if (noLocalePrefix(config.locales, nextUrl.pathname)) {
// nextUrl.pathname = `/${locale}${nextUrl.pathname}`;
//
// const strategy = config.urlMappingStrategy ?? DEFAULT_STRATEGY;
// if (strategy === 'rewrite' || (strategy === 'rewriteDefault' && locale === config.defaultLocale)) {
// const response = NextResponse.rewrite(nextUrl);
// return addLocaleToResponse(request, response, locale);
// } else {
// if (!['redirect', 'rewriteDefault'].includes(strategy)) {
// warn(`Invalid urlMappingStrategy: ${strategy}. Defaulting to redirect.`);
// }
//
// const response = NextResponse.redirect(nextUrl);
// return addLocaleToResponse(request, response, locale);
// }
// }
//
// let response = NextResponse.next();
// const pathnameLocale = nextUrl.pathname.split('/', 2)?.[1];
//
// if (!pathnameLocale || config.locales.includes(pathnameLocale)) {
// // If the URL mapping strategy is set to 'rewrite' and the locale from the request doesn't match the locale in the pathname,
// // or if the URL mapping strategy is set to 'rewriteDefault' and the locale from the request doesn't match the locale in the pathname
// // or is the same as the default locale, then proceed with the following logic
// if (
// (config.urlMappingStrategy === 'rewrite' && pathnameLocale !== locale) ||
// (config.urlMappingStrategy === 'rewriteDefault' &&
// (pathnameLocale !== locale || pathnameLocale === config.defaultLocale))
// ) {
// // Remove the locale from the pathname
// const pathnameWithoutLocale = nextUrl.pathname.slice(pathnameLocale.length + 1);
//
// // Create a new URL without the locale in the pathname
// const newUrl = new URL(pathnameWithoutLocale || '/', request.url);
//
// // Preserve the original search parameters
// newUrl.search = nextUrl.search;
// response = NextResponse.redirect(newUrl);
// }
//
// return addLocaleToResponse(request, response, pathnameLocale ?? config.defaultLocale);
// }
//
// return response;
// };
// }
//
// /**
// * Retrieve `Next-Locale` header from request
// * and check if it is an handled locale.
// */
// function localeFromRequest<Locales extends readonly string[]>(
// locales: Locales,
// request: NextRequest,
// resolveLocaleFromRequest: NonNullable<
// I18nMiddlewareConfig<Locales>['resolveLocaleFromRequest']
// > = defaultResolveLocaleFromRequest,
// ) {
// const locale = request.cookies.get(LOCALE_COOKIE)?.value ?? resolveLocaleFromRequest(request);
//
// if (!locale || !locales.includes(locale)) {
// return null;
// }
//
// return locale;
// }
//
// /**
// * Default implementation of the `resolveLocaleFromRequest` function for the I18nMiddlewareConfig.
// * This function extracts the locale from the 'Accept-Language' header of the request.
// */
// const defaultResolveLocaleFromRequest: NonNullable<I18nMiddlewareConfig<any>['resolveLocaleFromRequest']> = request => {
// const header = request.headers.get('Accept-Language');
// const locale = header?.split(',', 1)?.[0]?.split('-', 1)?.[0];
// return locale ?? null;
// };
//
// /**
// * Returns `true` if the pathname does not start with an handled locale
// */
// function noLocalePrefix(locales: readonly string[], pathname: string) {
// return locales.every(locale => {
// return !(pathname === `/${locale}` || pathname.startsWith(`/${locale}/`));
// });
// }
//
// /**
// * Add `X-Next-Locale` header and `Next-Locale` cookie to response
// *
// * **NOTE:** The cookie is only set if the locale is different from the one in the cookie
// */
// function addLocaleToResponse(request: NextRequest, response: NextResponse, locale: string) {
// response.headers.set(LOCALE_HEADER, locale);
//
// if (request.cookies.get(LOCALE_COOKIE)?.value !== locale) {
// response.cookies.set(LOCALE_COOKIE, locale, { sameSite: 'strict' });
// }
// return response;
// }

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

export function createI18nMiddleware(middleware: NextMiddleware): NextMiddleware {
return (request, event) => {
import { NextResponse } from 'next/server';
import type { NextMiddleware, NextRequest } from 'next/server';

type I18nMiddlewareConfig<Locales extends readonly string[]> = {
locales: Locales;
defaultLocale: Locales[number];
resolveLocaleFromRequest?: (request: NextRequest) => string | null;
urlMappingStrategy?: 'redirect' | 'rewrite' | 'rewriteDefault';
};

function getLocaleFromRequest<const Locales extends readonly string[]>(
request: NextRequest,
config: I18nMiddlewareConfig<Locales>,
) {
if (config.resolveLocaleFromRequest) {
return config.resolveLocaleFromRequest(request);
}

const locale = request.cookies.get('locale')?.value;

if (!locale || !config.locales.includes(locale)) {
return null;
}

return locale;
}

function noLocalePrefix(locales: readonly string[], pathname: string) {
return locales.every(locale => {
return !(pathname === `/${locale}` || pathname.startsWith(`/${locale}/`));
});
}

export function createI18nMiddleware<const Locales extends readonly string[]>(
middleware: NextMiddleware,
config: I18nMiddlewareConfig<Locales>,
): NextMiddleware {
return async (request, event) => {
let currentLocale = getLocaleFromRequest(request, config);

if (currentLocale === null) {
currentLocale = config.defaultLocale;
const response = await middleware(request, event);

if (response instanceof NextResponse) {
response.cookies.set('locale', currentLocale, { sameSite: 'strict' });
} else if (response instanceof Response) {
const cookies = response.headers.get('set-cookie') ?? '';
response.headers.set('set-cookie', `${cookies}; locale=${currentLocale}; SameSite=Strict`);
}

return response;
}

if (!config.urlMappingStrategy || config.urlMappingStrategy === 'redirect') {
const nextUrl = request.nextUrl;
const pathname = new URL(request.url).pathname;

if (noLocalePrefix(config.locales, pathname)) {
nextUrl.pathname = `/${currentLocale}${pathname}`;
const response = NextResponse.redirect(nextUrl);
return response;
}
}

if (
(config.urlMappingStrategy === 'rewriteDefault' && currentLocale === config.defaultLocale) ||
config.urlMappingStrategy === 'rewrite'
) {
const nextUrl = request.nextUrl;

if (noLocalePrefix(config.locales, nextUrl.pathname)) {
nextUrl.pathname = `/${currentLocale}${nextUrl.pathname}`;
const response = NextResponse.rewrite(nextUrl);
return response;
}

const urlWithoutLocale = nextUrl.pathname.slice(currentLocale.length + 1);
const newUrl = new URL(urlWithoutLocale || '/', request.url);
newUrl.search = nextUrl.search;

const response = NextResponse.redirect(newUrl);
return response;
}

return middleware(request, event);
};
}

0 comments on commit 1c69c87

Please sign in to comment.