PHP's setlocale in TypeScript

How to use

Install via yarn add locutus and import: import { setlocale } from 'locutus/php/strings/setlocale'.

Or with CommonJS: const { setlocale } = require('locutus/php/strings/setlocale')

Use a bundler that supports tree-shaking so you only ship the functions you actually use. Vite, webpack, Rollup, and Parcel all handle this. For server-side use this is less of a concern.

Examples

These examples are extracted from test cases that automatically verify our functions against their native counterparts.

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

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

Dependencies

This function uses the following Locutus functions:

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

import { getPhpRuntimeEntry, getPhpRuntimeString, setPhpRuntimeEntry } from '../_helpers/_phpRuntimeState.ts'
import { isPhpAssocObject, type PhpAssoc, type PhpInput } from '../_helpers/_phpTypes.ts'
import { getenv } from '../info/getenv.ts'

type LocaleDefinition = {
LC_COLLATE: (str1: string, str2: string) => number
LC_CTYPE: Record<string, RegExp | string>
LC_TIME: Record<string, string | string[]>
LC_MONETARY: Record<string, string | number | number[]>
LC_NUMERIC: Record<string, string | number[]>
LC_MESSAGES: Record<string, string>
nplurals: (n: number) => number
}

type LocaleInput = string | string[] | number | null

const isLocaleDefinitionMap = (value: PhpInput): value is Record<string, LocaleDefinition> =>
typeof value === 'object' && value !== null && !Array.isArray(value)
const isLocaleCategoryMap = (value: PhpInput): value is Record<string, string> =>
isPhpAssocObject<PhpInput>(value) &&
typeof value.LC_COLLATE === 'string' &&
typeof value.LC_CTYPE === 'string' &&
typeof value.LC_MONETARY === 'string' &&
typeof value.LC_NUMERIC === 'string' &&
typeof value.LC_TIME === 'string' &&
typeof value.LC_MESSAGES === 'string'

function copyValue<T>(orig: T): T
function copyValue(orig: PhpInput): PhpInput {
if (orig instanceof RegExp) {
return new RegExp(orig)
}
if (orig instanceof Date) {
return new Date(orig)
}
if (Array.isArray(orig)) {
return orig.map((item) => copyValue(item))
}
if (orig !== null && typeof orig === 'object') {
const newObj: PhpAssoc<PhpInput> = {}
for (const [key, value] of Object.entries(orig)) {
newObj[key] = value !== null && typeof value === 'object' ? copyValue(value) : value
}
return newObj
}
return orig
}

export function setlocale(category: string, locale: LocaleInput): string | false {
// 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 cats: string[] = []
let i = 0

// 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: number) {
// e.g., English
return n !== 1 ? 1 : 0
}
const _nplurals2b = function (n: number) {
// e.g., French
return n > 1 ? 1 : 0
}

const localesValue = getPhpRuntimeEntry('locales')
let locales: Record<string, LocaleDefinition> = isLocaleDefinitionMap(localesValue) ? localesValue : {}
if (localesValue !== locales) {
setPhpRuntimeEntry('locales', locales)
}

// 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 (!locales.fr_CA?.LC_TIME?.x) {
// Can add to the locales
locales = {}
setPhpRuntimeEntry('locales', locales)

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,
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional for LC_CTYPE control character class
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,
}
locales.en_US = copyValue(locales.en)
locales.en_US.LC_TIME.c = '%a %d %b %Y %r %Z'
locales.en_US.LC_TIME.x = '%D'
locales.en_US.LC_TIME.X = '%r'
// The following are based on *nix settings
locales.en_US.LC_MONETARY.int_curr_symbol = 'USD '
locales.en_US.LC_MONETARY.p_sign_posn = 1
locales.en_US.LC_MONETARY.n_sign_posn = 1
locales.en_US.LC_MONETARY.mon_grouping = [3, 3]
locales.en_US.LC_NUMERIC.thousands_sep = ''
locales.en_US.LC_NUMERIC.grouping = []

locales.en_GB = copyValue(locales.en)
locales.en_GB.LC_TIME.r = '%l:%M:%S %P %Z'

locales.en_AU = copyValue(locales.en_GB)
// Assume C locale is like English (?) (We need C locale for LC_CTYPE)
locales.C = copyValue(locales.en)
locales.C.LC_CTYPE.CODESET = 'ANSI_X3.4-1968'
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,
}
locales.C.LC_NUMERIC = {
decimal_point: '.',
thousands_sep: '',
grouping: [],
}
// D_T_FMT
locales.C.LC_TIME.c = '%a %b %e %H:%M:%S %Y'
// D_FMT
locales.C.LC_TIME.x = '%m/%d/%y'
// T_FMT
locales.C.LC_TIME.X = '%H:%M:%S'
locales.C.LC_MESSAGES.YESEXPR = '^[yY]'
locales.C.LC_MESSAGES.NOEXPR = '^[nN]'

