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 LC_TIME: Record LC_MONETARY: Record LC_NUMERIC: Record LC_MESSAGES: Record nplurals: (n: number) => number } type LocaleInput = string | string[] | number | null const isLocaleDefinitionMap = (value: PhpInput): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value) const isLocaleCategoryMap = (value: PhpInput): value is Record => isPhpAssocObject(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(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 = {} 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 = 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 = 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 }