Skip to content

Commit

Permalink
feat: add getCookieLocale
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Sep 21, 2023
1 parent 0e8a660 commit 6f86dab
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 13 deletions.
1 change: 1 addition & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default defineBuildConfig({
declaration: true,
rollup: {
emitCJS: true,
inlineDependencies: true,
},
entries: [
{
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"@types/node": "^20.6.0",
"@vitest/coverage-v8": "^0.34.4",
"bumpp": "^9.2.0",
"cookie-es": "^1.0.0",
"gh-changelogen": "^0.2.8",
"h3": "^1.8.1",
"lint-staged": "^14.0.0",
Expand Down
82 changes: 81 additions & 1 deletion src/h3.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { getAcceptLanguages, getLocale } from './h3.ts'
import { getAcceptLanguages, getCookieLocale, getLocale } from './h3.ts'

import type { H3Event } from 'h3'

Expand Down Expand Up @@ -111,3 +111,83 @@ describe('getLocale', () => {
expect(() => getLocale(eventMock, 'ja-JP')).toThrowError(RangeError)
})
})

describe('getCookieLocale', () => {
test('basic', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {
cookie: 'i18n_locale=ja-US',
},
},
},
} as H3Event
const locale = getCookieLocale(eventMock)

expect(locale.baseName).toEqual('ja-US')
expect(locale.language).toEqual('ja')
expect(locale.region).toEqual('US')
})

test('cookie is empty', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {},
},
},
} as H3Event
const locale = getCookieLocale(eventMock)

expect(locale.baseName).toEqual('en-US')
})

test('specify default language', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {},
},
},
} as H3Event
const locale = getCookieLocale(eventMock, { lang: 'ja-JP' })

expect(locale.baseName).toEqual('ja-JP')
})

test('specify cookie name', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {
cookie: 'intlify_locale=fr-FR',
},
},
},
} as H3Event
const locale = getCookieLocale(eventMock, { name: 'intlify_locale' })

expect(locale.baseName).toEqual('fr-FR')
})

test('RangeError', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {
cookie: 'intlify_locale=f',
},
},
},
} as H3Event

expect(() => getCookieLocale(eventMock, { name: 'intlify_locale' }))
.toThrowError(RangeError)
})
})
40 changes: 36 additions & 4 deletions src/h3.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAcceptLanguagesFromGetter, getLocaleWithGetter } from './http.ts'
import { getHeaders } from 'h3'
import { getCookie, getHeaders } from 'h3'

import type { H3Event } from 'h3'

