Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getNavigatorLanguages, getNavigatorLanguage and normalizeLanguageName #8

Merged
merged 1 commit into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}