Skip to content

Commit

Permalink
feat: support try locale getting APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Dec 6, 2023
1 parent 1ec90a5 commit eb98e62
Show file tree
Hide file tree
Showing 11 changed files with 1,108 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ You can do `import { ... } from '@intlify/utils'` the above utilities
- `setCookieLocale`
- `getPathLocale`
- `getQueryLocale`
- `tryHeaderLocales`
- `tryHeaderLocale`
- `tryCookieLocale`
- `tryPathLocale`
- `tryQueryLocale`

The about utilies functions accpet Web APIs such as [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) that is supported by JS environments (such as Deno, Bun, and Browser)

Expand Down
Binary file modified bun.lockb
Binary file not shown.
120 changes: 120 additions & 0 deletions deno/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export function getHeaderLanguage(
* @param {HeaderOptions['name']} options.name The header name, which is as default `accept-language`.
* @param {HeaderOptions['parser']} options.parser The parser function, which is as default {@link parseDefaultHeader}. If you are specifying more than one in your own format, you need a parser.
*
* @throws {RangeError} Throws the {@link RangeError} if header are not a well-formed BCP 47 language tag.
*
* @returns {Array<Intl.Locale>} The locales that wrapped from header, if you use `accept-language` header and `*` (any language) or empty string is detected, return an empty array.
*/
export function getHeaderLocales(
Expand All @@ -126,6 +128,31 @@ export function getHeaderLocales(
})
}

/**
* try to get locales from header
*
* @description wrap language tags with {@link Intl.Locale | locale}, languages tags will be parsed from `accept-language` header as default. Unlike {@link getHeaderLocales}, this function does not throw an error if the locale cannot be obtained, this function returns `null`.
*
* @param {Request} request The {@link Request | request}
* @param {HeaderOptions['name']} options.name The header name, which is as default `accept-language`.
* @param {HeaderOptions['parser']} options.parser The parser function, which is as default {@link parseDefaultHeader}. If you are specifying more than one in your own format, you need a parser.
*
* @returns {Array<Intl.Locale> | null} The locales that wrapped from header, if you use `accept-language` header and `*` (any language) or empty string is detected, return an empty array. if header are not a well-formed BCP 47 language tag, return `null`.
*/
export function tryHeaderLocales(
request: Request,
{
name = ACCEPT_LANGUAGE_HEADER,
parser = parseDefaultHeader,
}: HeaderOptions = {},
): Intl.Locale[] | null {
try {
return getHeaderLocales(request, { name, parser })
} catch {
return null
}
}

/**
* get locale from header
*
Expand Down Expand Up @@ -165,6 +192,33 @@ export function getHeaderLocale(
return getLocaleWithGetter(() => getHeaderLanguages(request, { name, parser })[0] || lang)
}

/**
* try to get locale from header
*
* @description wrap language tag with {@link Intl.Locale | locale}, languages tags will be parsed from `accept-language` header as default. Unlike {@link getHeaderLocale}, this function does not throw an error if the locale cannot be obtained, this function returns `null`.
*
* @param {Request} request The {@link Request | request}
* @param {string} options.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}.
* @param {HeaderOptions['name']} options.name The header name, which is as default `accept-language`.
* @param {HeaderOptions['parser']} options.parser The parser function, which is as default {@link parseDefaultHeader}. If you are specifying more than one in your own format, you need a parser.
*
* @returns {Intl.Locale | null} The first locale that resolved from header string. if you use `accept-language` header and `*` (any language) or empty string is detected, return `en-US`. if `lang` option or header are not a well-formed BCP 47 language tag, return `null`.
*/
export function tryHeaderLocale(
request: Request,
{
lang = DEFAULT_LANG_TAG,
name = ACCEPT_LANGUAGE_HEADER,
parser = parseDefaultHeader,
}: HeaderOptions & { lang?: string } = {},
): Intl.Locale | null {
try {
return getHeaderLocale(request, { lang, name, parser })
} catch {
return null
}
}

