Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
83.61% |
51 / 61 |
CRAP | |
86.79% |
381 / 439 |
Intuition | |
0.00% |
0 / 1 |
|
83.61% |
51 / 61 |
303.86 | |
86.79% |
381 / 439 |
clearCache | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
__construct | |
0.00% |
0 / 1 |
4.00 | |
94.12% |
16 / 17 |
|||
initHook | n/a |
0 / 0 |
3 | n/a |
0 / 0 |
|||||
getLang | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
setLang | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
getLocale | |
100.00% |
1 / 1 |
5 | |
100.00% |
16 / 16 |
|||
getDomain | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
setDomain | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
getCookieNames | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getCookieName | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
getParamNames | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getParamName | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
getUseRequestParam | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
setUseRequestParam | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
getMessagesFunctions | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
normalizeDomain | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
normalizeLang | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
msg | |
100.00% |
1 / 1 |
11 | |
100.00% |
33 / 33 |
|||
rawMsg | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
accessBlob | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
accessBlobWithFallback | |
100.00% |
1 / 1 |
6 | |
100.00% |
10 / 10 |
|||
bracketMsg | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
msgExists | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
setMsg | |
100.00% |
1 / 1 |
5 | |
100.00% |
8 / 8 |
|||
setMsgs | |
100.00% |
1 / 1 |
4 | |
100.00% |
3 / 3 |
|||
registerDomain | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
addDomainInfo | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
getDomainInfo | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
listMsgs | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
getLangFallbacks | |
100.00% |
1 / 1 |
3 | |
100.00% |
4 / 4 |
|||
fetchLangFallbacks | |
100.00% |
1 / 1 |
5 | |
100.00% |
6 / 6 |
|||
getLangName | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
getLangNames | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
getAvailableLangs | |
100.00% |
1 / 1 |
7 | |
100.00% |
17 / 17 |
|||
addAvailableLang | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
ensureLoaded | |
100.00% |
1 / 1 |
7 | |
100.00% |
17 / 17 |
|||
loadMessageFile | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
isLocalDomain | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
setCookie | |
100.00% |
1 / 1 |
3 | |
100.00% |
10 / 10 |
|||
setExpiryTrackerCookie | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
renewCookies | |
100.00% |
1 / 1 |
4 | |
100.00% |
7 / 7 |
|||
wipeCookies | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
getCookieExpiration | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
getCookieLifetime | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
hasCookies | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
gender | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
plural | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
dashboardBacklink | |
0.00% |
0 / 1 |
2.01 | |
87.50% |
7 / 8 |
|||
getPromoBox | |
0.00% |
0 / 1 |
9 | |
95.24% |
40 / 42 |
|||
getFooterLine | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getDashboardReturnToUrl | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
redirectTo | |
0.00% |
0 / 1 |
4.10 | |
81.82% |
9 / 11 |
|||
doRedirect | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
isRedirecting | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
parentheses | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
parensWrap | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
dateFormatted | |
0.00% |
0 / 1 |
9.45 | |
63.16% |
12 / 19 |
|||
initLangSelect | |
0.00% |
0 / 1 |
26.97 | |
74.07% |
20 / 27 |
|||
listToText | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 17 |
|||
refreshLang | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
errMsg | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
errTrigger | |
0.00% |
0 / 1 |
30.41 | |
53.12% |
17 / 32 |
|||
isRtl | |
100.00% |
1 / 1 |
4 | |
100.00% |
6 / 6 |
|||
getDir | |
100.00% |
1 / 1 |
3 | |
100.00% |
1 / 1 |
|||
getLangFallback | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getAllRegisteredDomains | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
loadTextdomain | n/a |
0 / 0 |
2 | n/a |
0 / 0 |
|||||
loadTextdomainFromFile | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
<?php declare( strict_types = 1 ); | |
/** | |
* Main class. | |
* | |
* @license MIT | |
* @package krinkle/intuition | |
*/ | |
namespace Krinkle\Intuition; | |
use MessagesFunctions; | |
/** | |
* This file contains the main class which the individual tools will | |
* creating an instance of to use and configure their i18n. | |
*/ | |
class Intuition { | |
/** Message file cache. Does not contain local overrides. See $messageBlob. */ | |
protected static $messageCache = []; | |
/** Fallback cache. Stored as an array of language codes with their fallback as value. */ | |
protected static $fallbackCache = null; | |
public $localBaseDir; | |
/** URL to where intuition-web is installed */ | |
public $dashboardHome = 'https://intuition.toolforge.org'; | |
protected $currentDomain = 'general'; | |
protected $currentLanguage; | |
protected $suppressfatal; | |
protected $suppressnotice; | |
protected $suppressbrackets; | |
protected $stayalive; | |
protected $useRequestParam; | |
/** Changing this will invalidate all cookies. */ | |
protected $cookieNames = [ | |
'userlang' => 'TsIntuition_userlang', | |
'track-expire' => 'TsIntuition_expiry' | |
]; | |
/** Changing this will break existing permalinks. */ | |
protected $paramNames = [ 'userlang' => 'userlang' ]; | |
/** | |
* In-class message storage. | |
* Format: `$messageBlob['domain']['lang']['message-key'] = 'Raw message value';` | |
*/ | |
protected $messageBlob = []; | |
/** | |
* Which domains and languages have been loaded. | |
* | |
* Format: `$loadedDomains['general']['en'] = true;` | |
*/ | |
protected $loadedDomains = []; | |
/** | |
* These codes are mapped to their replacements before loading. | |
* Associated language files may still exist, but will not be used. | |
* Based on MediaWiki 1.26alpha | |
*/ | |
protected $deprecatedLangCodes = [ | |
'als' => 'gsw', | |
'bat-smg' => 'sgs', | |
'be-x-old' => 'be-tarask', | |
'bh' => 'bho', | |
'fiu-vro' => 'vro', | |
'no' => 'nb', | |
'qqq' => 'en', | |
// Support for 'qqx' is hardcoded in rawMsg() | |
'qqx' => 'qqx', | |
'roa-rup' => 'rup', | |
'simple' => 'en', | |
'zh-classical' => 'lzh', | |
'zh-min-nan' => 'nan', | |
'zh-yue' => 'yue', | |
]; | |
/** | |
* Language names are stored as an array of language codes | |
* with their native name as value | |
* Such as as for Spanish: `$langNames['es'] = 'Español';` | |
*/ | |
protected $langNames = null; | |
protected $availableLanguages = []; | |
protected $domainInfos = []; | |
/** These variable names will be extracted from the message files. */ | |
protected $includeVariables = [ 'messages', 'url' ]; | |
/** Redirect address and status. */ | |
protected $redirectTo = null; | |
/** Instance of MessagesFunctions. */ | |
protected $messagesFunctions = null; | |
public static function clearCache() { | |
self::$messageCache = []; | |
self::$fallbackCache = null; | |
} | |
/** | |
* Initialize class | |
* | |
* Pass a string (domain) or array (options) | |
* | |
* Options: | |
* | |
* - lang | |
* - domain | |
* - globalfunctions | |
* - suppressfatal | |
* - suppressnotice | |
* - suppressbrackets | |
* - stayalive | |
* - param | |
*/ | |
public function __construct( $options = [] ) { | |
$this->localBaseDir = dirname( __DIR__ ); | |
if ( is_string( $options ) ) { | |
$options = [ 'domain' => $options ]; | |
} | |
$defaultOptions = [ | |
'domain' => null, | |
'lang' => null, | |
'globalfunctions' => false, | |
'suppressfatal' => false, | |
'suppressnotice' => true, | |
'suppressbrackets' => false, | |
'stayalive' => false, | |
'param' => true, | |
]; | |
$options = array_merge( $defaultOptions, $options ); | |
// The domain of your tool can be set here. | |
// Otherwise defaults to 'general'. See also documentation of msg() | |
// First character is case-insensitive | |
if ( $options['domain'] !== null ) { | |
$this->setDomain( $options['domain'] ); | |
} | |
// Allow a tool to disable the loading of global functions, | |
// in case they have a _() and/or _e() already. | |
if ( $options['globalfunctions'] === true ) { | |
require_once $this->localBaseDir . '/src/Functions.php'; | |
} | |
// Allow a tool to suppress fatals, which hide php fatal errors. | |
$this->suppressfatal = $options['suppressfatal']; | |
// Allow a tool to suppress notices, which hide php notices. | |
$this->suppressnotice = $options['suppressnotice']; | |
// Allow a tool to suppress brackets, msg() will return "Messagekey" instead of "[messagekey]" | |
// if this is true. | |
$this->suppressbrackets = $options['suppressbrackets']; | |
// Allow a tool to prevent exiting/dieing on fatal errors. | |
$this->stayalive = $options['stayalive']; | |
// Choose language based on a cookie. However it can be manually overriden for permalinks | |
// through a request parameter. By default this is 'userlang'. If you need this parameter | |
// for something else you can disable this system here. To avoid inconsistencies between | |
// tools a custom parameter name will not be supported. It's either on or off. | |
$this->setUseRequestParam( $options['param'] ); | |
// A tool may override the automatic initiation with cookies and paramters | |
// (ie. during development). Note you can also override it for individual msg calls, | |
// by passing the language code as third argument to msg(). | |
// If options['lang'] is a non-empty string, initLangSelect will use it, | |
// instead of it's own routine. | |
// Initialize language choise | |
$this->initLangSelect( $options['lang'] ); | |
$this->initHook( $this ); | |
} | |
/** | |
* @codeCoverageIgnore | |
* @param Intuition $intuition | |
*/ | |
public function initHook( Intuition $intuition ) { | |
if ( function_exists( 'intuitionHookInit' ) ) { | |
intuitionHookInit( $intuition ); | |
} elseif ( function_exists( 'TsIntuition_inithook' ) ) { | |
TsIntuition_inithook( $intuition ); | |
} | |
} | |
/** | |
* @return string Language code | |
*/ | |
public function getLang() { | |
return $this->currentLanguage; | |
} | |
/** | |
* Set the current language which will be used when requesting messages etc. | |
* | |
* @param string $lang Language code. If not a valid string, the setting | |
* will remain unchanged and false is returned. | |
* @return bool | |
*/ | |
public function setLang( $lang ) { | |
if ( !Util::nonEmptyStr( $lang ) ) { | |
return false; | |
} | |
// Pre-normalize the lang string to prevent invalid values. | |
$lang = trim( preg_replace( '/[^a-z0-9-]+/', '-', strtolower( (string)$lang ) ), '-' ); | |
if ( !$lang ) { | |
return false; | |
} | |
$this->currentLanguage = $this->normalizeLang( $lang ); | |
return true; | |
} | |
/** | |
* Get an array of common locale values for setlocale(). | |
* @param string|null $lang [optional] Pass a language code. Defaults to current language. | |
* @return array | |
*/ | |
public function getLocale( $lang = null, $utf8 = true ) { | |
$suffixes = $utf8 ? [ '.UTF-8', '.UTF-8', '.utf8' ] : [ '' ]; | |
$normal = isset( $lang ) ? $lang : $this->getLang(); | |
$normalUC = strtoupper( $normal ); | |
// Get 'foo' from 'foo-bar' | |
$parts = explode( '-', $normal ); | |
$short = $parts[0]; | |
$shortUC = strtoupper( $short ); | |
$versions = [ | |
// foo-br or en | |
$normal, | |
// FOO-BR or EN | |
$normalUC, | |
// foo_FOO or en_EN | |
$short . '_' . $shortUC, | |
// foo or en | |
$short, | |
// FOO or EN | |
$shortUC, | |
]; | |
$return = []; | |
foreach ( $versions as $version ) { | |
foreach ( $suffixes as $suffix ) { | |
$return[] = $version . $suffix; | |
} | |
} | |
return array_values( array_unique( $return ) ); | |
} | |
/** | |
* Return the currently selected text domain. | |
* @return string | |
*/ | |
public function getDomain() : string { | |
return $this->currentDomain; | |
} | |
/** | |
* Set the current domain which will be used when requesting messages etc. | |
* | |
* @param string $domain | |
* @return bool Always true | |
*/ | |
public function setDomain( string $domain ) : bool { | |
$this->currentDomain = $this->normalizeDomain( $domain ); | |
return true; | |
} | |
/** | |
* Cookie names may change over time, don't depend on them. | |
* Each cookie-name has an alias (eg. 'userlang' instead of 'pref_userlang') | |
* Use getCookieName() if you only need a single value. | |
* | |
* @return array An array of aliases as keys and actual cookienames as values | |
*/ | |
public function getCookieNames() { | |
return $this->cookieNames; | |
} | |
/** | |
* @param string $name | |
* @return string|null | |
*/ | |
public function getCookieName( $name ) { | |
return isset( $this->cookieNames[$name] ) | |
? $this->cookieNames[$name] | |
: null; | |
} | |
/** | |
* Parameter names may change over time, don't depend on them. | |
* Each paramter-name has an alias (so far the same as the actual value) | |
* Use getParamName() if you only need a single value. | |
* | |
* @return array An array of aliases as keys and actual parameter names as values. | |
*/ | |
public function getParamNames() { | |
return $this->paramNames; | |
} | |
/** | |
* @param string $name | |
* @return string|null | |
*/ | |
public function getParamName( $name ) { | |
return isset( $this->paramNames[$name] ) | |
? $this->paramNames[$name] | |
: null; | |
} | |
/** | |
* Whether or not the userlang-parameter is used to determine the | |
* userlanguage during initialization. | |
* | |
* @return bool | |
*/ | |
public function getUseRequestParam() { | |
return $this->useRequestParam; | |
} | |
/** | |
* Overwrite the setting to use or ignore the userlang-parameter. | |
* Note that it's likely the language intitialization/detection has already | |
* been ran. Call refreshLang() if you want it to re-check the cookies, | |
* parameters, overwrites etc. | |
* | |
* @param bool $bool True if you want it to use the parameter. | |
*/ | |
public function setUseRequestParam( $bool ) { | |
$this->useRequestParam = ( $bool === true ); | |
} | |
/** | |
* Get an instance of MessagesFunctions. | |
* @return MessagesFunction | |
*/ | |
protected function getMessagesFunctions() { | |
if ( $this->messagesFunctions == null ) { | |
$this->messagesFunctions = MessagesFunctions::getInstance( $this->localBaseDir, $this ); | |
} | |
return $this->messagesFunctions; | |
} | |
/** | |
* @param string $domain | |
* @return string | |
*/ | |
protected function normalizeDomain( string $domain ) : string { | |
return strtolower( $domain ); | |
} | |
/** | |
* @param string $lang | |
* @return string | |
*/ | |
protected function normalizeLang( $lang ) { | |
$lang = strtolower( str_replace( '_', '-', $lang ) ); | |
if ( isset( $this->deprecatedLangCodes[$lang] ) ) { | |
return $this->deprecatedLangCodes[$lang]; | |
} | |
return $lang; | |
} | |
/** | |
* Get a message from the message blob. | |
* | |
* @param string $key Message key to retrieve a message for. | |
* For backwards-compatibility, this can also be a non-string value, | |
* in which case it is casted to the empty string and rendered | |
* as non-existent message (e.g. the value of `$fail`, or `[]`). | |
* This is deprecated since v2.2.0. | |
* @param string|array $options [optional] A domain name or an array with one or more | |
* of the following options: | |
* - domain: overrides the currently selected domain, and if needed loads it from disk | |
* - lang: overrides the currently selected language | |
* - variables: numerical array to do variable replacements ($1> var[0], $2> var[1], etc.) | |
* - raw-variables: boolean to determine whether the variables should be escaped as well | |
* - parsemag: boolean to determine whether the message sould be tranformed | |
* using magic phrases (PLURAL, etc.) | |
* - escape: Optionally the return can be escaped. By default this takes place after variable | |
* replacement. Set 'raw-variables' to true if you just want the raw message | |
* to be escaped and have escaped the variables already. | |
* - * 'plain' | |
* - * 'html' (<>"& escaped) | |
* - * 'htmlspecialchars' (alias of 'html') | |
* - * 'htmlentities' (foreign/UTF-8 chars converted as well) | |
* | |
* @param string|false|null $fail [optional] Value if the message doesn't exist. Defaults to null. | |
* @return string|false|null | |
*/ | |
public function msg( $key, $options = [], $fail = null ) { | |
if ( !Util::nonEmptyStr( $key ) ) { | |
// Invalid message key | |
return $this->bracketMsg( '', $fail ); | |
} | |
$defaultOptions = [ | |
'domain' => $this->getDomain(), | |
'lang' => $this->getLang(), | |
'variables' => [], | |
'raw-variables' => false, | |
'escape' => 'plain', | |
'parsemag' => true, | |
'externallinks' => false, | |
// Set to a wiki article path for converting | |
'wikilinks' => false, | |
]; | |
// If $options was a domain string, convert it now. | |
if ( Util::nonEmptyStr( $options ) ) { | |
$options = [ 'domain' => $options ]; | |
} | |
// If $options is still not an array, ignore it and use default | |
// Otherwise merge the options with the defaults. | |
if ( !is_array( $options ) ) { | |
// @codeCoverageIgnoreStart | |
$options = $defaultOptions; | |
} else { | |
// @codeCoverageIgnoreEnd | |
$options = array_merge( $defaultOptions, $options ); | |
} | |
$msg = $this->rawMsg( $options['domain'], $options['lang'], $key ); | |
if ( $msg === null ) { | |
$this->errTrigger( | |
"Message \"$key\" for lang \"{$options['lang']}\" in domain \"{$options['domain']}\" not found", | |
__METHOD__, | |
E_NOTICE | |
); | |
return $this->bracketMsg( $key, $fail ); | |
} | |
// Now that we've got the message, apply any post processing | |
$escapeDone = false; | |
// If using raw variables, escape message before replacement | |
if ( $options['raw-variables'] === true ) { | |
$msg = Util::strEscape( $msg, $options['escape'] ); | |
$escapeDone = true; | |
} | |
// Replace variables | |
foreach ( $options['variables'] as $i => $val ) { | |
$n = $i + 1; | |
$msg = str_replace( "\$$n", strval( $val ), $msg ); | |
} | |
if ( $options['parsemag'] === true ) { | |
$msg = $this->getMessagesFunctions()->parse( $msg, $options['lang'] ); | |
} | |
// If not using raw vars, escape the message now (after variable replacement). | |
if ( !$escapeDone ) { | |
$escapeDone = true; | |
$msg = Util::strEscape( $msg, $options['escape'] ); | |
} | |
if ( is_string( $options['wikilinks'] ) ) { | |
$msg = Util::parseWikiLinks( $msg, $options['wikilinks'] ); | |
} | |
if ( $options['externallinks'] ) { | |
$msg = Util::parseExternalLinks( $msg ); | |
} | |
return $msg; | |
} | |
/** | |
* Get a raw message. (Handles language fallback.) | |
* | |
* @param string $domain | |
* @param string $lang | |
* @param string $key | |
* @return string|null | |
*/ | |
public function rawMsg( string $domain, string $lang, string $key ) : ?string { | |
$domain = $this->normalizeDomain( $domain ); | |
$lang = $this->normalizeLang( $lang ); | |
// Normalise key. First character is case-insensitive. | |
$key = lcfirst( $key ); | |
if ( $lang === 'qqx' ) { | |
return "($domain/$key)"; | |
} | |
return $this->accessBlobWithFallback( $domain, $lang, $key ); | |
} | |
/** | |
* Access message blob directly. (Does not handle fallback.) | |
* | |
* @param string $domain | |
* @param string $lang | |
* @param string $key | |
* @return string|null | |
*/ | |
protected function accessBlob( $domain, $lang, $key ) { | |
if ( !isset( $this->messageBlob[$domain][$lang][$key] ) ) { | |
return null; | |
} | |
return $this->messageBlob[$domain][$lang][$key]; | |
} | |
/** | |
* Internal method for rawMsg() that handles fallbacks. | |
* | |
* If possible, returns the preferred lang right away, otherwise it looks | |
* for a suitable falback | |
* | |
* @param string $domain Normalised domain | |
* @param string $lang Normalised language code of preferred language | |
* @param string $key Key of message | |
* @return string|null | |
*/ | |
protected function accessBlobWithFallback( string $domain, string $lang, string $key ) : ?string { | |
$this->ensureLoaded( $domain, $lang ); | |
$msg = $this->accessBlob( $domain, $lang, $key ); | |
if ( $msg === null ) { | |
// Check fallbacks | |
$fallbacks = $this->getLangFallbacks( $lang ); | |
// @codeCoverageIgnoreStart | |
if ( !in_array( 'en', $fallbacks ) ) { | |
// Ensure 'en' is in the fallback list | |
// (normally added by getLangFallbacks/fetchLangFallbacks) | |
$fallbacks[] = 'en'; | |
} | |
// @codeCoverageIgnoreEnd | |
foreach ( $fallbacks as $fallbackLang ) { | |
$this->ensureLoaded( $domain, $fallbackLang ); | |
$msg = $this->accessBlob( $domain, $fallbackLang, $key ); | |
if ( $msg !== null ) { | |
break; | |
} | |
} | |
} | |
return $msg; | |
} | |
/** | |
* Don't show [brackets] when suppressing errors. | |
* In that case there could be message files missing and invalid language codes chosen. | |
* Just return a somewhat readable string. | |
* We use square brackets for simplicity sake, using inequality brackets (< >) may cause | |
* conflicts with HTML when used wrong. | |
* | |
* @param string $key Name of the key to be used | |
* @param string|false|null $fail [optional] Custom failure return | |
* @return string|false|null | |
*/ | |
public function bracketMsg( string $key, $fail = null ) { | |
if ( $fail !== null ) { | |
return $fail; | |
} | |
if ( $this->suppressbrackets ) { | |
// Keyname | |
return ucfirst( $key ); | |
} | |
// [keyname] | |
return "[$key]"; | |
} | |
/** | |
* Check if a message exists at all, optionally for a given domain and language. | |
* If this returns false it means msg() would return "[message-key]" | |
* | |
* @param string $key The message key. | |
* @param string[] $options Only 'domain' and 'lang' keys are used. | |
* @return bool | |
*/ | |
public function msgExists( string $key, array $options = [] ) : bool { | |
$domain = $options['domain'] ?? $this->getDomain(); | |
$lang = $options['lang'] ?? $this->getLang(); | |
return $this->rawMsg( $domain, $lang, $key ) !== null; | |
} | |
/** | |
* Add or overwrites a message in the blob. | |
* | |
* This function is public so tools can use it while testing their tools | |
* and don't need a message to exist in translatewiki.net yet, but don't want to see [msgkey] | |
* either. See also addMsgs() for registering multiple messages. | |
* | |
* @param string $key | |
* @param string $message | |
* @param string|null $domain [optional] Defaults to current domain | |
* @param string|null $lang [optional] Defaults to current language | |
*/ | |
public function setMsg( | |
string $key, | |
string $message, | |
?string $domain = null, | |
?string $lang = null | |
) : void { | |
$domain = Util::nonEmptyStr( $domain ) | |
? $this->normalizeDomain( $domain ) | |
: $this->getDomain(); | |
$lang = Util::nonEmptyStr( $lang ) | |
? $this->normalizeLang( $lang ) | |
: $this->getLang(); | |
$this->messageBlob[$domain][$lang][$key] = $message; | |
} | |
/** | |
* Set multiple messages in the blob. | |
* | |
* @param string[] $messagesByKey | |
* @param string|null $domain [optional] Defaults to current domain | |
* @param string|null $lang [optional] Defaults to current language | |
*/ | |
public function setMsgs( | |
array $messagesByKey, | |
?string $domain = null, | |
?string $lang = null | |
) : void { | |
foreach ( $messagesByKey as $key => $message ) { | |
$this->setMsg( $key, $message, $domain, $lang ); | |
} | |
} | |
/** | |
* Register a custom domain. | |
* | |
* @param string $domain Name of domain | |
* @param string $dir Path to messages directory | |
* @param array $info [optional] Domain info | |
*/ | |
public function registerDomain( | |
string $domain, | |
string $dir, | |
array $info = [] | |
) : void { | |
$info['dir'] = $dir; | |
$this->domainInfos[ $this->normalizeDomain( $domain ) ] = $info; | |
} | |
/** | |
* Store information related to a domain. | |
* | |
* @param string $domain Name of domain | |
* @param array $info | |
*/ | |
public function addDomainInfo( string $domain, array $info ) : void { | |
$domain = $this->normalizeDomain( $domain ); | |
if ( isset( $this->domainInfos[ $domain ] ) ) { | |
$this->domainInfos[ $domain ] += $info; | |
} | |
} | |
/** | |
* Get information about a domain (if any). | |
* | |
* @param string $domain Name of the domain | |
* @return array|false Array with 'dir' property or false if not found | |
*/ | |
public function getDomainInfo( string $domain ) { | |
$domain = $this->normalizeDomain( $domain ); | |
// Check cache and custom-registered domains | |
if ( !isset( $this->domainInfos[ $domain ] ) ) { | |
// Default to local domain | |
$dir = $this->localBaseDir . '/language/messages/' . $domain; | |
if ( !is_dir( $dir ) ) { | |
// Domain does not exist | |
return false; | |
} | |
$this->domainInfos[ $domain ] = [ | |
'dir' => $dir, | |
]; | |
} | |
return $this->domainInfos[ $domain ]; | |
} | |
/** | |
* Get all known message keys for a domain. | |
* | |
* If the domain is not loaded, this returns an empty list. | |
* This assumes "en" is the source language containing all keys. | |
* | |
* @param string $domain | |
* @return array | |
*/ | |
public function listMsgs( string $domain ) : array { | |
$domain = $this->normalizeDomain( $domain ); | |
$this->ensureLoaded( $domain, 'en' ); | |
// Ignore load failure to allow listing of messages that | |
// were manually registered (in case there are any). | |
if ( !isset( $this->messageBlob[$domain]['en'] ) ) { | |
return []; | |
} | |
return array_keys( $this->messageBlob[$domain]['en'] ); | |
} | |
/* Lang functions | |
* ------------------------------------------------- */ | |
/** | |
* Get fallback chain for a given language. | |
* | |
* @param string $lang Language code | |
* @return string[] List of one or more language codes | |
*/ | |
public function getLangFallbacks( string $lang ) : array { | |
if ( self::$fallbackCache === null ) { | |
// Lazy-initialize | |
self::$fallbackCache = $this->fetchLangFallbacks(); | |
} | |
$lang = $this->normalizeLang( $lang ); | |
return isset( self::$fallbackCache[$lang] ) ? self::$fallbackCache[$lang] : [ 'en' ]; | |
} | |
/** | |
* @return string[] | |
*/ | |
protected function fetchLangFallbacks() : array { | |
$file = $this->localBaseDir . '/language/fallbacks.json'; | |
// @codeCoverageIgnoreStart | |
if ( !is_file( $file ) || !is_readable( $file ) ) { | |
$this->errTrigger( 'Unable to open fallbacks.json', __METHOD__, E_NOTICE ); | |
return []; | |
} | |
// @codeCoverageIgnoreEnd | |
$fallbacks = json_decode( file_get_contents( $file ), true ) ?: []; | |
foreach ( $fallbacks as &$fallback ) { | |
// Expand string values to arrays | |
$fallback = (array)$fallback; | |
// Add English to the end of the fallback chain | |
$fallback[] = 'en'; | |
} | |
return $fallbacks; | |
} | |
/** | |
* Get the language name in the native language. | |
* | |
* @param string|null $lang Language code. Default: Current language. | |
* @return string | |
*/ | |
public function getLangName( ?string $lang = null ) : string { | |
$lang = $lang ? $this->normalizeLang( $lang ) : $this->getLang(); | |
return $this->getLangNames()[$lang] ?? ''; | |
} | |
/** | |
* Get all known languages. | |
* | |
* NOTE: This method includes languages that have no translations. | |
* If you create a "Language selector" that relates to the interface | |
* where Intuition messages are used, use getAvailableLangs() instead. | |
* | |
* @return string[] | |
*/ | |
public function getLangNames() : array { | |
// Lazy-load and cache | |
if ( $this->langNames === null ) { | |
$path = $this->localBaseDir . '/language/mw-classes/Names.php'; | |
// @codeCoverageIgnoreStart | |
if ( !is_readable( $path ) ) { | |
$this->errTrigger( 'Names.php is missing', __METHOD__, E_NOTICE ); | |
$this->langNames = []; | |
return []; | |
} | |
// @codeCoverageIgnoreEnd | |
// Load it | |
require_once $path; | |
$this->langNames = \MediaWiki\Languages\Data\Names::$names; | |
} | |
return $this->langNames; | |
} | |
/** | |
* Get all available languages for the current (or given) domain. | |
* | |
* This will also return additional languages set via addAvailableLang(). | |
* | |
* @param string|null $domain Domain. Default: Current domain. | |
* @return string[] Language names keyed by language code | |
*/ | |
public function getAvailableLangs( ?string $domain = null ) : array { | |
$domain = $domain ?? $this->getDomain(); | |
$domainInfo = $this->getDomainInfo( $domain ); | |
if ( !$domainInfo ) { | |
$languages = []; | |
} else { | |
if ( isset( $domainInfo['langs'] ) ) { | |
$languages = $domainInfo['langs']; | |
} else { | |
$files = @scandir( $domainInfo['dir'], SCANDIR_SORT_NONE ) ?: []; | |
$languages = []; | |
foreach ( $files as $filename ) { | |
$langCode = basename( $filename, '.json' ); | |
$langName = $this->getLangName( $langCode ); | |
if ( $langName !== '' ) { | |
$languages[$langCode] = $langName; | |
} | |
} | |
$this->addDomainInfo( $domain, [ 'langs' => $languages ] ); | |
} | |
} | |
$languages = array_merge( $languages, $this->availableLanguages ); | |
ksort( $languages ); | |
return $languages; | |
} | |
/** | |
* Add a language that isn't listed in Intuition's included language list. | |
* | |
* @since 1.1.0 | |
* @param string $code Language code | |
* @param string $name Localized name of the language | |
*/ | |
public function addAvailableLang( string $code, string $name ) : void { | |
// Initialise $this->langNames so that we can extend it | |
$this->getLangNames(); | |
$normalizedCode = $this->normalizeLang( $code ); | |
$this->langNames[$normalizedCode] = $name; | |
$this->availableLanguages[$normalizedCode] = $name; | |
} | |
/** | |
* Ensure a domain's language is loaded. | |
* | |
* @param string $domain Name of the domain | |
* @param string $lang Language code | |
* @return bool | |
*/ | |
protected function ensureLoaded( string $domain, string $lang ) : bool { | |
if ( isset( $this->loadedDomains[ $domain ][ $lang ] ) ) { | |
// Already tried | |
return $this->loadedDomains[ $domain ][ $lang ]; | |
} | |
// Validate input and protect against path traversal | |
if ( !Util::nonEmptyStrs( $domain, $lang ) || | |
strcspn( $domain, ":/\\\000" ) !== strlen( $domain ) || | |
strcspn( $lang, ":/\\\000" ) !== strlen( $lang ) | |
) { | |
$this->errTrigger( 'Illegal domain or lang', __METHOD__, E_NOTICE ); | |
return false; | |
} | |
$this->loadedDomains[ $domain ][ $lang ] = false; | |
if ( !isset( self::$messageCache[ $domain ][ $lang ] ) ) { | |
// Load from disk | |
$domainInfo = $this->getDomainInfo( $domain ); | |
if ( !$domainInfo ) { | |
// Unknown domain. Perhaps dev-mode only with | |
// messages provided via setMsgs()? | |
return false; | |
} | |
$file = $domainInfo['dir'] . "/$lang.json"; | |
$this->loadMessageFile( $domain, $lang, $file ); | |
} else { | |
// Load from static cache, e.g. from a previous instance of this class | |
$this->setMsgs( self::$messageCache[ $domain ][ $lang ], $domain, $lang ); | |
} | |
$this->loadedDomains[ $domain ][ $lang ] = true; | |
return true; | |
} | |
/** | |
* @param string $domain Normalised domain | |
* @param string $lang Normalised language code | |
* @param string $file | |
* @return bool | |
*/ | |
public function loadMessageFile( string $domain, string $lang, string $file ) : bool { | |
// Prefer EAFP over LBYL, | |
// for performance and to avoid race bugs. | |
$json = @file_get_contents( $file ); | |
if ( !$json ) { | |
// Most domains don't have translations in every single language. | |
return false; | |
} | |
$messages = json_decode( $json, true ); | |
// @codeCoverageIgnoreStart | |
if ( !is_array( $messages ) ) { | |
return false; | |
} | |
// @codeCoverageIgnoreEnd | |
unset( $messages['@metadata'] ); | |
self::$messageCache[ $domain ][ $lang ] = $messages; | |
$this->setMsgs( $messages, $domain, $lang ); | |
return true; | |
} | |
/** | |
* @param string $domain | |
* @return bool | |
*/ | |
protected function isLocalDomain( string $domain ) : bool { | |
$domain = $this->normalizeDomain( $domain ); | |
return is_dir( $this->localBaseDir . '/language/messages/' . $domain ); | |
} | |
/** | |
* Set a cookie. | |
* | |
* @param string $key Canonical name of the cookie. | |
* @param string $val Value to be set. | |
* @param int $lifetime Lifetime in seconds from now (defaults to 30 days). | |
* @param bool $track Whether to set a expiry-tracking cookie. | |
* @return bool | |
*/ | |
public function setCookie( | |
string $key, | |
string $val, | |
int $lifetime = 2592000, | |
bool $track = TSINT_COOKIE_TRACK | |
) : bool { | |
// Validate cookie name | |
$name = $this->getCookieName( $key ); | |
if ( !$name ) { | |
return false; | |
} | |
$val = strval( $val ); | |
$lifetime = intval( $lifetime ); | |
$expire = time() + $lifetime; | |
// Set a 30-day domain-wide cookie | |
setcookie( $name, $val, $expire, '/' ); | |
// In order to keep track of the expiration date, we set another cookie | |
if ( $track === TSINT_COOKIE_TRACK ) { | |
$this->setExpiryTrackerCookie( $lifetime ); | |
} | |
return true; | |
} | |
/** | |
* Browsers don't send the expiration date of cookies with the request | |
* In order to keep track of the expiration date, we set an additional cookie. | |
* | |
* @param int $lifetime | |
* @return bool Always true | |
*/ | |
protected function setExpiryTrackerCookie( int $lifetime ) : bool { | |
$val = time() + $lifetime; | |
$this->setCookie( 'track-expire', (string)$val, $lifetime, TSINT_COOKIE_NOTRACK ); | |
return true; | |
} | |
/** | |
* Renew all cookies | |
* | |
* @param int $lifetime [optional] Defaults to 30 days | |
* @return bool Always true | |
*/ | |
public function renewCookies( int $lifetime = 2592000 ) : bool { | |
foreach ( $this->getCookieNames() as $key => $name ) { | |
if ( $key === 'track-expire' ) { | |
continue; | |
} | |
if ( isset( $_COOKIE[$name] ) ) { | |
$this->setCookie( $key, $_COOKIE[$name], $lifetime, TSINT_COOKIE_NOTRACK ); | |
} | |
} | |
$this->setExpiryTrackerCookie( $lifetime ); | |
return true; | |
} | |
/** | |
* Delete all cookies. | |
* | |
* It's recommended to redirectTo() directly after this. | |
* | |
* @return bool Always true | |
*/ | |
public function wipeCookies() : bool { | |
foreach ( $this->getCookieNames() as $key => $name ) { | |
$this->setCookie( $key, '', -3600, TSINT_COOKIE_NOTRACK ); | |
unset( $_COOKIE[$name] ); | |
} | |
return true; | |
} | |
/** | |
* Get expiration timestamp. | |
* | |
* @return int Unix timestamp of expiration date or 0 if not available. | |
*/ | |
public function getCookieExpiration() : int { | |
$name = $this->getCookieName( 'track-expire' ); | |
return isset( $_COOKIE[$name] ) ? intval( $_COOKIE[$name] ) : 0; | |
} | |
/** | |
* Get remaining lifetime in seconds. | |
* | |
* @return int Lifetime, or 0 if expired or unavailable. | |
*/ | |
public function getCookieLifetime() : int { | |
$expire = $this->getCookieExpiration(); | |
$lifetime = $expire - time(); | |
// If already expired (-xxx), return 0 | |
return $lifetime > 0 ? $lifetime : 0; | |
} | |
public function hasCookies() : bool { | |
return isset( $_COOKIE[ $this->getCookieName( 'userlang' ) ] ); | |
} | |
/** | |
* @todo FIXME: Implement in language/MessagesFunctions.php. | |
* @codeCoverageIgnore | |
*/ | |
public function gender( $male, $female, $neutral ) { | |
// Depends on getGender() which doesn't exist yet | |
throw new BadMethodCallException( 'Not supported yet!' ); | |
} | |
/** | |
* Can be founded in language/MessagesFunctions.php. | |
* | |
* @see MessagesFunctions::parse | |
* @see MessagesFunctions::plural | |
* @deprecated | |
* @codeCoverageIgnore | |
*/ | |
public function plural( $count, $forms ) { | |
throw new BadMethodCallException( | |
"Use msg() with \"parse\" option to support PLURAL!" | |
); | |
} | |
/* Output promo and dashboard backlinks | |
* ------------------------------------------------- */ | |
/** | |
* Get HTML link to Intuition dashboard page. | |
* | |
* Use this to indicate that your tool has been localized via Intuition, | |
* and that users can use this link to change the interface language. | |
* | |
* @return string HTML | |
*/ | |
public function dashboardBacklink() : string { | |
if ( $this->hasCookies() ) { | |
$text = $this->msg( 'bl-mysettings', 'tsintuition' ); | |
} else { | |
$text = $this->msg( 'bl-mysettings-new', 'tsintuition' ); | |
} | |
return Util::tag( | |
$text, | |
'a', | |
[ | |
'class' => 'int-dashboardbacklink', | |
'href' => $this->getDashboardReturnToUrl(), | |
'title' => $this->msg( 'bl-changelanguage', 'tsintuition' ), | |
] | |
); | |
} | |
/** | |
* Show a promobox on the bottom of your tool. | |
* | |
* @param int $imgSize [optional] Defaults to 28px. | |
* Set to 0 to omit the image from the promobox. | |
* @param bool|string|null $helpTranslateDomain [optional] Provide a link to translatewiki.net | |
* where this tool can be translated. | |
* - TSINT_HELP_CURRENT (null): Link to translations for the current domain. | |
* - TSINT_HELP_ALL (true): Link to translations for all domains. | |
* - TSINT_HELP_NONE (false): Disable this link. | |
* - string value: Link to translations for the specified domain. | |
* @return string The HTML for the promo box. | |
*/ | |
public function getPromoBox( | |
int $imgSize = 28, | |
$helpTranslateDomain = TSINT_HELP_CURRENT | |
) : string { | |
// Logo | |
if ( is_int( $imgSize ) && $imgSize > 0 ) { | |
$src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Tool_labs_logo.svg/' | |
. '/' . $imgSize . 'px-Tool_labs_logo.svg.png'; | |
$src_2x = 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Tool_labs_logo.svg/' | |
. '/' . ( $imgSize * 2 ) . 'px-Tool_labs_logo.svg.png'; | |
$img = Util::tag( '', 'img', [ | |
'src' => $src, | |
'srcset' => "$src 1x, $src_2x 2x", | |
'width' => $imgSize, | |
'height' => $imgSize, | |
'alt' => '', | |
'title' => '', | |
'class' => 'int-logo', | |
] ); | |
} else { | |
$img = ''; | |
} | |
// Promo message | |
$promoMsgOpts = [ | |
'domain' => 'tsintuition', | |
'escape' => 'html', | |
'raw-variables' => true, | |
'variables' => [ | |
'<a href="https://translatewiki.net/">translatewiki.net</a>', | |
'<a href="' . $this->dashboardHome . '">Intuition</a>' | |
], | |
]; | |
$poweredHtml = $this->msg( 'bl-promo', $promoMsgOpts ); | |
// "Help translation" link | |
$translateGroup = null; | |
if ( $helpTranslateDomain === TSINT_HELP_ALL ) { | |
$translateGroup = 'tsint-0-all'; | |
$twLinkText = $this->msg( 'help-translate-all', 'tsintuition' ); | |
} elseif ( $helpTranslateDomain === TSINT_HELP_CURRENT ) { | |
$domain = $this->getDomain(); | |
$translateGroup = $this->isLocalDomain( $domain ) ? "tsint-{$domain}" : "int-{$domain}"; | |
$twLinkText = $this->msg( 'help-translate-tool', 'tsintuition' ); | |
} elseif ( $helpTranslateDomain !== TSINT_HELP_NONE ) { | |
// Custom domain | |
$domain = $this->normalizeDomain( $helpTranslateDomain ); | |
$translateGroup = $this->isLocalDomain( $domain ) ? "tsint-{$domain}" : "int-{$domain}"; | |
$twLinkText = $this->msg( 'help-translate-tool', 'tsintuition' ); | |
} | |
$helpTranslateLink = ''; | |
if ( $translateGroup ) { | |
// https://translatewiki.net/w/i.php?language=nl&title=Special:Translate&group=tsint-0-all | |
$twParams = [ | |
'title' => 'Special:Translate', | |
'language' => $this->getLang(), | |
'group' => $translateGroup, | |
]; | |
$twParams = http_build_query( $twParams ); | |
$helpTranslateLink = '<small>(' . Util::tag( $twLinkText, 'a', [ | |
'href' => "https://translatewiki.net/w/i.php?$twParams", | |
'title' => $this->msg( 'help-translate-tooltip', 'tsintuition' ) | |
] ) . ')</small>'; | |
} | |
// Build output | |
return '<div class="int-promobox"><p><a href="' . | |
htmlspecialchars( $this->getDashboardReturnToUrl() ) | |
. "\">$img</a> " | |
. "$poweredHtml {$this->dashboardBacklink()} $helpTranslateLink</p></div>"; | |
} | |
/** | |
* Show a typical "powered by .." footer line. | |
* | |
* Same as getPromoBox() but without the image. | |
* | |
* @return string HTML | |
*/ | |
public function getFooterLine( $helpTranslateDomain = TSINT_HELP_CURRENT ) : string { | |
return $this->getPromoBox( 0, $helpTranslateDomain ); | |
} | |
/** | |
* Build a permalink to the dashboard with a returnto query | |
* to return to the current page. To be used in other tools. | |
* | |
* Example: | |
* | |
* Location: https://tools.wmflabs.org/example/foo.php?bar=baz | |
* HTML: | |
* '<p>Change the settings <a href="' . $I18N->getDashboardReturnToUrl() . '">here</a>'; | |
* | |
* @return string URL | |
*/ | |
public function getDashboardReturnToUrl() : string { | |
$p = [ | |
'returnto' => $_SERVER['SCRIPT_NAME'], | |
'returntoquery' => http_build_query( $_GET ), | |
]; | |
return rtrim( $this->dashboardHome, '/' ) | |
. '/?' | |
. http_build_query( $p ) | |
. '#tab-settingsform'; | |
} | |
/** | |
* Redirect or refresh to url. Pass null to undo redirection. | |
* | |
* @param string|null $url URL or null to undo any prior redirection. | |
* @param int $code [optional] Defaults to 302 | |
* @return bool | |
*/ | |
public function redirectTo( $url = null, $code = 302 ) : bool { | |
if ( $url === null ) { | |
$this->redirectTo = null; | |
return true; | |
} | |
if ( !is_string( $url ) ) { | |
// Deprecated since v2.2.0 | |
trigger_error( __METHOD__ . ' argument $url must be of type string', E_USER_DEPRECATED ); | |
return false; | |
} | |
if ( !is_int( $code ) ) { | |
// Deprecated since v2.2.0 | |
trigger_error( __METHOD__ . ' argument $url must be of type string', E_USER_DEPRECATED ); | |
return false; | |
} | |
$this->redirectTo = [ $url, $code ]; | |
return true; | |
} | |
public function doRedirect() { | |
if ( !is_array( $this->redirectTo ) ) { | |
return false; | |
} | |
header( 'Content-Type: text/html; charset=utf-8' ); | |
header( 'Location: ' . $this->redirectTo[0], true, $this->redirectTo[1] ); | |
exit; | |
} | |
public function isRedirecting() : bool { | |
return is_array( $this->redirectTo ); | |
} | |
/** | |
* @param string|string[] ...$args | |
* @return string | |
*/ | |
public function parentheses( ...$args ) : string { | |
$msg = call_user_func_array( | |
[ $this, 'msg' ], | |
$args | |
); | |
return $this->parensWrap( $msg ); | |
} | |
/** | |
* @param string $content Text or HTML to be wrapped in parentheses. | |
* @param string $escape Any valid format for Util::strEscape. | |
* @return string | |
*/ | |
public function parensWrap( $content, $escape = 'plain' ) : string { | |
return $this->msg( | |
'parentheses', | |
[ | |
'domain' => 'general', | |
'raw-variables' => true, | |
'variables' => [ Util::strEscape( $content, $escape ) ], | |
] | |
); | |
} | |
/** | |
* Get a localized date. Pass a format, time or both. | |
* Defaults to the current timestamp in the language's default date format. | |
* | |
* @param string|int|null $first Date format compatible with strftime() | |
* @param string|int|null $second Timestamp (seconds since unix epoch) or string (ie. "2011-12-31") | |
* @param string|null $lang Language code. Default: Current language. | |
* @return string | |
*/ | |
public function dateFormatted( | |
$first = null, | |
$second = null, | |
?string $lang = null | |
) : string { | |
// One argument or less | |
if ( $second === null ) { | |
// No arguments | |
if ( $first === null ) { | |
$format = $this->msg( 'dateformat', 'general' ); | |
$timestamp = time(); | |
// Timestamp only | |
} elseif ( is_int( $first ) ) { | |
$format = $this->msg( 'dateformat', 'general' ); | |
$timestamp = $first; | |
// Date string only | |
} elseif ( strtotime( $first ) ) { | |
$format = $this->msg( 'dateformat', 'general' ); | |
$timestamp = strtotime( $first ); | |
// Format only | |
} else { | |
$format = $first; | |
$timestamp = time(); | |
} | |
// Two arguments | |
} else { | |
$format = $first; | |
$timestamp = is_int( $second ) ? $second : strtotime( $second ); | |
} | |
// Save current setlocale | |
$saved = setlocale( LC_ALL, 0 ); | |
// Overwrite for current language | |
setlocale( LC_ALL, $this->getLocale( $lang ) ); | |
$return = strftime( $format, $timestamp ); | |
// Reset back to what it was | |
setlocale( LC_ALL, $saved ); | |
return $return; | |
} | |
/** | |
* Check language choice tree. | |
* | |
* In the following order: | |
* | |
* 1. Constructor option. | |
* 2. Request parameter. | |
* 3. Request cookie. | |
* 4. Request Accept-Language header (exactly). | |
* 5. Request Accept-Language header (prefix match). | |
* 6. Source fallback (English). | |
* | |
* @param string|null|bool $option A language code, or null/false to traverse further down the | |
* choice tree. | |
* @return bool Whether a language was set or not. | |
*/ | |
protected function initLangSelect( $option = null ) : bool { | |
if ( $option !== null && | |
$option !== false && | |
$option !== '' && | |
$this->setLang( $option ) | |
) { | |
return true; | |
} | |
if ( $this->getUseRequestParam() ) { | |
$key = $this->paramNames['userlang']; | |
if ( isset( $_GET[ $key ] ) && $this->setLang( $_GET[ $key ] ) ) { | |
return true; | |
} | |
if ( isset( $_POST[ $key ] ) && $this->setLang( $_POST[ $key ] ) ) { | |
return true; | |
} | |
} | |
if ( isset( $_COOKIE[ $this->cookieNames['userlang'] ] ) ) { | |
$set = $this->setLang( $_COOKIE[ $this->cookieNames['userlang'] ] ); | |
if ( $set ) { | |
return true; | |
} | |
} | |
$acceptableLanguages = Util::getAcceptableLanguages(); | |
foreach ( $acceptableLanguages as $acceptLang => $qVal ) { | |
// If the lang code is known (we have a display name for it), | |
// and we were able to set it, end the search. | |
if ( $this->getLangName( $acceptLang ) && $this->setLang( $acceptLang ) ) { | |
return true; | |
} | |
} | |
// After this, we'll be choosing from | |
// user-specified languages with a $qVal of 0. | |
foreach ( $acceptableLanguages as $acceptLang => $qVal ) { | |
// Some browsers show (apparently by default) only a tag, | |
// such as "ru-RU", "fr-FR" or "es-mx". The browser should | |
// provide a qval. Providing only a lang code is invalid. | |
// See RFC 2616 section 1.4 <https://tools.ietf.org/html/rfc2616#page-105>. | |
if ( !$qVal ) { | |
continue; | |
} | |
// Progressively truncate $acceptLang (from the right) to each hyphen, | |
// checking each time to see if the remaining string is an available language. | |
while ( strpos( $acceptLang, '-' ) !== false ) { | |
$acceptLang = substr( $acceptLang, 0, strrpos( $acceptLang, '-' ) ); | |
if ( $this->getLangName( $acceptLang ) && $this->setLang( $acceptLang ) ) { | |
return true; | |
} | |
} | |
} | |
// Fallback | |
return (bool)$this->setLang( 'en' ); | |
} | |
/** | |
* Take a list of strings and build a locale-friendly comma-separated | |
* list, using the local comma-separator message. | |
* The last two strings are chained with an "and". | |
* | |
* @param array $l | |
* @return string | |
*/ | |
public function listToText( array $l ) : string { | |
$s = ''; | |
$m = count( $l ) - 1; | |
if ( $m == 1 ) { | |
$s = $l[0] . $this->msg( 'and', [ 'domain' => 'general' ] ) . | |
$this->msg( 'word-separator', [ 'domain' => 'general' ] ) . | |
$l[1]; | |
} else { | |
for ( $i = $m; $i >= 0; $i-- ) { | |
if ( $i == $m ) { | |
$s = $l[$i]; | |
} elseif ( $i == $m - 1 ) { | |
$s = $l[$i] . $this->msg( 'and', [ 'domain' => 'general' ] ) . | |
$this->msg( 'word-separator', [ 'domain' => 'general' ] ) . | |
$s; | |
} else { | |
$s = $l[$i] . | |
$this->msg( 'comma-separator', [ 'domain' => 'general' ] ) . | |
$s; | |
} | |
} | |
} | |
return str_replace( ' ', ' ', $s ); | |
} | |
/** | |
* Re-intiialise the language selection. | |
* | |
* Call this if you've changed the default domain or added additional | |
* available languages after constucting the object. | |
* | |
* @return bool Always true | |
*/ | |
public function refreshLang() : bool { | |
$this->initLangSelect(); | |
return true; | |
} | |
/** | |
* @param string $msg | |
* @param string $context | |
* @return string | |
*/ | |
protected function errMsg( string $msg, string $context ) : string { | |
return "[$context] $msg"; | |
} | |
/** | |
* Custom version of trigger_error() that can be passed a custom filename and line number | |
* | |
* @param string $msg | |
* @param string $context | |
* @param int $level [optional] | |
*/ | |
public function errTrigger( string $msg, string $context, int $level = E_WARNING ) : void { | |
$die = false; | |
$error = false; | |
$notice = false; | |
// Create $code string, decide error/die behaviour | |
// and cast to USER constant as required by trigger_error(). | |
switch ( $level ) { | |
// Fatal | |
case E_ERROR: | |
case E_USER_ERROR: | |
$level = E_USER_ERROR; | |
$code = 'Fatal error'; | |
$error = true; | |
$die = true; | |
break; | |
// Warning | |
case E_WARNING: | |
case E_USER_WARNING: | |
$level = E_USER_WARNING; | |
$code = 'Warning'; | |
$error = true; | |
break; | |
// Notice | |
case E_NOTICE: | |
case E_USER_NOTICE: | |
$level = E_USER_NOTICE; | |
$code = 'Notice'; | |
$notice = true; | |
break; | |
// Unknown | |
default: | |
$code = 'Unknown error'; | |
} | |
if ( $error && $this->suppressfatal ) { | |
return; | |
} | |
if ( $notice && $this->suppressnotice ) { | |
return; | |
} | |
$errorMsg = "$code: [$context] $msg"; | |
trigger_error( $errorMsg, $level ); | |
if ( $die && !$this->stayalive ) { | |
die; | |
} | |
} | |
/** | |
* Whether the language is right-to-left | |
* | |
* @param string|null $lang Language code to get the property from, | |
* current language if missing | |
* @return bool | |
*/ | |
public function isRtl( ?string $lang = null ) : bool { | |
static $rtlLanguages = null; | |
$lang = $lang ? $this->normalizeLang( $lang ) : $this->getLang(); | |
if ( $rtlLanguages === null ) { | |
$file = $this->localBaseDir . '/language/rtl.json'; | |
$rtlLanguages = json_decode( file_get_contents( $file ), true ); | |
} | |
return in_array( $lang, $rtlLanguages ); | |
} | |
/** | |
* Return the correct HTML 'dir' attribute value for this language. | |
* | |
* @param string|null $lang | |
* @return string One of "ltr" or "rtl". | |
*/ | |
public function getDir( ?string $lang = null ) : string { | |
return $this->isRtl( $lang ) ? 'rtl' : 'ltr'; | |
} | |
/** | |
* Return the fallback language for a given language. | |
* | |
* @deprecated since 0.2.0: Use #getLangFallbacks instead | |
* @param string $lang Language code | |
* @return string Language code | |
* @codeCoverageIgnore | |
*/ | |
public function getLangFallback( $lang ) { | |
$fallbacks = $this->getLangFallbacks( $lang ); | |
return $fallbacks[0]; | |
} | |
/** | |
* Get a list of registered text domains. | |
* | |
* @deprecated since 0.2.0: Obsolete. To get available languages, use getAvailableLangs() | |
* @return array | |
* @codeCoverageIgnore | |
*/ | |
public function getAllRegisteredDomains() { | |
return []; | |
} | |
/** | |
* @deprecated since 0.2.0: Use #ensureLoaded instead. | |
* @param string $domain | |
* @return bool|string Normalised domain name or boolean false | |
* @codeCoverageIgnore | |
*/ | |
public function loadTextdomain( $domain ) { | |
$domain = $this->normalizeDomain( $domain ); | |
if ( !$this->ensureLoaded( $domain, $this->getLang() ) ) { | |
return false; | |
} | |
return $domain; | |
} | |
/** | |
* @deprecated since 0.2.0: Use #loadMessageFile instead. | |
* @param string $filePath | |
* @param string $domain | |
* @return bool | |
* @codeCoverageIgnore | |
*/ | |
public function loadTextdomainFromFile( $filePath, $domain ) { | |
return false; | |
} | |
} |