diff --git a/examples/app/app/[locale]/[...notFound]/page.tsx b/examples/app/app/[locale]/[...notFound]/page.tsx
index 5cc2a45..2c4bcd6 100644
--- a/examples/app/app/[locale]/[...notFound]/page.tsx
+++ b/examples/app/app/[locale]/[...notFound]/page.tsx
@@ -1,5 +1,10 @@
-import { notFound } from 'next/navigation';
+import React from 'react';
export default function NotFound() {
- notFound();
+ return (
+
+
Custom Not Found page
+
Could not find requested resource
+
+ );
}
diff --git a/examples/app/app/[locale]/layout.tsx b/examples/app/app/[locale]/layout.tsx
index 5e20df1..d2bdb57 100644
--- a/examples/app/app/[locale]/layout.tsx
+++ b/examples/app/app/[locale]/layout.tsx
@@ -13,5 +13,11 @@ export default function RootLayout({
children: React.ReactNode;
params: { locale: string };
}>) {
- return {children};
+ return (
+
+
+ {children}
+
+
+ );
}
diff --git a/examples/app/app/layout.tsx b/examples/app/app/layout.tsx
deleted file mode 100644
index 668746b..0000000
--- a/examples/app/app/layout.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
-
- {children}
-
- );
-}
diff --git a/examples/app/app/not-found.tsx b/examples/app/app/not-found.tsx
deleted file mode 100644
index 2c4bcd6..0000000
--- a/examples/app/app/not-found.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-
-export default function NotFound() {
- return (
-
-
Custom Not Found page
-
Could not find requested resource
-
- );
-}
diff --git a/examples/app/middleware.ts b/examples/app/middleware.ts
index ebbf7ed..4be91f1 100644
--- a/examples/app/middleware.ts
+++ b/examples/app/middleware.ts
@@ -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).*)'],
};
diff --git a/packages/next-international/src/app/app/client.ts b/packages/next-international/src/app/app/client.ts
index ff28b8a..c469955 100644
--- a/packages/next-international/src/app/app/client.ts
+++ b/packages/next-international/src/app/app/client.ts
@@ -57,8 +57,11 @@ export function createI18n {
+ const finalLocale = locale as string;
+ document.cookie = `locale=${finalLocale};`;
+
// TODO: preserve URL & search params
- router.push(`/${locale as string}`);
+ router.push(`/${finalLocale}`);
};
};
diff --git a/packages/next-international/src/app/app/server.ts b/packages/next-international/src/app/app/server.ts
index 7bba45c..8ab9895 100644
--- a/packages/next-international/src/app/app/server.ts
+++ b/packages/next-international/src/app/app/server.ts
@@ -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>(
diff --git a/packages/next-international/src/app/middleware/index.ts b/packages/next-international/src/app/middleware/index.ts
index 996ca69..7990032 100644
--- a/packages/next-international/src/app/middleware/index.ts
+++ b/packages/next-international/src/app/middleware/index.ts
@@ -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['urlMappingStrategy']> = 'redirect';
-//
-// export function createI18nMiddleware(config: I18nMiddlewareConfig) {
-// 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: Locales,
-// request: NextRequest,
-// resolveLocaleFromRequest: NonNullable<
-// I18nMiddlewareConfig['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['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: Locales;
+ defaultLocale: Locales[number];
+ resolveLocaleFromRequest?: (request: NextRequest) => string | null;
+ urlMappingStrategy?: 'redirect' | 'rewrite' | 'rewriteDefault';
+};
+
+function getLocaleFromRequest(
+ request: NextRequest,
+ config: I18nMiddlewareConfig,
+) {
+ 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(
+ middleware: NextMiddleware,
+ config: I18nMiddlewareConfig,
+): 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);
};
}