diff --git a/build.config.ts b/build.config.ts index d20a7a9..3527223 100644 --- a/build.config.ts +++ b/build.config.ts @@ -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'], }) diff --git a/bun.lockb b/bun.lockb index 1b0935c..5e0dcf3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 288abaa..f1aba0d 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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", diff --git a/src/h3.test.ts b/src/h3.test.ts new file mode 100644 index 0000000..d5f3a04 --- /dev/null +++ b/src/h3.test.ts @@ -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([]) + }) +}) diff --git a/src/h3.ts b/src/h3.ts new file mode 100644 index 0000000..c63d42b --- /dev/null +++ b/src/h3.ts @@ -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} 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) +} diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..9e48954 --- /dev/null +++ b/src/http.ts @@ -0,0 +1,8 @@ +import { parseAcceptLanguage } from './shared.ts' + +export function getAcceptLanguagesFromGetter( + getter: () => string | null | undefined, +): string[] { + const acceptLanguage = getter() + return acceptLanguage ? parseAcceptLanguage(acceptLanguage) : [] +} diff --git a/src/index.ts b/src/index.ts index 980dfdb..34e93b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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} 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' diff --git a/src/node.test.ts b/src/node.test.ts new file mode 100644 index 0000000..04d59a1 --- /dev/null +++ b/src/node.test.ts @@ -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([]) + }) +}) diff --git a/src/node.ts b/src/node.ts new file mode 100644 index 0000000..2d1bb6c --- /dev/null +++ b/src/node.ts @@ -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} 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) +} diff --git a/test/index.test.ts b/src/shared.test.ts similarity index 92% rename from test/index.test.ts rename to src/shared.test.ts index 2ca08d6..85d2923 100644 --- a/test/index.test.ts +++ b/src/shared.test.ts @@ -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', () => { diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 0000000..980dfdb --- /dev/null +++ b/src/shared.ts @@ -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} 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 + } +} diff --git a/src/web.test.ts b/src/web.test.ts new file mode 100644 index 0000000..aa8ebe0 --- /dev/null +++ b/src/web.test.ts @@ -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([]) + }) +}) diff --git a/src/web.ts b/src/web.ts new file mode 100644 index 0000000..19a9e69 --- /dev/null +++ b/src/web.ts @@ -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} 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) +}