diff --git a/README.md b/README.md index 9d2dda0..c9acea0 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ You can do `import { ... } from '@intlify/utils'` the above utilities - `getAcceptLanguages` - `getAcceptLanguage` +- `getAcceptLocales` - `getAcceptLocale` - `getCookieLocale` - `setCookieLocale` diff --git a/src/h3.test.ts b/src/h3.test.ts index 72411d4..f5e80e0 100644 --- a/src/h3.test.ts +++ b/src/h3.test.ts @@ -5,6 +5,7 @@ import { getAcceptLanguage, getAcceptLanguages, getAcceptLocale, + getAcceptLocales, getCookieLocale, setCookieLocale, } from './h3.ts' @@ -97,6 +98,49 @@ describe('getAcceptLanguage', () => { }) }) +describe('getAcceptLocales', () => { + test('basic', () => { + const mockEvent = { + node: { + req: { + method: 'GET', + headers: { + 'accept-language': 'en-US,en;q=0.9,ja;q=0.8', + }, + }, + }, + } as H3Event + expect(getAcceptLocales(mockEvent).map((locale) => locale.baseName)) + .toEqual(['en-US', 'en', 'ja']) + }) + + test('any language', () => { + const mockEvent = { + node: { + req: { + method: 'GET', + headers: { + 'accept-language': '*', + }, + }, + }, + } as H3Event + expect(getAcceptLocales(mockEvent)).toEqual([]) + }) + + test('empty', () => { + const mockEvent = { + node: { + req: { + method: 'GET', + headers: {}, + }, + }, + } as H3Event + expect(getAcceptLocales(mockEvent)).toEqual([]) + }) +}) + describe('getAcceptLocale', () => { test('basic', () => { const mockEvent = { diff --git a/src/h3.ts b/src/h3.ts index 62b8131..128ac60 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -1,6 +1,7 @@ import { getAcceptLanguagesWithGetter, getLocaleWithGetter, + mapToLocaleFromLanguageTag, validateLocale, } from './http.ts' import { getCookie, getHeaders, setCookie } from 'h3' @@ -70,28 +71,57 @@ export function getAcceptLanguage(event: H3Event): string { } /** - * get {@link Intl.Locale | locale} from `accept-language` header + * get locales from `accept-language` header * - * @description wrap with {@link Intl.Locale | locale} + * @description wrap language tags with {@link Intl.Locale | locale} * * @example * example for h3: * * ```ts * import { createApp, eventHandler } from 'h3' - * import { getLocale } from '@intlify/utils/h3' + * import { getAcceptLocales } from '@intlify/utils/h3' * * app.use(eventHandler(event) => { - * const locale = getLocale(event) - * console.log(locale) // output `Intl.Locale` instance + * const locales = getAcceptLocales(event) + * // ... + * return `accepted locales: ${locales.map(locale => locale.toString()).join(', ')}` + * }) + * ``` + * + * @param {H3Event} event The {@link H3Event | H3} event + * + * @returns {Array} The locales that wrapped from `accept-language` header, if `*` (any language) or empty string is detected, return an empty array. + */ +export function getAcceptLocales( + event: H3Event, +): Intl.Locale[] { + return mapToLocaleFromLanguageTag(getAcceptLanguages, event) +} + +/** + * get locale from `accept-language` header + * + * @description wrap language tag with {@link Intl.Locale | locale} + * + * @example + * example for h3: + * + * ```ts + * import { createApp, eventHandler } from 'h3' + * import { getAcceptLocale } from '@intlify/utils/h3' + * + * app.use(eventHandler(event) => { + * const locale = getAcceptLocale(event) * // ... + * return `accepted locale: ${locale.toString()}` * }) * ``` * * @param {H3Event} event The {@link H3Event | H3} event - * @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}. + * @param {string} lang The default language tag, Optional. default value 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. + * @throws {RangeError} Throws the {@link RangeError} if `lang` option or `accpet-languages` are not a well-formed BCP 47 language tag. * * @returns {Intl.Locale} The first locale that resolved from `accept-language` header string, first language tag is used. if `*` (any language) or empty string is detected, return `en-US`. */ diff --git a/src/http.ts b/src/http.ts index 0a9e742..8101982 100644 --- a/src/http.ts +++ b/src/http.ts @@ -123,6 +123,16 @@ export function validateLocale(locale: string | Intl.Locale): void { } } +export function mapToLocaleFromLanguageTag( + // deno-lint-ignore no-explicit-any + getter: (...args: any[]) => string[], + arg: unknown, +): Intl.Locale[] { + return (Reflect.apply(getter, null, [arg]) as string[]).map((lang) => + getLocaleWithGetter(() => lang) + ) +} + export function getExistCookies( name: string, getter: () => unknown, diff --git a/src/node.test.ts b/src/node.test.ts index 38143f5..769fe0a 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -4,6 +4,7 @@ import { getAcceptLanguage, getAcceptLanguages, getAcceptLocale, + getAcceptLocales, getCookieLocale, setCookieLocale, } from './node.ts' @@ -64,6 +65,34 @@ describe('getAcceptLanguage', () => { }) }) +describe('getAcceptLocales', () => { + test('basic', () => { + const mockRequest = { + headers: { + 'accept-language': 'en-US,en;q=0.9,ja;q=0.8', + }, + } as IncomingMessage + expect(getAcceptLocales(mockRequest).map((locale) => locale.baseName)) + .toEqual(['en-US', 'en', 'ja']) + }) + + test('any language', () => { + const mockRequest = { + headers: { + 'accept-language': '*', + }, + } as IncomingMessage + expect(getAcceptLocales(mockRequest)).toEqual([]) + }) + + test('empty', () => { + const mockRequest = { + headers: {}, + } as IncomingMessage + expect(getAcceptLocales(mockRequest)).toEqual([]) + }) +}) + describe('getAcceptLocale', () => { test('basic', () => { const mockRequest = { diff --git a/src/node.ts b/src/node.ts index aec7191..88c9ef9 100644 --- a/src/node.ts +++ b/src/node.ts @@ -4,6 +4,7 @@ import { getAcceptLanguagesWithGetter, getExistCookies, getLocaleWithGetter, + mapToLocaleFromLanguageTag, validateLocale, } from './http.ts' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' @@ -68,28 +69,59 @@ export function getAcceptLanguage(request: IncomingMessage): string { } /** - * get {@link Intl.Locale | locale} from `accept-language` header + * get locales from `accept-language` header * - * @description wrap with {@link Intl.Locale | locale} + * @description wrap language tags with {@link Intl.Locale | locale} * * @example * example for Node.js request: * * ```ts * import { createServer } from 'node:http' - * import { getLocale } from '@intlify/utils/node' + * import { getAcceptLocales } from '@intlify/utils/node' * * const server = createServer((req, res) => { - * const locale = getLocale(req) - * console.log(locale) // output `Intl.Locale` instance + * const locales = getAcceptLocales(req) * // ... + * res.writeHead(200) + * res.end(`accpected locales: ${locales.map(locale => locale.toString()).join(', ')}`) + * }) + * ``` + * + * @param {IncomingMessage} request The {@link IncomingMessage | request} + * + * @returns {Array} The locales that wrapped from `accept-language` header, if `*` (any language) or empty string is detected, return an empty array. + */ +export function getAcceptLocales( + request: IncomingMessage, +): Intl.Locale[] { + return mapToLocaleFromLanguageTag(getAcceptLanguages, request) +} + +/** + * get locale from `accept-language` header + * + * @description wrap language tag with {@link Intl.Locale | locale} + * + * @example + * example for Node.js request: + * + * ```ts + * import { createServer } from 'node:http' + * import { getAcceptLocale } from '@intlify/utils/node' + * + * const server = createServer((req, res) => { + * const locale = getAcceptLocale(req) + * // ... + * res.writeHead(200) + * res.end(`accpected locale: ${locale.toString()}`) * }) * ``` * * @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}. + * @param {string} lang The default language tag, Optional. default value 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. + * @throws {RangeError} Throws the {@link RangeError} if `lang` option or `accpet-languages` are not a well-formed BCP 47 language tag. * * @returns {Intl.Locale} The first locale that resolved from `accept-language` header string, first language tag is used. if `*` (any language) or empty string is detected, return `en-US`. */ diff --git a/src/web.test.ts b/src/web.test.ts index 55bbd37..849a712 100644 --- a/src/web.test.ts +++ b/src/web.test.ts @@ -3,6 +3,7 @@ import { getAcceptLanguage, getAcceptLanguages, getAcceptLocale, + getAcceptLocales, getCookieLocale, setCookieLocale, } from './web.ts' @@ -27,6 +28,26 @@ describe('getAcceptLanguages', () => { }) }) +describe('getAcceptLocales', () => { + 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(getAcceptLocales(mockRequest).map((locale) => locale.baseName)) + .toEqual(['en-US', 'en', 'ja']) + }) + + test('any language', () => { + const mockRequest = new Request('https://example.com') + mockRequest.headers.set('accept-language', '*') + expect(getAcceptLocales(mockRequest)).toEqual([]) + }) + + test('empty', () => { + const mockRequest = new Request('https://example.com') + expect(getAcceptLocales(mockRequest)).toEqual([]) + }) +}) + describe('getAcceptLanguage', () => { test('basic', () => { const mockRequest = new Request('https://example.com') diff --git a/src/web.ts b/src/web.ts index 5baf386..2a4b5a7 100644 --- a/src/web.ts +++ b/src/web.ts @@ -3,6 +3,7 @@ import { getAcceptLanguagesWithGetter, getExistCookies, getLocaleWithGetter, + mapToLocaleFromLanguageTag, validateLocale, } from './http.ts' import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts' @@ -67,28 +68,58 @@ export function getAcceptLanguage(request: Request): string { } /** - * get {@link Intl.Locale | locale} from `accept-language` header + * get locales from `accept-language` header * - * @description wrap with {@link Intl.Locale | locale} + * @description wrap language tags with {@link Intl.Locale | locale} * * @example * example for Web API request on Bun: * - * import { getLocale } from '@intlify/utils/web' + * import { getAcceptLocales } from '@intlify/utils/web' * * Bun.serve({ * port: 8080, * fetch(req) { - * const locale = getLocale(req) - * console.log(locale) // output `Intl.Locale` instance + * const locales = getAcceptLocales(req) * // ... + * return new Response(`accpected locales: ${locales.map(locale => locale.toString()).join(', ')}`) + * }, + * }) + * ``` + * + * @param {Request} request The {@link Request | request} + * + * @returns {Array} The locales that wrapped from `accept-language` header, if `*` (any language) or empty string is detected, return an empty array. + */ +export function getAcceptLocales( + request: Request, +): Intl.Locale[] { + return mapToLocaleFromLanguageTag(getAcceptLanguages, request) +} + +/** + * get locale from `accept-language` header + * + * @description wrap language tag with {@link Intl.Locale | locale} + * + * @example + * example for Web API request on Bun: + * + * import { getAcceptLocale } from '@intlify/utils/web' + * + * Bun.serve({ + * port: 8080, + * fetch(req) { + * const locale = getAcceptLocale(req) + * // ... + * return new Response(`accpected locale: ${locale.toString()}`) * }, * }) * * @param {Request} request The {@link Request | 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}. + * @param {string} lang The default language tag, Optional. default value 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. + * @throws {RangeError} Throws the {@link RangeError} if `lang` option or `accpet-languages` are not a well-formed BCP 47 language tag. * * @returns {Intl.Locale} The first locale that resolved from `accept-language` header string, first language tag is used. if `*` (any language) or empty string is detected, return `en-US`. */