Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support runtime aginostic getAcceptLanguages #4

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}