Skip to content

Commit

Permalink
feat: add getNavigatorLanguages, getNavigatorLanguage and normalizeLa…
Browse files Browse the repository at this point in the history
…nguageName
  • Loading branch information
kazupon committed Sep 22, 2023
1 parent 3803ffb commit eadb477
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 7 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,23 @@ You can play the below examples:
- `isLocale`
- `parseAcceptLanguage`
- `validateLanguageTag`
- `normalizeLanguageName`

You can do `import { ... } from '@intlify/utils'` the above utilities

### Navigator

- `getNavigatorLanguages`
- `getNavigatorLanguage`

You can do `import { ... } from '@intlify/utils/{ENV}'` the above utilities.

The namespace `{ENV}` is one of the following:

- `node`: Node.js
- `web`: JS environments (such as Deno, Bun, and Browser) supporting Web APIs
(`navigator.language(s)`)

### HTTP

- `getAcceptLanguages`
Expand All @@ -148,10 +162,6 @@ The namespace `{ENV}` is one of the following:
and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
- `h3`: HTTP framework [h3](https://github.com/unjs/h3)

### Browser

TODO: WIP

## 🙌 Contributing guidelines

If you are interested in contributing to `@intlify/utils`, I highly recommend
Expand Down
8 changes: 8 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare namespace NodeJS {
interface ProcessEnv {
LC_ALL?: string
LC_MESSAGES?: string
LANG?: string
LANGUAGE?: string
}
}
50 changes: 49 additions & 1 deletion src/node.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { describe, expect, test } from 'vitest'
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
import supertest from 'supertest'
import {
getAcceptLanguage,
getAcceptLanguages,
getAcceptLocale,
getAcceptLocales,
getCookieLocale,
getNavigatorLanguage,
getNavigatorLanguages,
setCookieLocale,
} from './node.ts'
import { createServer, IncomingMessage, OutgoingMessage } from 'node:http'
Expand Down Expand Up @@ -244,3 +246,49 @@ describe('setCookieLocale', () => {
.toThrowError(/locale is invalid: j/)
})
})

describe('getNavigatorLanguages', () => {
let orgEnv = {}
beforeEach(() => {
orgEnv = process.env
})
afterEach(() => {
process.env = orgEnv
})

test('basic', () => {
process.env.LC_ALL = 'en-GB'
process.env.LC_MESSAGES = 'en-US'
process.env.LANG = 'ja-JP'
process.env.LANGUAGE = 'en'

const values = [
'en-GB',
'en-US',
'ja-JP',
'en',
]
expect(getNavigatorLanguages()).toEqual(values)
expect(getNavigatorLanguages()).toEqual(values)
})
})

describe('getNavigatorLanguage', () => {
let orgEnv = {}
beforeEach(() => {
orgEnv = process.env
})
afterEach(() => {
process.env = orgEnv
})

test('basic', () => {
process.env.LC_ALL = 'en-GB'
process.env.LC_MESSAGES = 'en-US'
process.env.LANG = 'ja-JP'
process.env.LANGUAGE = 'en'

expect(getNavigatorLanguage()).toEqual('en-GB')
expect(getNavigatorLanguage()).toEqual('en-GB')
})
})
42 changes: 42 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mapToLocaleFromLanguageTag,
validateLocale,
} from './http.ts'
import { normalizeLanguageName } from './shared.ts'
import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts'

import type { CookieOptions } from './http.ts'
Expand Down Expand Up @@ -207,3 +208,44 @@ export function setCookieLocale(
})
response.setHeader('set-cookie', [...setCookies, target])
}

let navigatorLanguages: string[] | undefined

/**
* get navigator languages
*
* @description
* You can get the language tags from system environment variables.
*
* @returns {Array<string>} {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tags}, if you can't get the language tag, return an empty array.
*/
export function getNavigatorLanguages(): readonly string[] {
if (navigatorLanguages) {
return navigatorLanguages
}

const env = process.env
const langs = new Set<string>()

env.LC_ALL && langs.add(normalizeLanguageName(env.LC_ALL))
env.LC_MESSAGES && langs.add(normalizeLanguageName(env.LC_MESSAGES))
env.LANG && langs.add(normalizeLanguageName(env.LANG))
env.LANGUAGE && langs.add(normalizeLanguageName(env.LANGUAGE))

return navigatorLanguages = [...langs].filter(Boolean)
}

let navigatorLanguage: string | undefined