/**
* get locale from cookie
*
Expand Down Expand Up @@ -203,6 +257,28 @@ export function getCookieLocale(
return getLocaleWithGetter(getter)
}

/**
* try to get locale from cookie
*
* @description Unlike {@link getCookieLocale}, this function does not throw an error if the locale cannot be obtained, this function returns `null`.
*
* @param {Request} request The {@link Request | request}
* @param {string} options.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} options.name The cookie name, default is `i18n_locale`
*
* @returns {Intl.Locale | null} The locale that resolved from cookie. if `lang` option or cookie name value are not a well-formed BCP 47 language tag, return `null`.
*/
export function tryCookieLocale(
request: Request,
{ lang = DEFAULT_LANG_TAG, name = DEFAULT_COOKIE_NAME } = {},
): Intl.Locale | null {
try {
return getCookieLocale(request, { lang, name })
} catch {
return null
}
}

/**
* set locale to the response `Set-Cookie` header.
*
Expand Down Expand Up @@ -264,6 +340,28 @@ export function getPathLocale(
return _getPathLocale(new URL(request.url), { lang, parser })
}

/**
* try to get the locale from the path
*
* @description Unlike {@link getPathLocale}, this function does not throw an error if the locale cannot be obtained, this function returns `null`.
*
* @param {Request} request the {@link Request | request}
* @param {PathOptions['lang']} options.lang the language tag, which is as default `'en-US'`. optional
* @param {PathOptions['parser']} options.parser the path language parser, default {@link pathLanguageParser}, optional
*
* @returns {Intl.Locale | null} The locale that resolved from path. if the language in the path, that is not a well-formed BCP 47 language tag, return `null`.
*/
export function tryPathLocale(
request: Request,
{ lang = DEFAULT_LANG_TAG, parser = pathLanguageParser }: PathOptions = {},
): Intl.Locale | null {
try {
return getPathLocale(request, { lang, parser })
} catch {
return null
}
}

/**
* get the locale from the query
*
Expand All @@ -282,6 +380,28 @@ export function getQueryLocale(
return _getQueryLocale(new URL(request.url), { lang, name })
}

/**
* try to get the locale from the query
*
* @description Unlike {@link getQueryLocale}, this function does not throw an error if the locale cannot be obtained, this function returns `null`.
*
* @param {Request} request the {@link Request | request}
* @param {QueryOptions['lang']} options.lang the language tag, which is as default `'en-US'`. optional
* @param {QueryOptions['name']} options.name the query param name, default `'locale'`. optional
*
* @returns {Intl.Locale | null} The locale that resolved from query. if the language in the query, that is not a well-formed BCP 47 language tag, return `null`.
*/
export function tryQueryLocale(
request: Request,
{ lang = DEFAULT_LANG_TAG, name = 'locale' }: QueryOptions = {},
): Intl.Locale | null {
try {
return getQueryLocale(request, { lang, name })
} catch {
return null
}
}

