Skip to content

Commit

Permalink
feat: support runtime aginostic getAcceptLanguages (#4)
Browse files Browse the repository at this point in the history
* feat: add `parseAcceptLanguage`

* feat: support framework/runtime aginostic `getAcceptLanguages`
  • Loading branch information
kazupon authored Sep 20, 2023
1 parent c8b348c commit 9ddc86b
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 49 deletions.
16 changes: 15 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,19 @@ export default defineBuildConfig({
rollup: {
emitCJS: true,
},
entries: ['./src/index.ts'],
entries: [
{
input: './src/index.ts',
},
{
input: './src/h3.ts',
},
{
input: './src/node.ts',
},
{
input: './src/web.ts',
},
],
externals: ['h3'],
})
Binary file modified bun.lockb
Binary file not shown.
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./h3": {
"types": "./dist/h3.d.ts",
"import": "./dist/h3.mjs",
"require": "./dist/h3.cjs"
},
"./node": {
"types": "./dist/node.d.ts",
"import": "./dist/node.mjs",
"require": "./dist/node.cjs"
},
"./web": {
"types": "./dist/web.d.ts",
"import": "./dist/web.mjs",
"require": "./dist/web.cjs"
},
"./dist/*": "./dist/*",
"./package.json": "./package.json"
},
Expand Down Expand Up @@ -67,6 +82,7 @@
"@vitest/coverage-v8": "^0.34.4",
"bumpp": "^9.2.0",
"gh-changelogen": "^0.2.8",
"h3": "^1.8.1",
"lint-staged": "^14.0.0",
"typescript": "^5.2.2",
"unbuild": "^2.0.0",
Expand Down
46 changes: 46 additions & 0 deletions src/h3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, test } from 'vitest'
import { getAcceptLanguages } from './h3.ts'

import type { H3Event } from 'h3'

describe('getAcceptLanguages', () => {
test('basic', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {
'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
},
},
},
} as H3Event
expect(getAcceptLanguages(eventMock)).toEqual(['en-US', 'en', 'ja'])
})

test('any language', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {
'accept-language': '*',
},
},
},
} as H3Event
expect(getAcceptLanguages(eventMock)).toEqual([])
})

test('empty', () => {
const eventMock = {
node: {
req: {
method: 'GET',
headers: {},
},
},
} as H3Event
expect(getAcceptLanguages(eventMock)).toEqual([])
})
})
21 changes: 21 additions & 0 deletions src/h3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getAcceptLanguagesFromGetter } from './http.ts'
import { getHeaders } from 'h3'

import type { H3Event } from 'h3'

/**
* get accpet languages
*
* @description parse `accept-language` header string
*
* @param {H3Event} event The {@link H3Event | H3} event
*
* @returns {Array<string>} The array of language tags, if `*` (any language) or empty string is detected, return an empty array.
*/
export function getAcceptLanguages(event: H3Event): string[] {
const getter = () => {
const headers = getHeaders(event)
return headers['accept-language']
}
return getAcceptLanguagesFromGetter(getter)
}
8 changes: 8 additions & 0 deletions src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { parseAcceptLanguage } from './shared.ts'

export function getAcceptLanguagesFromGetter(
getter: () => string | null | undefined,
): string[] {
const acceptLanguage = getter()
return acceptLanguage ? parseAcceptLanguage(acceptLanguage) : []
}
44 changes: 1 addition & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1 @@
const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)

/**
* check whether the value is a {@link Intl.Locale} instance
*
* @param {unknown} val The locale value
*
* @returns {boolean} Returns `true` if the value is a {@link Intl.Locale} instance, else `false`.
*/
export function isLocale(val: unknown): val is Intl.Locale {
return toTypeString(val) === '[object Intl.Locale]'
}

/**
* parse `accept-language` header string
*
* @param {string} value The accept-language header string
*
* @returns {Array<string>} The array of language tags, if `*` (any language) or empty string is detected, return an empty array.
*/
export function parseAcceptLanguage(value: string): string[] {
return value.split(',').map((tag) => tag.split(';')[0]).filter((tag) =>
!(tag === '*' || tag === '')
)
}