locales.fr = copyValue(locales.en)
locales.fr.nplurals = _nplurals2b
locales.fr.LC_TIME.a = ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam']
locales.fr.LC_TIME.A = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']
locales.fr.LC_TIME.b = [
'jan',
'f\u00E9v',
'mar',
'avr',
'mai',
'jun',
'jui',
'ao\u00FB',
'sep',
'oct',
'nov',
'd\u00E9c',
]
locales.fr.LC_TIME.B = [
'janvier',
'f\u00E9vrier',
'mars',
'avril',
'mai',
'juin',
'juillet',
'ao\u00FBt',
'septembre',
'octobre',
'novembre',
'd\u00E9cembre',
]
locales.fr.LC_TIME.c = '%a %d %b %Y %T %Z'
locales.fr.LC_TIME.p = ['', '']
locales.fr.LC_TIME.P = ['', '']
locales.fr.LC_TIME.x = '%d.%m.%Y'
locales.fr.LC_TIME.X = '%T'

locales.fr_CA = copyValue(locales.fr)
locales.fr_CA.LC_TIME.x = '%Y-%m-%d'
}
let currentLocale = getPhpRuntimeString('locale', '')
if (!currentLocale) {
currentLocale = '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'
const htmlNsElement = d.getElementsByTagNameNS ? d.getElementsByTagNameNS(NS_XHTML, 'html')[0] : undefined
if (htmlNsElement) {
const xmlLang = htmlNsElement.getAttributeNS(NS_XML, 'lang')
if (xmlLang) {
currentLocale = xmlLang
} else {
const htmlLang = htmlNsElement.getAttribute('lang')
if (htmlLang) {
currentLocale = htmlLang
}
}
} else {
const htmlElement = d.getElementsByTagName('html')[0]
const htmlLang = htmlElement?.getAttribute('lang')
if (htmlLang) {
currentLocale = htmlLang
}
}
}
}
// PHP-style
currentLocale = currentLocale.replace('-', '_')
// @todo: locale if declared locale hasn't been defined
if (!(currentLocale in locales)) {
const languageLocale = currentLocale.replace(/_[a-zA-Z]+$/, '')
if (languageLocale in locales) {
currentLocale = languageLocale
}
}
setPhpRuntimeEntry('locale', currentLocale)

const localeCategoriesValue = getPhpRuntimeEntry('localeCategories')
const localeCategories: Record<string, string> = isLocaleCategoryMap(localeCategoriesValue)
? localeCategoriesValue
: {
LC_COLLATE: currentLocale,
// for string comparison, see strcoll()
LC_CTYPE: currentLocale,
// for character classification and conversion, for example strtoupper()
LC_MONETARY: currentLocale,
// for localeconv()
LC_NUMERIC: currentLocale,
// for decimal separator (See also localeconv())
LC_TIME: currentLocale,
// for date and time formatting with strftime()
// for system responses (available if PHP was compiled with libintl):
LC_MESSAGES: currentLocale,
}
if (localeCategoriesValue !== localeCategories) {
setPhpRuntimeEntry('localeCategories', localeCategories)
}

let requestedLocale: LocaleInput | false = locale

if (requestedLocale === null || requestedLocale === '') {
requestedLocale = getenv(category) || getenv('LANG')
} else if (Array.isArray(requestedLocale)) {
for (i = 0; i < requestedLocale.length; i++) {
const candidate = requestedLocale[i]
if (typeof candidate !== 'string') {
if (i === requestedLocale.length - 1) {
return false
}
continue
}
if (!(candidate in locales)) {
if (i === requestedLocale.length - 1) {
// none found
return false
}
continue
}
requestedLocale = candidate
break
}
}

// Just get the locale
if (requestedLocale === '0' || requestedLocale === 0) {
if (category === 'LC_ALL') {
for (const categ of Object.keys(localeCategories)) {
// Add ".UTF-8" or allow ".@latint", etc. to the end?
cats.push(categ + '=' + localeCategories[categ])
}
return cats.join(';')
}
return localeCategories[category] ?? false
}

if (typeof requestedLocale !== 'string' || !(requestedLocale in locales)) {
// Locale not found
return false
}

// Set and get locale
if (category === 'LC_ALL') {
for (const categ of Object.keys(localeCategories)) {
localeCategories[categ] = requestedLocale
}
} else {
localeCategories[category] = requestedLocale
}

return requestedLocale
}

Improve this function

Locutus is a community effort following The McDonald's Theory: we ship first iterations, hoping others will improve them. If you see something that could be better, we'd love your contribution.

View on GitHub · Edit on GitHub · View Raw


« More PHP strings functions


Star