Expand All @@ -9,7 +9,7 @@ import type { H3Event } from 'h3'
* @description parse `accept-language` header string
*
* @example
* example for h3 event:
* example for h3:
*
* ```ts
* import { createApp, eventHandler } from 'h3'
Expand Down Expand Up @@ -38,11 +38,11 @@ export function getAcceptLanguages(event: H3Event): string[] {
* get locale
*
* @example
* example for h3 event:
* example for h3:
*
* ```ts
* import { createApp, eventHandler } from 'h3'
* import { getAcceptLanguages } from '@intlify/utils/h3'
* import { getLocale } from '@intlify/utils/h3'
*
* app.use(eventHandler(event) => {
* const locale = getLocale(event)
Expand All @@ -61,3 +61,35 @@ export function getAcceptLanguages(event: H3Event): string[] {
export function getLocale(event: H3Event, lang = 'en-US'): Intl.Locale {
return getLocaleWithGetter(() => getAcceptLanguages(event)[0] || lang)
}

/**
* get locale from cookie
*
* @example
* example for h3:
*
* ```ts
* import { createApp, eventHandler } from 'h3'
* import { getCookieLocale } from '@intlify/utils/h3'
*
* app.use(eventHandler(event) => {
* const locale = getCookieLocale(event)
* console.log(locale) // output `Intl.Locale` instance
* // ...
* })
* ```
*
* @param {H3Event} event The {@link H3Event | H3} event
* @param {string} options.lang The default language tag, default is `en-US`. You must specify the language tag with the {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 syntax}.
* @param {string} options.name The cookie name, default is `i18n_locale`
*
* @throws {RangeError} Throws a {@link RangeError} if `lang` option or cookie name value are not a well-formed BCP 47 language tag.
*
* @returns The locale that resolved from cookie
*/
export function getCookieLocale(
event: H3Event,
{ lang = 'en-US', name = 'i18n_locale' } = {},
): Intl.Locale {
return getLocaleWithGetter(() => getCookie(event, name) || lang)
}
59 changes: 58 additions & 1 deletion src/node.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { getAcceptLanguages, getLocale } from './node.ts'
import { getAcceptLanguages, getCookieLocale, getLocale } from './node.ts'
import { IncomingMessage } from 'node:http'

describe('getAcceptLanguages', () => {
Expand Down Expand Up @@ -74,3 +74,60 @@ describe('getLocale', () => {
expect(() => getLocale(mockRequest, 'ja-JP')).toThrowError(RangeError)
})
})

describe('getCookieLocale', () => {
test('basic', () => {
const mockRequest = {
headers: {
cookie: 'i18n_locale=ja-US',
},
} as IncomingMessage
const locale = getCookieLocale(mockRequest)

expect(locale.baseName).toEqual('ja-US')
expect(locale.language).toEqual('ja')
expect(locale.region).toEqual('US')
})

test('cookie is empty', () => {
const mockRequest = {
headers: {
cookie: '',
},
} as IncomingMessage
const locale = getCookieLocale(mockRequest)

expect(locale.baseName).toEqual('en-US')
})

test('specify default language', () => {
const mockRequest = {
headers: {},
} as IncomingMessage
const locale = getCookieLocale(mockRequest, { lang: 'ja-JP' })

expect(locale.baseName).toEqual('ja-JP')
})

test('specify cookie name', () => {
const mockRequest = {
headers: {
cookie: 'intlify_locale=fr-FR',
},
} as IncomingMessage
const locale = getCookieLocale(mockRequest, { name: 'intlify_locale' })

expect(locale.baseName).toEqual('fr-FR')
})

test('RangeError', () => {
const mockRequest = {
headers: {
cookie: 'intlify_locale=f',
},
} as IncomingMessage

expect(() => getCookieLocale(mockRequest, { name: 'intlify_locale' }))
.toThrowError(RangeError)
})
})
44 changes: 41 additions & 3 deletions src/node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IncomingMessage } from 'node:http'
import { parse } from 'cookie-es'
import { getAcceptLanguagesFromGetter, getLocaleWithGetter } from './http.ts'

/**
Expand All @@ -19,7 +20,7 @@ import { getAcceptLanguagesFromGetter, getLocaleWithGetter } from './http.ts'
* })
* ```
*
* @param {IncomingMessage} event The {@link IncomingMessage | request}
* @param {IncomingMessage} request The {@link IncomingMessage | request}
*
* @returns {Array<string>} The array of language tags, if `*` (any language) or empty string is detected, return an empty array.
*/
Expand All @@ -36,7 +37,7 @@ export function getAcceptLanguages(req: IncomingMessage) {
*
* ```ts
* import { createServer } from 'node:http'
* import { getAcceptLanguages } from '@intlify/utils/node'
* import { getLocale } from '@intlify/utils/node'
*
* const server = createServer((req, res) => {
* const locale = getLocale(req)
Expand All @@ -45,7 +46,7 @@ export function getAcceptLanguages(req: IncomingMessage) {
* })
* ```
*
* @param {IncomingMessage} event The {@link IncomingMessage | request}
* @param {IncomingMessage} request The {@link IncomingMessage | request}
* @param {string} lang The default language tag, default is `en-US`. You must specify the language tag with the {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 syntax}.
*
* @throws {RangeError} Throws a {@link RangeError} if `lang` option or `accpet-languages` are not a well-formed BCP 47 language tag.
Expand All @@ -55,3 +56,40 @@ export function getAcceptLanguages(req: IncomingMessage) {
export function getLocale(req: IncomingMessage, lang = 'en-US'): Intl.Locale {
return getLocaleWithGetter(() => getAcceptLanguages(req)[0] || lang)
}

/**
* get locale from cookie
*
* @example
* example for Node.js request:
*
* ```ts
* import { createServer } from 'node:http'
* import { getCookieLocale } from '@intlify/utils/node'
*
* const server = createServer((req, res) => {
* const locale = getCookieLocale(req)
* console.log(locale) // output `Intl.Locale` instance
* // ...
* })
* ```
*
* @param {IncomingMessage} request The {@link IncomingMessage | request}
* @param {string} options.lang The default language tag, default is `en-US`. You must specify the language tag with the {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 syntax}.
* @param {string} options.name The cookie name, default is `i18n_locale`
*
* @throws {RangeError} Throws a {@link RangeError} if `lang` option or cookie name value are not a well-formed BCP 47 language tag.
*
* @returns The locale that resolved from cookie
*/
export function getCookieLocale(
req: IncomingMessage,
{ lang = 'en-US', name = 'i18n_locale' } = {},
): Intl.Locale {
const getter = () => {
const cookieRaw = req.headers.cookie
const cookie = parse(cookieRaw || '')
return cookie[name] || lang
}
return getLocaleWithGetter(getter)
}
44 changes: 43 additions & 1 deletion src/web.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { getAcceptLanguages, getLocale } from './web.ts'
import { getAcceptLanguages, getCookieLocale, getLocale } from './web.ts'

describe('getAcceptLanguages', () => {
test('basic', () => {
Expand Down Expand Up @@ -53,3 +53,45 @@ describe('getLocale', () => {
expect(() => getLocale(mockRequest, 'ja-JP')).toThrowError(RangeError)
})
})

describe('getCookieLocale', () => {
test('basic', () => {
const mockRequest = new Request('https://example.com')
mockRequest.headers.set('cookie', 'i18n_locale=ja-US')
const locale = getCookieLocale(mockRequest)

expect(locale.baseName).toEqual('ja-US')
expect(locale.language).toEqual('ja')
expect(locale.region).toEqual('US')
})

test('cookie is empty', () => {
const mockRequest = new Request('https://example.com')
mockRequest.headers.set('cookie', '')
const locale = getCookieLocale(mockRequest)

expect(locale.baseName).toEqual('en-US')
})

test('specify default language', () => {
const mockRequest = new Request('https://example.com')
const locale = getCookieLocale(mockRequest, { lang: 'ja-JP' })

expect(locale.baseName).toEqual('ja-JP')
})

test('specify cookie name', () => {
const mockRequest = new Request('https://example.com')
mockRequest.headers.set('cookie', 'intlify_locale=fr-FR')
const locale = getCookieLocale(mockRequest, { name: 'intlify_locale' })

expect(locale.baseName).toEqual('fr-FR')
})

test('RangeError', () => {
const mockRequest = new Request('https://example.com')
mockRequest.headers.set('cookie', 'intlify_locale=f')
expect(() => getCookieLocale(mockRequest, { name: 'intlify_locale' }))
.toThrowError(RangeError)
})
})
Loading

0 comments on commit 6f86dab

Please sign in to comment.