├── .editorconfig ├── .gitattributes ├── .github ├── funding.yml ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── example.js ├── exec.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: sindresorhus 2 | open_collective: sindresorhus 3 | tidelift: npm/os-locale 4 | custom: https://sindresorhus.com/donate 5 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import {osLocaleSync} from './index.js'; 2 | 3 | // TODO: Enable when ESLint supports top-level await. 4 | // console.log(await osLocale()); 5 | console.log(osLocaleSync()); 6 | -------------------------------------------------------------------------------- /exec.js: -------------------------------------------------------------------------------- 1 | // Mini wrapper around `child_process` to make it behave a little like `execa`. 2 | 3 | import {promisify} from 'node:util'; 4 | import childProcess from 'node:child_process'; 5 | 6 | const execFile = promisify(childProcess.execFile); 7 | 8 | /** 9 | @param {string} command 10 | @param {string[]} arguments_ 11 | 12 | @returns {Promise} 13 | */ 14 | export async function exec(command, arguments_) { 15 | const subprocess = await execFile(command, arguments_, {encoding: 'utf8'}); 16 | subprocess.stdout = subprocess.stdout.trim(); 17 | return subprocess; 18 | } 19 | 20 | /** 21 | @param {string} command 22 | @param {string[]} arguments_ 23 | 24 | @returns {string} 25 | */ 26 | export function execSync(command, arguments_) { 27 | return childProcess.execFileSync(command, arguments_, { 28 | encoding: 'utf8', 29 | stdio: ['ignore', 'pipe', 'ignore'], 30 | }).trim(); 31 | } 32 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | /** 3 | Set to `false` to avoid spawning subprocesses and instead only resolve the locale from environment variables. 4 | 5 | @default true 6 | */ 7 | readonly spawn?: boolean; 8 | } 9 | 10 | /** 11 | Get the system [locale](https://en.wikipedia.org/wiki/Locale_(computer_software)). 12 | 13 | @returns The locale. 14 | 15 | @example 16 | ``` 17 | import {osLocale} from 'os-locale'; 18 | 19 | console.log(await osLocale()); 20 | //=> 'en-US' 21 | ``` 22 | */ 23 | export function osLocale(options?: Options): Promise; 24 | 25 | /** 26 | Synchronously get the system [locale](https://en.wikipedia.org/wiki/Locale_(computer_software)). 27 | 28 | @returns The locale. 29 | 30 | @example 31 | ``` 32 | import {osLocaleSync} from 'os-locale'; 33 | 34 | console.log(osLocaleSync()); 35 | //=> 'en-US' 36 | ``` 37 | */ 38 | export function osLocaleSync(options?: Options): string; 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import lcid from 'lcid'; 2 | import {exec, execSync} from './exec.js'; 3 | 4 | const defaultOptions = {spawn: true}; 5 | const defaultLocale = 'en-US'; 6 | 7 | async function getStdOut(command, args) { 8 | return (await exec(command, args)).stdout; 9 | } 10 | 11 | function getStdOutSync(command, args) { 12 | return execSync(command, args); 13 | } 14 | 15 | function getEnvLocale(env = process.env) { 16 | return env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE; 17 | } 18 | 19 | function parseLocale(string) { 20 | const env = {}; 21 | for (const definition of string.split('\n')) { 22 | const [key, value] = definition.split('='); 23 | env[key] = value.replace(/^"|"$/g, ''); 24 | } 25 | 26 | return getEnvLocale(env); 27 | } 28 | 29 | function getLocale(string) { 30 | return (string && string.replace(/[.:].*/, '')); 31 | } 32 | 33 | async function getLocales() { 34 | return getStdOut('locale', ['-a']); 35 | } 36 | 37 | function getLocalesSync() { 38 | return getStdOutSync('locale', ['-a']); 39 | } 40 | 41 | function getSupportedLocale(locale, locales = '') { 42 | return locales.includes(locale) ? locale : defaultLocale; 43 | } 44 | 45 | async function getAppleLocale() { 46 | const results = await Promise.all([ 47 | getStdOut('defaults', ['read', '-globalDomain', 'AppleLocale']), 48 | getLocales(), 49 | ]); 50 | 51 | return getSupportedLocale(results[0], results[1]); 52 | } 53 | 54 | function getAppleLocaleSync() { 55 | return getSupportedLocale( 56 | getStdOutSync('defaults', ['read', '-globalDomain', 'AppleLocale']), 57 | getLocalesSync(), 58 | ); 59 | } 60 | 61 | async function getUnixLocale() { 62 | return getLocale(parseLocale(await getStdOut('locale'))); 63 | } 64 | 65 | function getUnixLocaleSync() { 66 | return getLocale(parseLocale(getStdOutSync('locale'))); 67 | } 68 | 69 | async function getWinLocale() { 70 | const stdout = await getStdOut('wmic', ['os', 'get', 'locale']); 71 | const lcidCode = Number.parseInt(stdout.replace('Locale', ''), 16); 72 | 73 | return lcid.from(lcidCode); 74 | } 75 | 76 | function getWinLocaleSync() { 77 | const stdout = getStdOutSync('wmic', ['os', 'get', 'locale']); 78 | const lcidCode = Number.parseInt(stdout.replace('Locale', ''), 16); 79 | 80 | return lcid.from(lcidCode); 81 | } 82 | 83 | function normalise(input) { 84 | return input.replace(/_/, '-'); 85 | } 86 | 87 | const cache = new Map(); 88 | 89 | export async function osLocale(options = defaultOptions) { 90 | if (cache.has(options.spawn)) { 91 | return cache.get(options.spawn); 92 | } 93 | 94 | let locale; 95 | 96 | try { 97 | const envLocale = getEnvLocale(); 98 | 99 | if (envLocale || options.spawn === false) { 100 | locale = getLocale(envLocale); 101 | } else if (process.platform === 'win32') { 102 | locale = await getWinLocale(); 103 | } else if (process.platform === 'darwin') { 104 | locale = await getAppleLocale(); 105 | } else { 106 | locale = await getUnixLocale(); 107 | } 108 | } catch {} 109 | 110 | const normalised = normalise(locale || defaultLocale); 111 | cache.set(options.spawn, normalised); 112 | return normalised; 113 | } 114 | 115 | export function osLocaleSync(options = defaultOptions) { 116 | if (cache.has(options.spawn)) { 117 | return cache.get(options.spawn); 118 | } 119 | 120 | let locale; 121 | try { 122 | const envLocale = getEnvLocale(); 123 | 124 | if (envLocale || options.spawn === false) { 125 | locale = getLocale(envLocale); 126 | } else if (process.platform === 'win32') { 127 | locale = getWinLocaleSync(); 128 | } else if (process.platform === 'darwin') { 129 | locale = getAppleLocaleSync(); 130 | } else { 131 | locale = getUnixLocaleSync(); 132 | } 133 | } catch {} 134 | 135 | const normalised = normalise(locale || defaultLocale); 136 | cache.set(options.spawn, normalised); 137 | return normalised; 138 | } 139 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import {osLocale, osLocaleSync} from './index.js'; 3 | 4 | expectType>(osLocale()); 5 | expectType>(osLocale({spawn: false})); 6 | 7 | expectType(osLocaleSync()); 8 | expectType(osLocaleSync({spawn: false})); 9 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "os-locale", 3 | "version": "6.0.2", 4 | "description": "Get the system locale", 5 | "license": "MIT", 6 | "repository": "sindresorhus/os-locale", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./index.js", 15 | "engines": { 16 | "node": ">=12.20" 17 | }, 18 | "scripts": { 19 | "//test": "xo && ava && tsd", 20 | "test": "xo && tsd" 21 | }, 22 | "files": [ 23 | "index.js", 24 | "index.d.ts", 25 | "exec.js" 26 | ], 27 | "keywords": [ 28 | "locale", 29 | "language", 30 | "system", 31 | "os", 32 | "string", 33 | "user", 34 | "country", 35 | "id", 36 | "identifier", 37 | "region" 38 | ], 39 | "dependencies": { 40 | "lcid": "^3.1.1" 41 | }, 42 | "devDependencies": { 43 | "ava": "^3.15.0", 44 | "proxyquire": "^2.1.3", 45 | "tsd": "^0.17.0", 46 | "xo": "^0.42.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # os-locale 2 | 3 | > Get the system [locale](https://en.wikipedia.org/wiki/Locale_(computer_software)) 4 | 5 | Useful for localizing your module or app. 6 | 7 | POSIX systems: The returned locale refers to the [`LC_MESSAGE`](http://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html#Locale-Categories) category, suitable for selecting the language used in the user interface for message translation. 8 | 9 | ## Install 10 | 11 | ``` 12 | $ npm install os-locale 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | import {osLocale} from 'os-locale'; 19 | 20 | console.log(await osLocale()); 21 | //=> 'en-US' 22 | ``` 23 | ## API 24 | 25 | ### osLocale(options?) 26 | 27 | Returns a `Promise` for the locale. 28 | 29 | ### osLocaleSync(options?) 30 | 31 | Returns the locale. 32 | 33 | #### options 34 | 35 | Type: `object` 36 | 37 | ##### spawn 38 | 39 | Type: `boolean`\ 40 | Default: `true` 41 | 42 | Set to `false` to avoid spawning subprocesses and instead only resolve the locale from environment variables. 43 | 44 | ## os-locale for enterprise 45 | 46 | Available as part of the Tidelift Subscription. 47 | 48 | The maintainers of os-locale and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-os-locale?utm_source=npm-os-locale&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 49 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import proxyquireBase from 'proxyquire'; 3 | 4 | const proxyquire = proxyquireBase.noPreserveCache().noCallThru(); 5 | 6 | const expectedFallback = 'en-US'; 7 | 8 | const execaImport = './execa.js'; 9 | 10 | const noExeca = t => { 11 | const fn = () => t.fail('Execa should not be called'); 12 | fn.stdout = async () => t.fail('Execa should not be called'); 13 | 14 | return fn; 15 | }; 16 | 17 | const syncExeca = callback => ({ 18 | sync: (...args) => ({stdout: callback(...args)}), 19 | }); 20 | 21 | const asyncExeca = callback => async (...args) => ({stdout: callback(...args)}); 22 | 23 | const setPlatform = platform => { 24 | Object.defineProperty(process, 'platform', {value: platform}); 25 | }; 26 | 27 | const cache = {}; 28 | 29 | test.beforeEach(() => { 30 | cache.env = process.env; 31 | cache.platform = process.platform; 32 | 33 | process.env = {}; 34 | setPlatform('linux'); 35 | }); 36 | 37 | test.afterEach.always(() => { 38 | process.env = cache.env; 39 | setPlatform(cache.platform); 40 | }); 41 | 42 | /* 43 | We're running tests in serial because we're mutating global state and we need to keep the tests separated. 44 | */ 45 | 46 | test.serial('Async retrieve locale from LC_ALL env as 1st priority', async t => { 47 | process.env.LANGUAGE = 'en-GB1'; 48 | process.env.LANG = 'en-GB2'; 49 | process.env.LC_MESSAGES = 'en-GB3'; 50 | process.env.LC_ALL = 'en-GB4'; 51 | 52 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)})(); 53 | 54 | t.is(locale, 'en-GB4'); 55 | }); 56 | 57 | test.serial('Async retrieve locale from LC_MESSAGES env as 2st priority', async t => { 58 | process.env.LANGUAGE = 'en-GB1'; 59 | process.env.LANG = 'en-GB2'; 60 | process.env.LC_MESSAGES = 'en-GB3'; 61 | 62 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)})(); 63 | 64 | t.is(locale, 'en-GB3'); 65 | }); 66 | 67 | test.serial('Async retrieve locale from LANG env as 3st priority', async t => { 68 | process.env.LANGUAGE = 'en-GB1'; 69 | process.env.LANG = 'en-GB2'; 70 | 71 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)})(); 72 | 73 | t.is(locale, 'en-GB2'); 74 | }); 75 | 76 | test.serial('Async retrieve locale from LANGUAGE env as 4st priority', async t => { 77 | process.env.LANGUAGE = 'en-GB1'; 78 | 79 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)})(); 80 | 81 | t.is(locale, 'en-GB1'); 82 | }); 83 | 84 | test.serial('Sync retrieve locale from LC_ALL env as 1st priority', async t => { 85 | process.env.LANGUAGE = 'en-GB1'; 86 | process.env.LANG = 'en-GB2'; 87 | process.env.LC_MESSAGES = 'en-GB3'; 88 | process.env.LC_ALL = 'en-GB4'; 89 | 90 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)}).sync(); 91 | 92 | t.is(locale, 'en-GB4'); 93 | }); 94 | 95 | test.serial('Sync retrieve locale from LC_MESSAGES env as 2st priority', async t => { 96 | process.env.LANGUAGE = 'en-GB1'; 97 | process.env.LANG = 'en-GB2'; 98 | process.env.LC_MESSAGES = 'en-GB3'; 99 | 100 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)}).sync(); 101 | 102 | t.is(locale, 'en-GB3'); 103 | }); 104 | 105 | test.serial('Sync retrieve locale from LANG env as 3st priority', async t => { 106 | process.env.LANGUAGE = 'en-GB1'; 107 | process.env.LANG = 'en-GB2'; 108 | 109 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)}).sync(); 110 | 111 | t.is(locale, 'en-GB2'); 112 | }); 113 | 114 | test.serial('Sync retrieve locale from LANGUAGE env as 4st priority', async t => { 115 | process.env.LANGUAGE = 'en-GB1'; 116 | 117 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)}).sync(); 118 | 119 | t.is(locale, 'en-GB1'); 120 | }); 121 | 122 | test.serial('Async normalises locale', async t => { 123 | process.env.LC_ALL = 'en_GB'; 124 | 125 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)})(); 126 | 127 | t.is(locale, 'en-GB'); 128 | }); 129 | 130 | test.serial('Sync normalises locale', async t => { 131 | process.env.LC_ALL = 'en_GB'; 132 | 133 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)}).sync(); 134 | 135 | t.is(locale, 'en-GB'); 136 | }); 137 | 138 | test.serial('Async fallback locale when env variables missing and spawn=false ', async t => { 139 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)})({spawn: false}); 140 | 141 | t.is(locale, expectedFallback, 'Locale did not match expected fallback'); 142 | }); 143 | 144 | test.serial('Sync fallback locale when env variables missing and spawn=false ', async t => { 145 | const locale = await proxyquire('./index.js', {[execaImport]: noExeca(t)}).sync({spawn: false}); 146 | 147 | t.is(locale, expectedFallback, 'Locale did not match expected fallback'); 148 | }); 149 | 150 | test.serial('Async handle darwin locale ', async t => { 151 | setPlatform('darwin'); 152 | const execa = asyncExeca(cmd => cmd === 'defaults' ? 'en-GB' : ['en-US', 'en-GB']); 153 | 154 | const locale = await proxyquire('./index.js', {[execaImport]: execa})(); 155 | 156 | t.is(locale, 'en-GB'); 157 | }); 158 | 159 | test.serial('Sync handle darwin locale ', async t => { 160 | setPlatform('darwin'); 161 | const execa = syncExeca(cmd => cmd === 'defaults' ? 'en-GB' : ['en-US', 'en-GB']); 162 | 163 | const locale = await proxyquire('./index.js', {[execaImport]: execa}).sync(); 164 | 165 | t.is(locale, 'en-GB'); 166 | }); 167 | 168 | test.serial('Async handle win32 locale ', async t => { 169 | setPlatform('win32'); 170 | const execa = asyncExeca(() => 'Locale\n0809\n'); 171 | 172 | const locale = await proxyquire('./index.js', {[execaImport]: execa})(); 173 | 174 | t.is(locale, 'en-GB'); 175 | }); 176 | 177 | test.serial('Sync handle win32 locale ', async t => { 178 | setPlatform('win32'); 179 | const execa = syncExeca(() => 'Locale\n0809\n'); 180 | 181 | const locale = await proxyquire('./index.js', {[execaImport]: execa}).sync(); 182 | 183 | t.is(locale, 'en-GB'); 184 | }); 185 | 186 | test.serial('Async handle linux locale ', async t => { 187 | setPlatform('linux'); 188 | const execa = asyncExeca(() => `LANG="en-GB" 189 | LC_COLLATE="en_GB" 190 | LC_CTYPE="UTF-8" 191 | LC_MESSAGES="en_GB" 192 | LC_MONETARY="en_GB" 193 | LC_NUMERIC="en_GB" 194 | LC_TIME="en_GB" 195 | LC_ALL=en_GB`); 196 | 197 | const locale = await proxyquire('./index.js', {[execaImport]: execa})(); 198 | 199 | t.is(locale, 'en-GB'); 200 | }); 201 | 202 | test.serial('Sync handle linux locale ', async t => { 203 | setPlatform('linux'); 204 | const execa = syncExeca(() => `LANG="en-GB" 205 | LC_COLLATE="en_GB" 206 | LC_CTYPE="UTF-8" 207 | LC_MESSAGES="en_GB" 208 | LC_MONETARY="en_GB" 209 | LC_NUMERIC="en_GB" 210 | LC_TIME="en_GB" 211 | LC_ALL=en_GB`); 212 | 213 | const locale = await proxyquire('./index.js', {[execaImport]: execa}).sync(); 214 | 215 | t.is(locale, 'en-GB'); 216 | }); 217 | --------------------------------------------------------------------------------