Files
mrbs-equbao-2026/web/lib/MRBS/Intl/LocaleEmulator.php
T
人事系统开发 48092cab42
Docker image / push (push) Canceled after 0s
MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
2026-09-08 21:19:47 +08:00

300 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS\Intl;
use InvalidArgumentException;
use MRBS\Language;
use MRBS\System;
/**
* A partial emulation of the PHP \Locale class.
*
* It is necessary because (a) the 'intl' extension isn't always enabled and (b) even if it is some
* methods are only available in later versions of PHP, eg isRightToLeft is only available from PHP 8.5
*/
class LocaleEmulator
{
/**
* A list of languages that use Right to Left text
*/
private const RTL_LANGUAGES = [
// TODO: Expand this list
'he'
];
/**
* Tries to find out best available locale based on HTTP "Accept-Language" header.
*
* @return string|false the corresponding locale identifier, or FALSE if none found
*/
public static function acceptFromHttp(string $header)
{
$accept_languages = self::toSortedArray($header);
foreach($accept_languages as $accept_language => $value)
{
if (System::isAvailableLocale($accept_language))
{
return $accept_language;
}
}
return false;
}
// Returns a correctly ordered and delimited locale ID
public static function composeLocale(array $subtags)
{
if (isset($subtags[Locale::GRANDFATHERED_LANG_TAG]))
{
return $subtags[Locale::GRANDFATHERED_LANG_TAG];
}
$pieces = array();
foreach (array(Locale::LANG_TAG, Locale::EXTLANG_TAG, Locale::SCRIPT_TAG, Locale::REGION_TAG) as $subtag_label)
{
if (isset($subtags[$subtag_label]))
{
$pieces[] = $subtags[$subtag_label];
}
}
if (array_key_exists(Locale::VARIANT_TAG . '0', $subtags))
{
for ($i=0; $i<15; $i++)
{
if (isset($subtags[Locale::VARIANT_TAG . $i]))
{
$pieces[] = $subtags[Locale::VARIANT_TAG . $i];
}
}
}
if (array_key_exists(Locale::PRIVATE_TAG . '0', $subtags))
{
$pieces[] = 'x';
for ($i=0; $i<15; $i++)
{
if (isset($subtags[Locale::PRIVATE_TAG . $i]))
{
$pieces[] = $subtags[Locale::PRIVATE_TAG . $i];
}
}
}
return implode('_', $pieces);
}
// Returns a key-value array of locale ID subtag elements.
// Parses a language tag according to BCP 47
// See http://tools.ietf.org/html/bcp47
public static function parseLocale(string $locale) : ?array
{
static $regex = array('extlang' => '/^[[:alpha:]]{3}$/', // 3ALPHA
'script' => '/^[[:alpha:]]{4}$/', // 4ALPHA
'region' => '/^[[:alpha:]]{2}$|^[[:digit:]]{3}$/', // 2ALPHA or 3DIGIT
'variant' => '/^[[:alnum:]]{5,8}$|^[[:digit:]][[:alnum:]]{3}$/'); // 5*8alphanum or (DIGIT 3alphanum)
static $grandfathered = array('en-GB-oed', // Irregular
'i-ami',
'i-bnn',
'i-default',
'i-enochian',
'i-hak',
'i-klingon',
'i-lux',
'i-mingo',
'i-navajo',
'i-pwn',
'i-tao',
'i-tay',
'i-tsu',
'sgn-BE-FR',
'sgn-BE-NL',
'sgn-CH-DE',
'art-lojban', // Regular
'cel-gaulish',
'no-bok',
'no-nyn',
'zh-guoyu',
'zh-hakka',
'zh-min',
'zh-min-nan',
'zh-xiang');
// First check for a grandfathered tag
if (isset($locale) && in_array($locale, $grandfathered))
{
return array(Locale::GRANDFATHERED_LANG_TAG => $locale);
}
// Otherwise parse the subtags
$result = array();
if (isset($locale))
{
$subtags = preg_split('/[-_]/', $locale);
}
else
{
$subtags = array();
}
while (null !== ($subtag = array_shift($subtags)))
{
// Tags are case-insensitive, so convert to lowercase before processing and then
// later convert as necessary according to convention
$subtag = strtolower($subtag);
if ($subtag == 'x')
{
// If the subtag is an 'x' then everything else is a private subtag,
// even if it occurs as the first subtag:
// "The single-character subtag 'x' as the primary subtag indicates
// that the language tag consists solely of subtags whose meaning is
// defined by private agreement"
$i = 0;
while (null !== ($subtag = array_shift($subtags)))
{
$result[Locale::PRIVATE_TAG . $i] = $subtag;
$i++;
}
}
// The primary language subtag is the first subtag in a language tag and
// cannot be omitted, with two exceptions:
//
// o The single-character subtag 'x' as the primary subtag ...
// o The single-character subtag 'i' is used by some grandfathered tags ...
elseif (!isset($result[Locale::LANG_TAG]))
{
// [ISO639-1] recommends that language codes be written in lowercase ('mn' Mongolian).
// As the subtag will already be lowercase there's no need to do anything else
$result[Locale::LANG_TAG] = $subtag;
// Check if the next subtag looks like a language extension
if (count($subtags) > 0)
{
if (preg_match($regex['extlang'], $subtags[0]))
{
$result[Locale::EXTLANG_TAG] = strtolower(array_shift($subtags));
}
}
}
// Script
elseif (preg_match($regex['script'], $subtag))
{
// [ISO15924] recommends that script codes use lowercase with the
// initial letter capitalized ('Cyrl' Cyrillic).
$result[Locale::SCRIPT_TAG] = ucfirst($subtag);
}
// Region
elseif (preg_match($regex['region'], $subtag))
{
// [ISO3166-1] recommends that country codes be capitalized ('MN'
// Mongolia).
$result[Locale::REGION_TAG] = strtoupper($subtag);
}
// Variants
elseif (preg_match($regex['variant'], $subtag))
{
$i = 0;
do
{
// If the subtag doesn't look like a variant then we've got them all
// and gone one subtag too far, so put it back
if (!preg_match($regex['variant'], $subtag))
{
array_unshift($subtags, $subtag);
break;
}
$result[Locale::VARIANT_TAG . $i] = $subtag;
}
while (null !== ($subtag = array_shift($subtags)));
}
else
{
trigger_error("parseLocale: could not parse subtag '$subtag'", E_USER_NOTICE);
// This is how the PHP version behaves: if it can't parse the locale completely
// it returns an empty array.
$result = array();
}
}
return $result;
}
public static function isRightToLeft(string $locale): bool
{
return in_array(mb_strtolower($locale), self::RTL_LANGUAGES);
}
// Searches the items in the array $langtag for the best match to the language range
// specified in $locale according to RFC 4647's lookup algorithm. The langtags and
// locale can have subtags separated by '-' or '_' and the search is case-insensitive.
// Charsets (eg '.UTF-8') are stripped off $locale
//
// Returns the best match, or else an empty string if no match
public static function lookup(array $langtag, string $locale, bool $canonicalize=false, ?string $default=null) : ?string
{
if ($canonicalize)
{
throw new InvalidArgumentException('MRBS: the MRBS version of Locale::lookup() does not yet support $canonicalize = true');
}
// Get the langtags and locale in the same format, ie separated by '-' and
// all lower case
$standard_langtags = self::standardise($langtag);
// Strip off any charset (eg '.UTF-8');
$locale = preg_replace('/\..*$/', '', $locale);
$standard_locale = self::standardise($locale);
// Look for a match. If there isn't one remove the last subtag from the end
// of the locale and try again.
while (false === ($index = array_search($standard_locale, $standard_langtags)))
{
if (false === ($pos = strrpos($standard_locale, '-')))
{
return (isset($default)) ? $default : '';
}
$standard_locale = substr($standard_locale, 0, $pos);
}
return $langtag[$index]; // Return the match in its original format
}
// Converts $langtag, which can be a string or an array, into a standard form with
// subtags all in lower case and separated by '-';
private static function standardise($langtag)
{
$glue = ',';
$result = (is_array($langtag)) ? implode($glue, $langtag) : $langtag;
$result = mb_strtolower(str_replace('_', '-', $result));
return (is_array($langtag)) ? explode($glue, $result) : $result;
}
/**
* Converts an Accept-Language request-header to an array of acceptable languages
* with the language as the key and the quality value as the value, sorted in
* decreasing order of quality value. A wildcard in the header is translated.
*
* @return array<string, float>
*/
private static function toSortedArray(string $header) : array
{
return Language::getQualifiers($header, true);
}
}