Skip to content

Commit

Permalink
feat: add some locale getting APIs from path and query for platform (#28
Browse files Browse the repository at this point in the history
)
  • Loading branch information
kazupon committed Oct 17, 2023
1 parent fb9512c commit c18bb8c
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 6 deletions.
30 changes: 30 additions & 0 deletions src/h3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getHeaderLanguages,
getHeaderLocale,
getHeaderLocales,
getPathLocale,
getQueryLocale,
setCookieLocale,
} from './h3.ts'
import { parseAcceptLanguage } from './shared.ts'
Expand Down Expand Up @@ -452,3 +454,31 @@ describe('setCookieLocale', () => {
.toThrowError(/locale is invalid: j/)
})
})

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

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

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

app.use(
'/',
eventHandler((event) => {
return { locale: getQueryLocale(event).toString() }
}),
)
const res = await request.get('/?locale=ja')
expect(res.body).toEqual({ locale: 'ja' })
})
48 changes: 46 additions & 2 deletions src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ import {
import {
getHeaderLanguagesWithGetter,
getLocaleWithGetter,
getPathLocale as _getPathLocale,
getQueryLocale as _getQueryLocale,
mapToLocaleFromLanguageTag,
parseDefaultHeader,
validateLocale,
} from './http.ts'
import { getCookie, getHeaders, setCookie } from 'h3'
import { pathLanguageParser } from './shared.ts'
import { getCookie, getHeaders, getRequestURL, setCookie } from 'h3'

import type { H3Event } from 'h3'
import type { CookieOptions, HeaderOptions } from './http.ts'
import type {
CookieOptions,
HeaderOptions,
PathOptions,
QueryOptions,
} from './http.ts'

/**
* get languages from header
Expand Down Expand Up @@ -223,3 +231,39 @@ export function setCookieLocale(
validateLocale(locale)
setCookie(event, options.name!, locale.toString(), options)
}

/**
* get the locale from the path
*
* @param {H3Event} event the {@link H3Event | H3} event
* @param {PathOptions['lang']} options.lang the language tag, which is as default `'en-US'`. optional
* @param {PathOptions['parser']} options.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(
event: H3Event,
{ lang = DEFAULT_LANG_TAG, parser = pathLanguageParser }: PathOptions = {},
): Intl.Locale {
return _getPathLocale(getRequestURL(event), { lang, parser })
}

/**
* get the locale from the query
*
* @param {H3Event} event the {@link H3Event | H3} event
* @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
*
* @throws {RangeError} Throws the {@link RangeError} if the language in the query, that is not a well-formed BCP 47 language tag.
*
* @returns {Intl.Locale} The locale that resolved from query
*/
export function getQueryLocale(
event: H3Event,
{ lang = DEFAULT_LANG_TAG, name = 'locale' }: QueryOptions = {},
): Intl.Locale {
return _getQueryLocale(getRequestURL(event), { lang, name })
}
24 changes: 24 additions & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
getHeaderLanguages,
getHeaderLocale,
getHeaderLocales,
getPathLocale,
getQueryLocale,
setCookieLocale,
} from './node.ts'
import { createServer, IncomingMessage, OutgoingMessage } from 'node:http'
Expand Down Expand Up @@ -304,3 +306,25 @@ describe('setCookieLocale', () => {
.toThrowError(/locale is invalid: j/)
})
})

test('getPathLocale', async () => {
const server = createServer((req, res) => {
const locale = getPathLocale(req)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ locale: locale.toString() }))
})
const request = supertest(server)
const result = await request.get('/en-US/foo')
expect(result.body).toEqual({ locale: 'en-US' })
})

test('getQueryLocale', async () => {
const server = createServer((req, res) => {
const locale = getQueryLocale(req, { name: 'lang' })
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ locale: locale.toString() }))
})
const request = supertest(server)
const result = await request.get('/?lang=ja')
expect(result.body).toEqual({ locale: 'ja' })
})
90 changes: 88 additions & 2 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
getExistCookies,
getHeaderLanguagesWithGetter,
getLocaleWithGetter,
getPathLocale as _getPathLocale,
getQueryLocale as _getQueryLocale,
mapToLocaleFromLanguageTag,
parseDefaultHeader,
validateLocale,
Expand All @@ -13,9 +15,14 @@ import {
DEFAULT_COOKIE_NAME,
DEFAULT_LANG_TAG,
} from './constants.ts'
import { normalizeLanguageName } from './shared.ts'
import { normalizeLanguageName, pathLanguageParser } from './shared.ts'

import type { CookieOptions, HeaderOptions } from './http.ts'
import type {
CookieOptions,
HeaderOptions,
PathOptions,
QueryOptions,
} from './http.ts'

