PHP's setlocale in JavaScript

Here’s what our current JavaScript equivalent to PHP's setlocale looks like.

module.exports = function setlocale (category, locale) {
// discuss at: https://locutus.io/php/setlocale/
// original by: Brett Zamir (https://brett-zamir.me)
// original by: Blues (https://hacks.bluesmoon.info/strftime/strftime.js)
// original by: YUI Library (https://developer.yahoo.com/yui/docs/YAHOO.util.DateLocale.html)
// note 1: Is extensible, but currently only implements locales en,
// note 1: en_US, en_GB, en_AU, fr, and fr_CA for LC_TIME only; C for LC_CTYPE;
// note 1: C and en for LC_MONETARY/LC_NUMERIC; en for LC_COLLATE
// note 1: Uses global: locutus to store locale info
// note 1: Consider using https://demo.icu-project.org/icu-bin/locexp as basis for localization (as in i18n_loc_set_default())
// note 2: This function tries to establish the locale via the `window` global.
// note 2: This feature will not work in Node and hence is Browser-only
// example 1: setlocale('LC_ALL', 'en_US')
// returns 1: 'en_US'
const getenv = require('../info/getenv')
let categ = ''
const cats = []
let i = 0
const _copy = function _copy (orig) {
if (orig instanceof RegExp) {
return new RegExp(orig)
} else if (orig instanceof Date) {
return new Date(orig)
}
const newObj = {}
for (const i in orig) {
if (typeof orig[i] === 'object') {
newObj[i] = _copy(orig[i])
} else {
newObj[i] = orig[i]
}
}
return newObj
}
// Function usable by a ngettext implementation (apparently not an accessible part of setlocale(),
// but locale-specific) See https://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
// though amended with others from https://developer.mozilla.org/En/Localization_and_Plurals (new
// categories noted with "MDC" below, though not sure of whether there is a convention for the
// relative order of these newer groups as far as ngettext) The function name indicates the number
// of plural forms (nplural) Need to look into https://cldr.unicode.org/ (maybe future JavaScript);
// Dojo has some functions (under new BSD), including JSON conversions of LDML XML from CLDR:
// https://bugs.dojotoolkit.org/browser/dojo/trunk/cldr and docs at
// https://api.dojotoolkit.org/jsdoc/HEAD/dojo.cldr
// var _nplurals1 = function (n) {
// // e.g., Japanese
// return 0
// }
const _nplurals2a = function (n) {
// e.g., English
return n !== 1 ? 1 : 0
}
const _nplurals2b = function (n) {
// e.g., French
return n > 1 ? 1 : 0
}
const $global = (typeof window !== 'undefined' ? window : global)
$global.$locutus = $global.$locutus || {}
const $locutus = $global.$locutus
$locutus.php = $locutus.php || {}
// Reconcile Windows vs. *nix locale names?
// Allow different priority orders of languages, esp. if implement gettext as in
// LANGUAGE env. var.? (e.g., show German if French is not available)
if (!$locutus.php.locales ||
!$locutus.php.locales.fr_CA ||
!$locutus.php.locales.fr_CA.LC_TIME ||
!$locutus.php.locales.fr_CA.LC_TIME.x) {
// Can add to the locales
$locutus.php.locales = {}
$locutus.php.locales.en = {
LC_COLLATE: function (str1, str2) {
// @todo: This one taken from strcmp, but need for other locales; we don't use localeCompare
// since its locale is not settable
return (str1 === str2) ? 0 : ((str1 > str2) ? 1 : -1)
},
LC_CTYPE: {
// Need to change any of these for English as opposed to C?
an: /^[A-Za-z\d]+$/g,
al: /^[A-Za-z]+$/g,
ct: /^[\u0000-\u001F\u007F]+$/g,
dg: /^[\d]+$/g,
gr: /^[\u0021-\u007E]+$/g,
lw: /^[a-z]+$/g,
pr: /^[\u0020-\u007E]+$/g,
pu: /^[\u0021-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]+$/g,
sp: /^[\f\n\r\t\v ]+$/g,
up: /^[A-Z]+$/g,
xd: /^[A-Fa-f\d]+$/g,
CODESET: 'UTF-8',
// Used by sql_regcase
lower: 'abcdefghijklmnopqrstuvwxyz',
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
},
LC_TIME: {
// Comments include nl_langinfo() constant equivalents and any
// changes from Blues' implementation
a: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
// ABDAY_
A: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
// DAY_
b: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
// ABMON_
B: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October',
'November', 'December'
],
// MON_
c: '%a %d %b %Y %r %Z',
// D_T_FMT // changed %T to %r per results
p: ['AM', 'PM'],
// AM_STR/PM_STR
P: ['am', 'pm'],
// Not available in nl_langinfo()
r: '%I:%M:%S %p',
// T_FMT_AMPM (Fixed for all locales)
x: '%m/%d/%Y',
// D_FMT // switched order of %m and %d; changed %y to %Y (C uses %y)
X: '%r',
// T_FMT // changed from %T to %r (%T is default for C, not English US)
// Following are from nl_langinfo() or https://www.cptec.inpe.br/sx4/sx4man2/g1ab02e/strftime.4.html
alt_digits: '',
// e.g., ordinal
ERA: '',
ERA_YEAR: '',
ERA_D_T_FMT: '',
ERA_D_FMT: '',
ERA_T_FMT: ''
},
// Assuming distinction between numeric and monetary is thus:
// See below for C locale
LC_MONETARY: {
// based on Windows "english" (English_United States.1252) locale
int_curr_symbol: 'USD',
currency_symbol: '$',
mon_decimal_point: '.',
mon_thousands_sep: ',',
mon_grouping: [3],
// use mon_thousands_sep; "" for no grouping; additional array members
// indicate successive group lengths after first group
// (e.g., if to be 1,23,456, could be [3, 2])
positive_sign: '',
negative_sign: '-',
int_frac_digits: 2,
// Fractional digits only for money defaults?
frac_digits: 2,
p_cs_precedes: 1,
// positive currency symbol follows value = 0; precedes value = 1
p_sep_by_space: 0,
// 0: no space between curr. symbol and value; 1: space sep. them unless symb.
// and sign are adjacent then space sep. them from value; 2: space sep. sign
// and value unless symb. and sign are adjacent then space separates
n_cs_precedes: 1,
// see p_cs_precedes
n_sep_by_space: 0,
// see p_sep_by_space
p_sign_posn: 3,
// 0: parentheses surround quantity and curr. symbol; 1: sign precedes them;
// 2: sign follows them; 3: sign immed. precedes curr. symbol; 4: sign immed.
// succeeds curr. symbol
n_sign_posn: 0 // see p_sign_posn
},
LC_NUMERIC: {
// based on Windows "english" (English_United States.1252) locale
decimal_point: '.',
thousands_sep: ',',
grouping: [3] // see mon_grouping, but for non-monetary values (use thousands_sep)
},
LC_MESSAGES: {
YESEXPR: '^[yY].*',
NOEXPR: '^[nN].*',
YESSTR: '',
NOSTR: ''
},
nplurals: _nplurals2a
}
$locutus.php.locales.en_US = _copy($locutus.php.locales.en)
$locutus.php.locales.en_US.LC_TIME.c = '%a %d %b %Y %r %Z'
$locutus.php.locales.en_US.LC_TIME.x = '%D'
$locutus.php.locales.en_US.LC_TIME.X = '%r'
// The following are based on *nix settings
$locutus.php.locales.en_US.LC_MONETARY.int_curr_symbol = 'USD '
$locutus.php.locales.en_US.LC_MONETARY.p_sign_posn = 1
$locutus.php.locales.en_US.LC_MONETARY.n_sign_posn = 1
$locutus.php.locales.en_US.LC_MONETARY.mon_grouping = [3, 3]
$locutus.php.locales.en_US.LC_NUMERIC.thousands_sep = ''
$locutus.php.locales.en_US.LC_NUMERIC.grouping = []
$locutus.php.locales.en_GB = _copy($locutus.php.locales.en)
$locutus.php.locales.en_GB.LC_TIME.r = '%l:%M:%S %P %Z'
$locutus.php.locales.en_AU = _copy($locutus.php.locales.en_GB)
// Assume C locale is like English (?) (We need C locale for LC_CTYPE)
$locutus.php.locales.C = _copy($locutus.php.locales.en)
$locutus.php.locales.C.LC_CTYPE.CODESET = 'ANSI_X3.4-1968'
$locutus.php.locales.C.LC_MONETARY = {
int_curr_symbol: '',
currency_symbol: '',
mon_decimal_point: '',
mon_thousands_sep: '',
mon_grouping: [],
p_cs_precedes: 127,
p_sep_by_space: 127,
n_cs_precedes: 127,
n_sep_by_space: 127,
p_sign_posn: 127,
n_sign_posn: 127,
positive_sign: '',
negative_sign: '',
int_frac_digits: 127,
frac_digits: 127
}
$locutus.php.locales.C.LC_NUMERIC = {
decimal_point: '.',
thousands_sep: '',
grouping: []
}
// D_T_FMT
$locutus.php.locales.C.LC_TIME.c = '%a %b %e %H:%M:%S %Y'
// D_FMT
$locutus.php.locales.C.LC_TIME.x = '%m/%d/%y'
// T_FMT
$locutus.php.locales.C.LC_TIME.X = '%H:%M:%S'
$locutus.php.locales.C.LC_MESSAGES.YESEXPR = '^[yY]'
$locutus.php.locales.C.LC_MESSAGES.NOEXPR = '^[nN]'
$locutus.php.locales.fr = _copy($locutus.php.locales.en)
$locutus.php.locales.fr.nplurals = _nplurals2b
$locutus.php.locales.fr.LC_TIME.a = ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam']
$locutus.php.locales.fr.LC_TIME.A = ['dimanche', 'lundi', 'mardi', 'mercredi',
'jeudi', 'vendredi', 'samedi']
$locutus.php.locales.fr.LC_TIME.b = ['jan', 'f\u00E9v', 'mar', 'avr', 'mai',
'jun', 'jui', 'ao\u00FB', 'sep', 'oct',
'nov', 'd\u00E9c'
]
$locutus.php.locales.fr.LC_TIME.B = ['janvier', 'f\u00E9vrier', 'mars',
'avril', 'mai', 'juin', 'juillet', 'ao\u00FBt',
'septembre', 'octobre', 'novembre', 'd\u00E9cembre'
]
$locutus.php.locales.fr.LC_TIME.c = '%a %d %b %Y %T %Z'
$locutus.php.locales.fr.LC_TIME.p = ['', '']
$locutus.php.locales.fr.LC_TIME.P = ['', '']
$locutus.php.locales.fr.LC_TIME.x = '%d.%m.%Y'
$locutus.php.locales.fr.LC_TIME.X = '%T'
$locutus.php.locales.fr_CA = _copy($locutus.php.locales.fr)
$locutus.php.locales.fr_CA.LC_TIME.x = '%Y-%m-%d'
}
if (!$locutus.php.locale) {
$locutus.php.locale = 'en_US'
// Try to establish the locale via the `window` global
if (typeof window !== 'undefined' && window.document) {
const d = window.document
const NS_XHTML = 'https://www.w3.org/1999/xhtml'
const NS_XML = 'https://www.w3.org/XML/1998/namespace'
if (d.getElementsByTagNameNS &&
d.getElementsByTagNameNS(NS_XHTML, 'html')[0]) {
if (d.getElementsByTagNameNS(NS_XHTML, 'html')[0].getAttributeNS &&
d.getElementsByTagNameNS(NS_XHTML, 'html')[0].getAttributeNS(NS_XML, 'lang')) {
$locutus.php.locale = d.getElementsByTagName(NS_XHTML, 'html')[0]
.getAttributeNS(NS_XML, 'lang')
} else if (d.getElementsByTagNameNS(NS_XHTML, 'html')[0].lang) {
// XHTML 1.0 only
$locutus.php.locale = d.getElementsByTagNameNS(NS_XHTML, 'html')[0].lang
}
} else if (d.getElementsByTagName('html')[0] &&
d.getElementsByTagName('html')[0].lang) {
$locutus.php.locale = d.getElementsByTagName('html')[0].lang
}
}
}
// PHP-style
$locutus.php.locale = $locutus.php.locale.replace('-', '_')
// @todo: locale if declared locale hasn't been defined
if (!($locutus.php.locale in $locutus.php.locales)) {
if ($locutus.php.locale.replace(/_[a-zA-Z]+$/, '') in $locutus.php.locales) {
$locutus.php.locale = $locutus.php.locale.replace(/_[a-zA-Z]+$/, '')
}
}
if (!$locutus.php.localeCategories) {
$locutus.php.localeCategories = {
LC_COLLATE: $locutus.php.locale,
// for string comparison, see strcoll()
LC_CTYPE: $locutus.php.locale,
// for character classification and conversion, for example strtoupper()
LC_MONETARY: $locutus.php.locale,
// for localeconv()
LC_NUMERIC: $locutus.php.locale,
// for decimal separator (See also localeconv())
LC_TIME: $locutus.php.locale,
// for date and time formatting with strftime()
// for system responses (available if PHP was compiled with libintl):
LC_MESSAGES: $locutus.php.locale
}
}
if (locale === null || locale === '') {
locale = getenv(category) || getenv('LANG')
} else if (Object.prototype.toString.call(locale) === '[object Array]') {
for (i = 0; i < locale.length; i++) {
if (!(locale[i] in $locutus.php.locales)) {
if (i === locale.length - 1) {
// none found
return false
}
continue
}
locale = locale[i]
break
}
}
// Just get the locale
if (locale === '0' || locale === 0) {
if (category === 'LC_ALL') {
for (categ in $locutus.php.localeCategories) {
// Add ".UTF-8" or allow ".@latint", etc. to the end?
cats.push(categ + '=' + $locutus.php.localeCategories[categ])
}
return cats.join(';')
}
return $locutus.php.localeCategories[category]
}
if (!(locale in $locutus.php.locales)) {
// Locale not found
return false
}
// Set and get locale
if (category === 'LC_ALL') {
for (categ in $locutus.php.localeCategories) {
$locutus.php.localeCategories[categ] = locale
}
} else {
$locutus.php.localeCategories[category] = locale
}
return locale
}
[ View on GitHub | Edit on GitHub | Source on GitHub ]