/**
* get navigator languages
*
Expand Down
166 changes: 166 additions & 0 deletions src/h3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import {
getPathLocale,
getQueryLocale,
setCookieLocale,
tryCookieLocale,
tryHeaderLocale,
tryHeaderLocales,
tryPathLocale,
tryQueryLocale,
} from './h3.ts'
import { parseAcceptLanguage } from './shared.ts'
import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts'
Expand Down Expand Up @@ -219,6 +224,37 @@ describe('getHeaderLocales', () => {
})
})

describe('tryHeaderLocales', () => {
test('success', () => {
const mockEvent = {
node: {
req: {
method: 'GET',
headers: {
'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
},
},
},
} as H3Event
expect(tryHeaderLocales(mockEvent)!.map((locale) => locale.baseName))
.toEqual(['en-US', 'en', 'ja'])
})

test('failed', () => {
const mockEvent = {
node: {
req: {
method: 'GET',
headers: {
'accept-language': 'hoge',
},
},
},
} as H3Event
expect(tryHeaderLocales(mockEvent)).toBeNull()
})
})

describe('getHeaderLocale', () => {
test('basic', () => {
const mockEvent = {
Expand Down Expand Up @@ -308,6 +344,41 @@ describe('getHeaderLocale', () => {
})
})

describe('tryHeaderLocale', () => {
test('success', () => {
const mockEvent = {
node: {
req: {
method: 'GET',
headers: {
'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
},
},
},
} as H3Event
const locale = tryHeaderLocale(mockEvent)!

expect(locale.baseName).toEqual('en-US')
expect(locale.language).toEqual('en')
expect(locale.region).toEqual('US')
})

test('failed', () => {
const mockEvent = {
node: {
req: {
method: 'GET',
headers: {
'accept-language': 's',
},
},
},
} as H3Event

expect(tryHeaderLocale(mockEvent)).toBeNull()
})
})

describe('getCookieLocale', () => {
test('basic', () => {
const mockEvent = {
Expand Down Expand Up @@ -388,6 +459,41 @@ describe('getCookieLocale', () => {
})
})

describe('tryCookieLocale', () => {
test('success', () => {
const mockEvent = {
node: {
req: {
method: 'GET',
headers: {
cookie: `${DEFAULT_COOKIE_NAME}=en-US`,
},
},
},
} as H3Event
const locale = tryCookieLocale(mockEvent)!

expect(locale.baseName).toEqual('en-US')
expect(locale.language).toEqual('en')
expect(locale.region).toEqual('US')
})

test('failed', () => {
const mockEvent = {
node: {
req: {
method: 'GET',
headers: {
cookie: 'intlify_locale=f',
},
},
},
} as H3Event

expect(tryCookieLocale(mockEvent, { name: 'intlify_locale' })).toBeNull()
})
})

describe('setCookieLocale', () => {
let app: App
let request: SuperTest<Test>
Expand Down Expand Up @@ -469,6 +575,36 @@ test('getPathLocale', async () => {
expect(res.body).toEqual({ locale: 'en' })
})

describe('tryPathLocale', () => {
test('success', async () => {
const app = createApp({ debug: false })
const request = supertest(toNodeListener(app))

app.use(
'/',
eventHandler((event) => {
return { locale: tryPathLocale(event)!.toString() }
}),
)
const res = await request.get('/en/foo')
expect(res.body).toEqual({ locale: 'en' })
})

test('failed', async () => {
const app = createApp({ debug: false })
const request = supertest(toNodeListener(app))

app.use(
'/',
eventHandler((event) => {
return { locale: tryPathLocale(event) }
}),
)
const res = await request.get('/s/foo')
expect(res.body).toEqual({ locale: null })
})
})

test('getQueryLocale', async () => {
const app = createApp({ debug: false })
const request = supertest(toNodeListener(app))
Expand All @@ -482,3 +618,33 @@ test('getQueryLocale', async () => {
const res = await request.get('/?locale=ja')
expect(res.body).toEqual({ locale: 'ja' })
})

describe('tryQueryLocale', () => {
test('success', async () => {
const app = createApp({ debug: false })
const request = supertest(toNodeListener(app))

app.use(
'/',
eventHandler((event) => {
return { locale: tryQueryLocale(event)!.toString() }
}),
)
const res = await request.get('/?locale=ja')
expect(res.body).toEqual({ locale: 'ja' })
})

test('failed', async () => {
const app = createApp({ debug: false })
const request = supertest(toNodeListener(app))

app.use(
'/',
eventHandler((event) => {
return { locale: tryQueryLocale(event) }
}),
)
const res = await request.get('/?locale=j')
expect(res.body).toEqual({ locale: null })
})
})
Loading

0 comments on commit eb98e62

Please sign in to comment.