From c68bd9ce61a2edad8929850b0ae89d52dd5f53dc Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 27 Sep 2023 18:09:31 +0900 Subject: [PATCH 1/2] feat: support `getPathLanguage` and `getPathLocale` --- README.md | 2 ++ src/h3.ts | 2 +- src/http.test.ts | 30 ++++++++++++++++++++++++++++++ src/http.ts | 42 ++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 10 +++++++++- src/node.ts | 2 +- src/shared.test.ts | 17 +++++++++++++++++ src/shared.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/web.ts | 2 +- 9 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 src/http.test.ts diff --git a/README.md b/README.md index 9c53fb9..3b1ba87 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,8 @@ You can do `import { ... } from '@intlify/utils'` the above utilities - `getAcceptLocale` - `getCookieLocale` - `setCookieLocale` +- `getPathLanguage` +- `getPathLocale` The about utilies functions accpet Web APIs such as [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and diff --git a/src/h3.ts b/src/h3.ts index 128ac60..4413e43 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -155,7 +155,7 @@ export function getAcceptLocale( * * @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 + * @returns {Intl.Locale} The locale that resolved from cookie */ export function getCookieLocale( event: H3Event, diff --git a/src/http.test.ts b/src/http.test.ts new file mode 100644 index 0000000..691e143 --- /dev/null +++ b/src/http.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'vitest' +import { getPathLanguage, getPathLocale } from './http.ts' + +describe('getPathLanguage', () => { + test('basic', () => { + expect(getPathLanguage('/en/foo')).toBe('en') + }) + + test('parser option', () => { + const nullLangParser = { + parse: () => 'null', + } + expect(getPathLanguage('/en/foo', nullLangParser)).toBe('null') + }) +}) + +describe('getPathLocale', () => { + test('basic', () => { + expect(getPathLocale('/en-US/foo').toString()).toBe('en-US') + }) + + test('RangeError', () => { + const nullLangParser = { + parse: () => 'null', + } + expect(() => getPathLocale('/en/foo', nullLangParser)).toThrowError( + RangeError, + ) + }) +}) diff --git a/src/http.ts b/src/http.ts index 8101982..abd9adf 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,6 +1,11 @@ -import { parseAcceptLanguage } from './shared.ts' -import { isLocale, validateLanguageTag } from './shared.ts' +import { + isLocale, + parseAcceptLanguage, + pathLanguageParser, + validateLanguageTag, +} from './shared.ts' +import type { PathLanguageParser } from './shared.ts' // import type { CookieSerializeOptions } from 'cookie-es' // NOTE: This is a copy of the type definition from `cookie-es` package, we want to avoid building error for this type definition ... @@ -146,3 +151,36 @@ export function getExistCookies( ) return setCookies as string[] } + +/** + * get the language from the path + * + * @param {string} path the target path + * @param {PathLanguageParser} parser the path language parser, optional + * + * @returns {string} the language that is parsed by the path language parser + */ +export function getPathLanguage( + path: string, + parser?: PathLanguageParser, +): string { + const _parser = parser || pathLanguageParser + return _parser.parse(path) +} + +/** + * get the locale from the path + * + * @param {string} path the target path + * @param {PathLanguageParser} parser the path language parser, optional + * + * @throws {RangeError} Throws the {@link RangeError} if the language in the path, that is not a well-formed BCP 47 language tag. + * + * @returns {Intl.Locale} The locale that resolved from path + */ +export function getPathLocale( + path: string, + parser?: PathLanguageParser, +): Intl.Locale { + return new Intl.Locale(getPathLanguage(path, parser)) +} diff --git a/src/index.ts b/src/index.ts index bd768da..26e7f73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,10 @@ -export * from './shared.ts' +export { + createPathIndexLanguageParser, + isLocale, + normalizeLanguageName, + parseAcceptLanguage, + registerPathLanguageParser, + validateLanguageTag, +} from './shared.ts' +export { getPathLanguage, getPathLocale } from './http.ts' export * from './web.ts' diff --git a/src/node.ts b/src/node.ts index 298f5ad..cab3c59 100644 --- a/src/node.ts +++ b/src/node.ts @@ -156,7 +156,7 @@ export function getAcceptLocale( * * @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 + * @returns {Intl.Locale} The locale that resolved from cookie */ export function getCookieLocale( request: IncomingMessage, diff --git a/src/shared.test.ts b/src/shared.test.ts index f2a7348..00cb349 100644 --- a/src/shared.test.ts +++ b/src/shared.test.ts @@ -1,8 +1,10 @@ import { describe, expect, test } from 'vitest' import { + createPathIndexLanguageParser, isLocale, normalizeLanguageName, parseAcceptLanguage, + pathLanguageParser, validateLanguageTag, } from './shared.ts' @@ -74,3 +76,18 @@ describe('normalizeLanguageName', () => { expect(normalizeLanguageName('')).toBe('') }) }) + +describe('PathIndexLanguageParser', () => { + test('default index: 0', () => { + expect(pathLanguageParser.parse('/en/hello')).toEqual('en') + }) + + test('index 1', () => { + const parser = createPathIndexLanguageParser(1) + expect(parser.parse('/hello/ja/bar')).toEqual('ja') + }) + + test('empty', () => { + expect(pathLanguageParser.parse('/')).toEqual('') + }) +}) diff --git a/src/shared.ts b/src/shared.ts index ff41a2e..01bdd93 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -63,3 +63,48 @@ export function normalizeLanguageName(langName: string): string { const [lang] = langName.split('.') return lang.replace(/_/g, '-') } + +/** + * path language parser + */ +export interface PathLanguageParser { + /** + * parse the path that is include language + * + * @param {string} path the target path + * + * @returns {string} the language, if it cannot parse the path is not found, you need to return empty string (`''`) + */ + parse(path: string): string +} + +export function createPathIndexLanguageParser( + index = 0, +): PathLanguageParser { + return { + parse(path: string): string { + const normalizedPath = path.split('?')[0] + const parts = normalizedPath.split('/') + if (parts[0] === '') { + parts.shift() + } + return parts.length > index ? parts[index] || '' : '' + }, + } +} + +export let pathLanguageParser: PathLanguageParser = + /* #__PURE__*/ createPathIndexLanguageParser() + +/** + * register the path language parser + * + * @description register a parser to be used in the `getPathLanugage` utility function + * + * @param {PathLanguageParser} parser the path language parser + */ +export function registerPathLanguageParser( + parser: PathLanguageParser, +): void { + pathLanguageParser = parser +} diff --git a/src/web.ts b/src/web.ts index 172ad85..eb6b672 100644 --- a/src/web.ts +++ b/src/web.ts @@ -154,7 +154,7 @@ export function getAcceptLocale( * * @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 + * @returns {Intl.Locale} The locale that resolved from cookie */ export function getCookieLocale( request: Request, From c6517dc3348f86ca2bd48876da06e6a7adb8ffb6 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 27 Sep 2023 18:28:07 +0900 Subject: [PATCH 2/2] add URL instance supporting --- src/http.test.ts | 10 ++++++++++ src/http.ts | 8 ++++---- src/shared.ts | 9 +++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/http.test.ts b/src/http.test.ts index 691e143..7c1e428 100644 --- a/src/http.test.ts +++ b/src/http.test.ts @@ -6,6 +6,11 @@ describe('getPathLanguage', () => { expect(getPathLanguage('/en/foo')).toBe('en') }) + test('URL instance', () => { + const url = new URL('https://example.com/en/foo') + expect(getPathLanguage(url)).toBe('en') + }) + test('parser option', () => { const nullLangParser = { parse: () => 'null', @@ -19,6 +24,11 @@ describe('getPathLocale', () => { expect(getPathLocale('/en-US/foo').toString()).toBe('en-US') }) + test('URL instance', () => { + const url = new URL('https://example.com/ja-JP/foo') + expect(getPathLocale(url).toString()).toBe('ja-JP') + }) + test('RangeError', () => { const nullLangParser = { parse: () => 'null', diff --git a/src/http.ts b/src/http.ts index abd9adf..e73561d 100644 --- a/src/http.ts +++ b/src/http.ts @@ -155,13 +155,13 @@ export function getExistCookies( /** * get the language from the path * - * @param {string} path the target path + * @param {string | URL} path the target path * @param {PathLanguageParser} parser the path language parser, optional * * @returns {string} the language that is parsed by the path language parser */ export function getPathLanguage( - path: string, + path: string | URL, parser?: PathLanguageParser, ): string { const _parser = parser || pathLanguageParser @@ -171,7 +171,7 @@ export function getPathLanguage( /** * get the locale from the path * - * @param {string} path the target path + * @param {string | URL} path the target path * @param {PathLanguageParser} parser the path language parser, optional * * @throws {RangeError} Throws the {@link RangeError} if the language in the path, that is not a well-formed BCP 47 language tag. @@ -179,7 +179,7 @@ export function getPathLanguage( * @returns {Intl.Locale} The locale that resolved from path */ export function getPathLocale( - path: string, + path: string | URL, parser?: PathLanguageParser, ): Intl.Locale { return new Intl.Locale(getPathLanguage(path, parser)) diff --git a/src/shared.ts b/src/shared.ts index 01bdd93..75f70a7 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -71,19 +71,20 @@ export interface PathLanguageParser { /** * parse the path that is include language * - * @param {string} path the target path + * @param {string | URL} path the target path * * @returns {string} the language, if it cannot parse the path is not found, you need to return empty string (`''`) */ - parse(path: string): string + parse(path: string | URL): string } export function createPathIndexLanguageParser( index = 0, ): PathLanguageParser { return { - parse(path: string): string { - const normalizedPath = path.split('?')[0] + parse(path: string | URL): string { + const rawPath = typeof path === 'string' ? path : path.pathname + const normalizedPath = rawPath.split('?')[0] const parts = normalizedPath.split('/') if (parts[0] === '') { parts.shift()