How to use

You you can install via npm install locutus and require it via require('locutus/php/strings/setlocale'). You could also require the strings module in full so that you could access strings.setlocale instead.

If you intend to target the browser, you can then use a module bundler such as Parcel, webpack, Browserify, or rollup.js. This can be important because Locutus allows modern JavaScript in the source files, meaning it may not work in all browsers without a build/transpile step. Locutus does transpile all functions to ES5 before publishing to npm.

A community effort

Not unlike Wikipedia, Locutus is an ongoing community effort. Our philosophy follows The McDonald’s Theory. This means that we don't consider it to be a bad thing that many of our functions are first iterations, which may still have their fair share of issues. We hope that these flaws will inspire others to come up with better ideas.

This way of working also means that we don't offer any production guarantees, and recommend to use Locutus inspiration and learning purposes only.

Notes

  • Is extensible, but currently only implements locales en, en_US, en_GB, en_AU, fr, and fr_CA for LC_TIME only; C for LC_CTYPE; C and en for LC_MONETARY/LC_NUMERIC; en for LC_COLLATE Uses global: locutus to store locale info Consider using https://demo.icu-project.org/icu-bin/locexp as basis for localization (as in i18n_loc_set_default())

  • This function tries to establish the locale via the window global. This feature will not work in Node and hence is Browser-only

Examples

Please note that these examples are distilled from test cases that automatically verify our functions still work correctly. This could explain some quirky ones.

#codeexpected result
1setlocale('LC_ALL', 'en_US')'en_US'

« More PHP strings functions


Star