/**
* validate the language tag whether is a well-formed {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tag}.
*
* @param {string} lang a language tag
*
* @returns {boolean} Returns `true` if the language tag is valid, else `false`.
*/
export function validateLanguageTag(lang: string): boolean {
try {
// TODO: if we have a better way to validate the language tag, we should use it.
new Intl.Locale(lang)
return true
} catch {
return false
}
}
export * from './shared.ts'
30 changes: 30 additions & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, test } from 'vitest'
import { getAcceptLanguages } from './node.ts'
import { IncomingMessage } from 'node:http'

describe('getAcceptLanguages', () => {
test('basic', () => {
const mockRequest = {
headers: {
'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
},
} as IncomingMessage
expect(getAcceptLanguages(mockRequest)).toEqual(['en-US', 'en', 'ja'])
})

test('any language', () => {
const mockRequest = {
headers: {
'accept-language': '*',
},
} as IncomingMessage
expect(getAcceptLanguages(mockRequest)).toEqual([])
})

test('empty', () => {
const mockRequest = {
headers: {},
} as IncomingMessage
expect(getAcceptLanguages(mockRequest)).toEqual([])
})
})
16 changes: 16 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IncomingMessage } from 'node:http'
import { getAcceptLanguagesFromGetter } from './http.ts'

/**
* get accpet languages
*
* @description parse `accept-language` header string
*
* @param {IncomingMessage} event The {@link IncomingMessage | request}
*
* @returns {Array<string>} The array of language tags, if `*` (any language) or empty string is detected, return an empty array.
*/
export function getAcceptLanguages(req: IncomingMessage) {
const getter = () => req.headers['accept-language']
return getAcceptLanguagesFromGetter(getter)
}
6 changes: 1 addition & 5 deletions test/index.test.ts → src/shared.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { describe, expect, test } from 'vitest'
import {
isLocale,
parseAcceptLanguage,
validateLanguageTag,
} from '../src/index.ts'
import { isLocale, parseAcceptLanguage, validateLanguageTag } from './shared.ts'

describe('isLocale', () => {
test('Locale instance', () => {
Expand Down
43 changes: 43 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)

/**
* check whether the value is a {@link Intl.Locale} instance
*
* @param {unknown} val The locale value
*
* @returns {boolean} Returns `true` if the value is a {@link Intl.Locale} instance, else `false`.
*/
export function isLocale(val: unknown): val is Intl.Locale {
return toTypeString(val) === '[object Intl.Locale]'
}

/**
* parse `accept-language` header string
*
* @param {string} value The accept-language header string
*
* @returns {Array<string>} The array of language tags, if `*` (any language) or empty string is detected, return an empty array.
*/
export function parseAcceptLanguage(value: string): string[] {
return value.split(',').map((tag) => tag.split(';')[0]).filter((tag) =>
!(tag === '*' || tag === '')
)
}

/**
* validate the language tag whether is a well-formed {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tag}.
*
* @param {string} lang a language tag
*
* @returns {boolean} Returns `true` if the language tag is valid, else `false`.
*/
export function validateLanguageTag(lang: string): boolean {
try {
// TODO: if we have a better way to validate the language tag, we should use it.
new Intl.Locale(lang)
return true
} catch {
return false
}
}
21 changes: 21 additions & 0 deletions src/web.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, test } from 'vitest'
import { getAcceptLanguages } from './web.ts'

describe('getAcceptLanguages', () => {
test('basic', () => {
const mockRequest = new Request('https://example.com')
mockRequest.headers.set('accept-language', 'en-US,en;q=0.9,ja;q=0.8')
expect(getAcceptLanguages(mockRequest)).toEqual(['en-US', 'en', 'ja'])
})

test('any language', () => {
const mockRequest = new Request('https://example.com')
mockRequest.headers.set('accept-language', '*')
expect(getAcceptLanguages(mockRequest)).toEqual([])
})

test('empty', () => {
const mockRequest = new Request('https://example.com')
expect(getAcceptLanguages(mockRequest)).toEqual([])
})
})
15 changes: 15 additions & 0 deletions src/web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getAcceptLanguagesFromGetter } from './http.ts'

/**
* get accpet languages
*
* @description parse `accept-language` header string
*
* @param {Request} event The {@link Request | request}
*
* @returns {Array<string>} The array of language tags, if `*` (any language) or empty string is detected, return an empty array.
*/
export function getAcceptLanguages(req: Request) {
const getter = () => req.headers.get('accept-language')
return getAcceptLanguagesFromGetter(getter)
}

0 comments on commit 9ddc86b

Please sign in to comment.