diff --git a/README.md b/README.md index c9acea0..777c33e 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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 diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..c8f7bad --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,8 @@ +declare namespace NodeJS { + interface ProcessEnv { + LC_ALL?: string + LC_MESSAGES?: string + LANG?: string + LANGUAGE?: string + } +} diff --git a/src/node.test.ts b/src/node.test.ts index 769fe0a..25f0180 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'vitest' +import { afterEach, beforeEach, describe, expect, test } from 'vitest' import supertest from 'supertest' import { getAcceptLanguage, @@ -6,6 +6,8 @@ import { getAcceptLocale, getAcceptLocales, getCookieLocale, + getNavigatorLanguage, + getNavigatorLanguages, setCookieLocale, } from './node.ts' import { createServer, IncomingMessage, OutgoingMessage } from 'node:http' @@ -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') + }) +}) diff --git a/src/node.ts b/src/node.ts index 88c9ef9..bf801af 100644 --- a/src/node.ts +++ b/src/node.ts @@ -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' @@ -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} {@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() + + 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] || '') +} diff --git a/src/shared.test.ts b/src/shared.test.ts index 85d2923..f2a7348 100644 --- a/src/shared.test.ts +++ b/src/shared.test.ts @@ -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', () => { @@ -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('') + }) +}) diff --git a/src/shared.ts b/src/shared.ts index 980dfdb..ff41a2e 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -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, '-') +} diff --git a/src/web.test.ts b/src/web.test.ts index 849a712..126c3b5 100644 --- a/src/web.test.ts +++ b/src/web.test.ts @@ -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' @@ -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`/, + ) + }) +}) diff --git a/src/web.ts b/src/web.ts index 2a4b5a7..172ad85 100644 --- a/src/web.ts +++ b/src/web.ts @@ -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} {@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 +}