/**
* get navigator languages
*
* @description
* You can get the language tag from system environment variables.
*
* @returns {string} {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tag}, if you can't get the language tag, return a enmpty string.
*/
export function getNavigatorLanguage(): string {
return navigatorLanguage ||
(navigatorLanguage = getNavigatorLanguages()[0] || '')
}
25 changes: 24 additions & 1 deletion src/shared.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, test } from 'vitest'
import { isLocale, parseAcceptLanguage, validateLanguageTag } from './shared.ts'
import {
isLocale,
normalizeLanguageName,
parseAcceptLanguage,
validateLanguageTag,
} from './shared.ts'

describe('isLocale', () => {
test('Locale instance', () => {
Expand Down Expand Up @@ -51,3 +56,21 @@ describe('validateLanguageTag', () => {
expect(validateLanguageTag('j')).toBe(false)
})
})

describe('normalizeLanguageName', () => {
test('basic: en_US', () => {
expect(normalizeLanguageName('en_US')).toBe('en-US')
})

test('language only: en', () => {
expect(normalizeLanguageName('en')).toBe('en')
})

test('has encoding: en_US.UTF-8', () => {
expect(normalizeLanguageName('en_US.UTF-8')).toBe('en-US')
})

test('empty', () => {
expect(normalizeLanguageName('')).toBe('')
})
})
22 changes: 22 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,25 @@ export function validateLanguageTag(lang: string): boolean {
return false
}
}

/**
* nomralize the language name
*
* @description
* This function normalizes the locale name defined in {@link https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names | gettext(libc) style} to {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tag}
*
* @example
* ```ts
* const oldLangName = 'en_US'
* const langTag = nomralizeLanguageName(oldLangName)
* conosle.log(langTag) // en-US
* ```
*
* @param langName The target language name
*
* @returns {string} The normalized language tag
*/
export function normalizeLanguageName(langName: string): string {
const [lang] = langName.split('.')
return lang.replace(/_/g, '-')
}
40 changes: 39 additions & 1 deletion src/web.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { describe, expect, test } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import {
getAcceptLanguage,
getAcceptLanguages,
getAcceptLocale,
getAcceptLocales,
getCookieLocale,
getNavigatorLanguage,
getNavigatorLanguages,
setCookieLocale,
} from './web.ts'
import { DEFAULT_COOKIE_NAME, DEFAULT_LANG_TAG } from './constants.ts'
Expand Down Expand Up @@ -175,3 +177,39 @@ describe('setCookieLocale', () => {
.toThrowError(/locale is invalid: j/)
})
})

describe('getNavigatorLanguages', () => {
test('basic', () => {
vi.stubGlobal('navigator', {
languages: ['en-US', 'en', 'ja'],
})

expect(getNavigatorLanguages()).toEqual(['en-US', 'en', 'ja'])
})

test('error', () => {
vi.stubGlobal('navigator', undefined)

expect(() => getNavigatorLanguages()).toThrowError(
/not support `navigator`/,
)
})
})

describe('getNavigatorLanguage', () => {
test('basic', () => {
vi.stubGlobal('navigator', {
language: 'en-US',
})

expect(getNavigatorLanguage()).toEqual('en-US')
})

test('error', () => {
vi.stubGlobal('navigator', undefined)

expect(() => getNavigatorLanguage()).toThrowError(
/not support `navigator`/,
)
})
})
34 changes: 34 additions & 0 deletions src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,37 @@ export function setCookieLocale(
})
response.headers.set('set-cookie', [...setCookies, target].join('; '))
}

/**
* get navigator languages
*
* @description
* The value depends on the environments. if you use this function on the browser, you can get the languages, that are set in the browser, else if you use this function on the server side (Deno only), that value is the languages set in the server.
*
* @throws Throws the {@link Error} if the `navigator` is not exists.
*
* @returns {Array<string>} {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tags}
*/
export function getNavigatorLanguages(): readonly string[] {
if (typeof navigator === 'undefined') {
throw new Error('not support `navigator`')
}
return navigator.languages
}

/**
* get navigator language
*
* @description
* The value depends on the environments. if you use this function on the browser, you can get the languages, that are set in the browser, else if you use this function on the server side (Deno only), that value is the language set in the server.
*
* @throws Throws the {@link Error} if the `navigator` is not exists.
*
* @returns {string} {@link https://datatracker.ietf.org/doc/html/rfc4646#section-2.1 | BCP 47 language tag}
*/
export function getNavigatorLanguage(): string {
if (typeof navigator === 'undefined') {
throw new Error('not support `navigator`')
}
return navigator.language
}

0 comments on commit eadb477

Please sign in to comment.