/**
* get languages from header
Expand Down Expand Up @@ -245,6 +252,85 @@ export function setCookieLocale(
response.setHeader('set-cookie', [...setCookies, target])
}

function getRequestProtocol(
request: IncomingMessage,
opts: { xForwardedProto?: boolean } = {},
) {
if (
opts.xForwardedProto !== false &&
request.headers['x-forwarded-proto'] === 'https'
) {
return 'https'
}
// deno-lint-ignore no-explicit-any
return (request.socket as any).encrypted ? 'https' : 'http'
}

function getRequestHost(
request: IncomingMessage,
opts: { xForwardedHost?: boolean } = {},
) {
if (opts.xForwardedHost) {
const xForwardedHost = request.headers['x-forwarded-host'] as string
if (xForwardedHost) {
return xForwardedHost
}
}
return request.headers.host || 'localhost'
}

function getPath(request: IncomingMessage) {
// deno-lint-ignore no-explicit-any
const raw = (request as any).originalUrl || request.url || '/'
return raw.replace(
/^[/\\]+/g,
'/',
)
}

function getURL(request: IncomingMessage): URL {
const protocol = getRequestProtocol(request)
const host = getRequestHost(request)
const path = getPath(request)
return new URL(path, `${protocol}://${host}`)
}

/**
* get the locale from the path
*
* @param {IncomingMessage} request the {@link IncomingMessage | 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, 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(
request: IncomingMessage,
{ lang = DEFAULT_LANG_TAG, parser = pathLanguageParser }: PathOptions = {},
): Intl.Locale {
return _getPathLocale(getURL(request), { lang, parser })
}

/**
* get the locale from the query
*
* @param {IncomingMessage} request the {@link IncomingMessage | 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
*
* @throws {RangeError} Throws the {@link RangeError} if the language in the query, that is not a well-formed BCP 47 language tag.
*
* @returns {Intl.Locale} The locale that resolved from query
*/
export function getQueryLocale(
request: IncomingMessage,
{ lang = DEFAULT_LANG_TAG, name = 'locale' }: QueryOptions = {},
): Intl.Locale {
return _getQueryLocale(getURL(request), { lang, name })
}

let navigatorLanguages: string[] | undefined

/**
Expand Down
14 changes: 14 additions & 0 deletions src/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getHeaderLocales,
getNavigatorLocale,
getNavigatorLocales,
getPathLocale,
getQueryLocale,
setCookieLocale,
} from './web.ts'
import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts'
Expand Down Expand Up @@ -222,6 +224,18 @@ describe('setCookieLocale', () => {
})
})

test('getPathLocale', () => {
const mockRequest = new Request('https://locahost:3000/en/foo')
const locale = getPathLocale(mockRequest)
expect(locale.toString()).toEqual('en')
})

test('getQueryLocale', () => {
const mockRequest = new Request('https://locahost:3000/?intlify=ja')
const locale = getQueryLocale(mockRequest, { name: 'intlify' })
expect(locale.toString()).toEqual('ja')
})

describe('getNavigatorLocales', () => {
test('basic', () => {
vi.stubGlobal('navigator', {
Expand Down
48 changes: 46 additions & 2 deletions src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@ import {
getExistCookies,
getHeaderLanguagesWithGetter,
getLocaleWithGetter,
getPathLocale as _getPathLocale,
getQueryLocale as _getQueryLocale,
mapToLocaleFromLanguageTag,
parseDefaultHeader,
validateLocale,
} from './http.ts'
import { pathLanguageParser } from './shared.ts'
import {
ACCEPT_LANGUAGE_HEADER,
DEFAULT_COOKIE_NAME,
DEFAULT_LANG_TAG,
} from './constants.ts'

import type { CookieOptions, HeaderOptions } from './http.ts'
import type {
CookieOptions,
HeaderOptions,
PathOptions,
QueryOptions,
} from './http.ts'

/**
* get languages from header
Expand Down Expand Up @@ -186,7 +194,7 @@ export function getHeaderLocale(
* })
* ```
*
* @param {IncomingMessage} request The {@link IncomingMessage | request}
* @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`
*
Expand Down Expand Up @@ -249,6 +257,42 @@ export function setCookieLocale(
response.headers.set('set-cookie', [...setCookies, target].join('; '))
}

/**
* get the locale from the path
*
* @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, 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(
request: Request,
{ lang = DEFAULT_LANG_TAG, parser = pathLanguageParser }: PathOptions = {},
): Intl.Locale {
return _getPathLocale(new URL(request.url), { lang, parser })
}

/**
* get the locale from the query
*
* @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
*
* @throws {RangeError} Throws the {@link RangeError} if the language in the query, that is not a well-formed BCP 47 language tag.
*
* @returns {Intl.Locale} The locale that resolved from query
*/
export function getQueryLocale(
request: Request,
{ lang = DEFAULT_LANG_TAG, name = 'locale' }: QueryOptions = {},
): Intl.Locale {
return _getQueryLocale(new URL(request.url), { lang, name })
}

/**
* get navigator languages
*
Expand Down

0 comments on commit c18bb8c

Please sign in to comment.