Skip to content

Commit

Permalink
add cookieOptions config option for setting custom Set-Cookie att…
Browse files Browse the repository at this point in the history
…ributes
  • Loading branch information
dcporter44 committed May 29, 2024
1 parent c83a837 commit 78b1f2a
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 5.5.0

- Add `cookieOptions` config option for setting custom `Set-Cookie` attributes

## 5.4.3

- Remove console warning in `localeDetector` when invalid accept-language header present
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ You now have internationalized routing!
| `localeCookie` | `'NEXT_LOCALE'` | string | |
| `noPrefix` | `false` | boolean | |
| `serverSetCookie` | `'always'` | "always" \| "if-empty" \| "never" | |
| `cookieOptions` | (See below) | object | |
| `basePath` | `''` | string | |

## Locale Path Prefixing
Expand Down Expand Up @@ -124,6 +125,22 @@ The `serverSetCookie` option automatically changes a visitor's preferred locale

If you are using `noPrefix`, the `serverSetCookie` option does not do anything since there is no locale in the pathname to read from. All language changing must be done by setting the cookie manually.

### cookieOptions (optional)

The server sets the cookie by setting the `Set-Cookie` HTTP response header on the `NextResponse`. [(Learn More)](https://nextjs.org/docs/app/api-reference/functions/next-response#setname-value)

By default, `cookieOptions` is set to:

```
{
sameSite: 'strict',
maxAge: 31536000,
path: {the basePath of the incoming NextRequest}
}
```

You can set your own `cookieOptions` object containing any of the valid `Set-Cookie` attributes: [MDN: Set-Cookie](https://nextjs.org/docs/app/api-reference/functions/next-response)

## Using `basePath` (optional)

This is only needed if you are using the `basePath` option in `next.config.js`. You will need to also include it as the `basePath` option in your `i18nConfig`.
Expand Down
67 changes: 59 additions & 8 deletions __tests__/i18nRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ basePaths.forEach(basePath => {
});

it('should throw an error if localeDetector is not a function', () => {
const config = {
locales: ['en-US'],
defaultLocale: 'en-US',
localeDetector: 'invalid',
basePath,
serverSetCookie: 'never'
};

expect(() =>
i18nRouter(mockRequest('/', ['en']), {
locales: ['en-US'],
Expand All @@ -61,6 +53,18 @@ basePaths.forEach(basePath => {
).toThrow(/localeDetector/);
});

it('should throw an error if invalid serverSetCookie value', () => {
expect(() =>
i18nRouter(mockRequest('/', ['en']), {
locales: ['en-US'],
defaultLocale: 'en-US',
basePath,
// @ts-ignore
serverSetCookie: 'invalid'
})
).toThrow(/serverSetCookie/);
});

it('should throw an error if request argument is missing', () => {
// @ts-ignore
expect(() => i18nRouter()).toThrow(/request/);
Expand Down Expand Up @@ -400,5 +404,52 @@ basePaths.forEach(basePath => {
new URL(`${basePath}/de/faq`, 'https://example.com/faq').href
);
});

it('should have default cookie options', () => {
const mockRedirect = jest.fn().mockReturnValue(new NextResponse());
NextResponse.redirect = mockRedirect;

const request = mockRequest('/de/faq', ['en']);

const response = i18nRouter(request, {
locales: ['en', 'de'],
defaultLocale: 'en',
basePath
});

const cookieHeader = response.headers.get('set-cookie');
expect(cookieHeader).toContain('Path=/;');
expect(cookieHeader).toContain('Max-Age=31536000');
expect(cookieHeader).toContain('SameSite=strict');
});

it('should use cookieOptions option', () => {
const mockRedirect = jest.fn().mockReturnValue(new NextResponse());
NextResponse.redirect = mockRedirect;

const request = mockRequest('/de/faq', ['en']);

const response = i18nRouter(request, {
locales: ['en', 'de'],
defaultLocale: 'en',
basePath,
cookieOptions: {
path: '/test',
maxAge: 31536001,
sameSite: 'lax',
secure: true,
httpOnly: true,
domain: 'example.com'
}
});

const cookieHeader = response.headers.get('set-cookie');
expect(cookieHeader).toContain('Path=/test;');
expect(cookieHeader).toContain('Max-Age=31536001');
expect(cookieHeader).toContain('SameSite=lax');
expect(cookieHeader).toContain('Secure;');
expect(cookieHeader).toContain('HttpOnly;');
expect(cookieHeader).toContain('Domain=example.com');
});
});
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-i18n-router",
"version": "5.4.3",
"version": "5.5.0",
"description": "Next.js App Router internationalized routing and locale detection.",
"repository": "https://github.com/i18nexus/next-i18n-router",
"keywords": [
Expand Down
13 changes: 7 additions & 6 deletions src/i18nRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ function i18nRouter(request: NextRequest, config: Config): NextResponse {
prefixDefault = false,
basePath = '',
serverSetCookie = 'always',
noPrefix = false
noPrefix = false,
cookieOptions = {
path: request.nextUrl.basePath || undefined,
sameSite: 'strict',
maxAge: 31536000 // one year
}
} = config;

validateConfig(config);
Expand Down Expand Up @@ -133,11 +138,7 @@ function i18nRouter(request: NextRequest, config: Config): NextResponse {
}

const setCookie = () => {
response.cookies.set(localeCookie, pathLocale, {
path: request.nextUrl.basePath || undefined,
sameSite: 'strict',
maxAge: 31536000 // expires after one year
});
response.cookies.set(localeCookie, pathLocale, cookieOptions);
};

if (serverSetCookie !== 'never') {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextRequest } from 'next/server';
import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies';

export interface Config {
locales: readonly string[];
Expand All @@ -9,4 +10,5 @@ export interface Config {
noPrefix?: boolean;
basePath?: string;
serverSetCookie?: 'if-empty' | 'always' | 'never';
cookieOptions?: Partial<ResponseCookie>;
}
17 changes: 17 additions & 0 deletions src/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ function validateConfig(config: Config): void {
if (config.localeDetector && typeof config.localeDetector !== 'function') {
throw new Error(`'localeDetector' must be a function.`);
}

if (config.cookieOptions) {
if (typeof config.cookieOptions !== 'object') {
throw new Error(`'cookieOptions' must be an object.`);
}
}

if (config.serverSetCookie) {
const validOptions = ['if-empty', 'always', 'never'];
if (!validOptions.includes(config.serverSetCookie)) {
throw new Error(
`Invalid 'serverSetCookie' value. Valid values are ${validOptions.join(
' | '
)}`
);
}
}
}

export default validateConfig;

0 comments on commit 78b1f2a

Please sign in to comment.