> locale > domain = translator */ private array $domains = []; /** @var array bound paths for domains */ private array $paths = ['' => './']; // files /** @var string the full path to the mo file to loaded */ private string $mofile = ''; /** @var string base path to search level */ private string $base_locale_path = ''; /** @var string dynamic set path to where the mo file is actually */ private string $base_content_path = ''; // errors /** @var bool if load of mo file was unsuccessful */ private bool $load_failure = false; // object holders /** @var FileReader|bool reader class for file reading, false for short circuit */ private FileReader|bool $input = false; /** @var GetTextReader reader class for MO data */ private GetTextReader|null $l10n = null; /** * @static * @var L10n self class */ private static L10n $instance; /** * class constructor call for language getstring * if locale is not empty will load translation * else getTranslator needs to be called * * @param string $locale language name, default empty string * will return self instance * @param string $domain override CONTENT_PATH . $encoding name for mo file * @param string $path path, if empty fallback on default internal path * @param string $encoding Optional encoding, should be set if locale has * no encoding, defaults to UTF-8 */ public function __construct( string $locale = '', string $domain = '', string $path = '', string $encoding = '' ) { // auto load language only if at least locale and domain is set // New: path must be set too, or we fall through if (!empty($locale) && !empty($domain) && empty($path)) { /** @deprecated if locale and domain are set, path must be set too */ trigger_error( 'Empty path parameter is no longer allowed if locale and domain are set', E_USER_DEPRECATED ); } if (!empty($locale) && !empty($domain) && !empty($path)) { // check hack if domain and path is switched // Note this can be removed in future versions if (strstr($domain, DIRECTORY_SEPARATOR) !== false) { /** @deprecated domain must be 2nd and path must be third parameter */ trigger_error( 'L10n constructor parameter switch is no longer supported. domain is 2nd, path is 3rd parameter', E_USER_DEPRECATED ); $_domain = $path; $path = $domain; $domain = $_domain; } $this->getTranslator($locale, $domain, $path, $encoding); } } /** * Returns the singleton L10n object. * For function wrapper use * * @return L10n object */ public static function getInstance(): L10n { if (empty(self::$instance)) { self::$instance = new self(); } return self::$instance; } /** * Loads global localization functions. * prefixed with double underscore * eg: gettext -> __gettext */ public static function loadFunctions(): void { require_once __DIR__ . '/l10n_functions.php'; } /** * loads the mo file base on path, locale and domain set * * @param string $locale language name, if not set, try previous set * @param string $domain set name for mo file, if not set, try previous set * @param string $path path, if not set try to get from paths array, else self * @param string $override_encoding if locale does not env encoding set, use this one * @return GetTextReader the main gettext reader object */ public function getTranslator( string $locale = '', string $domain = '', string $path = '', string $override_encoding = '', ): GetTextReader { // set local if not from parameter if (empty($locale)) { $locale = $this->locale; } // set domain if not given if (empty($domain)) { $domain = $this->domain; } // override encoding for unset if (!empty($override_encoding)) { $this->override_encoding = $override_encoding; } // store old settings $old_mofile = $this->mofile; $old_lang = $this->locale; $old_lang_set = $this->locale_set; $old_domain = $this->domain; $old_encoding = $this->encoding; $old_base_locale_path = $this->base_locale_path; $old_base_content_path = $this->base_content_path; // if path is a dir // 1) from a previous set domain // 2) from method option as is // 3) fallback if BASE/INCLUDES/LOCALE set // 4) current dir if (!empty($this->paths[$domain]) && is_dir($this->paths[$domain])) { $this->base_locale_path = $this->paths[$domain]; } elseif (is_dir($path)) { $this->base_locale_path = $path; } elseif ( defined('BASE') && defined('INCLUDES') && defined('LOCALE') ) { /** @deprecated Do not use this anymore, define path on class load */ trigger_error( 'parameter $path must be set. Setting via BASE, INCLUDES and LOCALE constants is deprecated', E_USER_DEPRECATED ); // set fallback base path if constant set $this->base_locale_path = BASE . INCLUDES . LOCALE; } else { $this->base_locale_path = './'; } // now we loop over lang compositions to get the base path // then we check $locales = $this->listLocales($locale); $encoding = $this->getEncodingFromLocale($locale); foreach ($locales as $_locale) { $this->base_content_path = $_locale . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR; $this->mofile = $this->base_locale_path . $this->base_content_path . $domain . '.mo'; if (file_exists($this->mofile)) { $this->locale_set = $_locale; break; } } // check if get a readable mofile if (is_readable($this->mofile)) { // locale and domain current wanted $this->locale = $locale; $this->encoding = $encoding; $this->domain = $domain; // set empty domains path with current locale if (empty($this->domains[$locale])) { $this->domains[$locale] = []; } // store current base path (without locale, etc) if (empty($this->paths[$domain])) { $this->paths[$domain] = $this->base_locale_path; } // file reader and mo reader $this->input = new FileReader($this->mofile); $this->l10n = new GetTextReader($this->input); // if short circuit is true, we failed to have a translator loaded $this->load_failure = $this->l10n->getShortCircuit(); // below is not used at the moment, but can be to avoid reloading $this->domains[$this->locale][$domain] = $this->l10n; } elseif (!empty($old_mofile)) { // mo file not readable $this->load_failure = true; // else fall back to the old ones $this->mofile = $old_mofile; $this->locale = $old_lang; $this->locale_set = $old_lang_set; $this->encoding = $old_encoding; $this->domain = $old_domain; $this->base_locale_path = $old_base_locale_path; $this->base_content_path = $old_base_content_path; } else { // mo file not readable, no previous mo file set, set short circuit $this->load_failure = true; // dummy $this->l10n = new GetTextReader($this->input); } // if this is still null here, we abort if ($this->l10n === null) { throw new \RuntimeException( "Could not create CoreLibs\Language\Core\GetTextReader object", E_USER_ERROR ); } return $this->l10n; } /** * return current set GetTextReader or return the one for given * domain name if set * This can be used to access all the public methods from the * GetTextReader * * @param string $domain optional domain name * @return GetTextReader */ public function getTranslatorClass(string $domain = ''): GetTextReader { if (!empty($domain) && !empty($this->domains[$this->locale][$domain])) { return $this->domains[$this->locale][$domain]; } // if null return short circuit version if ($this->l10n === null) { return new GetTextReader($this->input); } return $this->l10n; } /** * Extract encoding from Locale, or fallback to override one if not set * * @param string $locale * @return string */ private function getEncodingFromLocale(string $locale): string { // extract charset from $locale // if not set get override encoding preg_match('/(?:\\.(?P[-A-Za-z0-9_]+))/', $locale, $matches); return $matches['charset'] ?? $this->override_encoding; } /** * Get the local as array same to the GetLocale::setLocale return * This does not set from outside, but only what is set in the l10n class * * @return array{locale: string, lang: string, lang_short: string, domain: string, encoding: string, path: string} */ public function getLocaleAsArray(): array { $locale = L10n::parseLocale($this->getLocale()); return [ 'locale' => $this->getLocale(), 'lang' => ($locale['lang'] ?? '') . (!empty($locale['country']) ? '_' . $locale['country'] : ''), 'lang_short' => $locale['lang'] ?? '', 'domain' => $this->getDomain(), 'encoding' => $this->getEncoding(), 'path' => $this->getBaseLocalePath(), ]; } /** * parse the locale string for further processing * * @param string $locale Locale to parse * @return array array with lang, country, charset, modifier */ public static function parseLocale(string $locale = ''): array { preg_match( // language code '/^(?P[a-z]{2,3})' // country code . '(?:_(?P[A-Z]{2}))?' // charset . '(?:\\.(?P[-A-Za-z0-9_]+))?' // @ modifier . '(?:@(?P[-A-Za-z0-9_]+))?$/', $locale, $matches ); return [ 'lang' => $matches['lang'] ?? null, 'country' => $matches['country'] ?? null, 'charset' => $matches['charset'] ?? null, 'modifier' => $matches['modifier'] ?? null, ]; } /** * original: * vendor/phpmyadmin/motranslator/src/Loader.php * * Returns array with all possible locale combinations based on the * given locale name * * I.e. for sr_CS.UTF-8@latin, look through all of * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr. * * @param string $locale Locale string * @return array List of locale path parts that can be possible */ public static function listLocales(string $locale): array { $locale_list = []; if (empty($locale)) { return $locale_list; } // is matching regex $locale_detail = L10n::parseLocale($locale); // all null = nothing mached, return locale as is if ($locale_detail === array_filter($locale_detail, 'is_null')) { return [$locale]; } // write to innteral vars $lang = $locale_detail['lang']; $country = $locale_detail['country']; $charset = $locale_detail['charset']; $modifier = $locale_detail['modifier']; // we need to add all possible cominations from not null set // entries to the list, from longest to shortest // %s_%s.%s@%s (lang _ country . encoding @ suffix) // %s_%s@%s (lang _ country @ suffix) // %s@%s (lang @ suffix) // %s_%s.%s (lang _ country . encoding) // %s_%s (lang _ country) // %s (lang) // if lang is set if ($lang) { // modifier group if ($modifier) { if ($country) { if ($charset) { array_push( $locale_list, sprintf('%s_%s.%s@%s', $lang, $country, $charset, $modifier) ); } array_push( $locale_list, sprintf('%s_%s@%s', $lang, $country, $modifier) ); } elseif ($charset) { array_push( $locale_list, sprintf('%s.%s@%s', $lang, $charset, $modifier) ); } array_push( $locale_list, sprintf('%s@%s', $lang, $modifier) ); } // country group if ($country) { if ($charset) { array_push( $locale_list, sprintf('%s_%s.%s', $lang, $country, $charset) ); } array_push( $locale_list, sprintf('%s_%s', $lang, $country) ); } elseif ($charset) { array_push( $locale_list, sprintf('%s.%s', $lang, $charset) ); } // lang only array_push($locale_list, $lang); } // If the locale name doesn't match POSIX style, just include it as-is. if (!in_array($locale, $locale_list)) { array_push($locale_list, $locale); } return $locale_list; } /** * tries to detect the locale set in the following order: * - globals: LOCALE * - globals: LANG * - env: LC_ALL * - env: LC_MESSAGES * - env: LANG * if nothing set, returns 'en' as default * * @return string */ public static function detectLocale(): string { // globals foreach (['LOCALE', 'LANG'] as $global) { if (!empty($GLOBALS[$global])) { return $GLOBALS[$global]; } } // enviroment foreach (['LC_ALL', 'LC_MESSAGES', 'LANG'] as $env) { $locale = getenv($env); if ($locale !== false && !empty($locale)) { return $locale; } } return 'en'; } /************ * INTERNAL VAR SET/GET */ /** * Sets the path for a domain. * must be set before running getTranslator (former l10nReloadMOfile) * * @param string $domain Domain name * @param string $path Path where to find locales */ public function setTextDomain(string $domain, string $path): void { $this->paths[$domain] = $path; } /** * return set path for given domain * if not found return false * * @param string $domain * @return string|bool */ public function getTextDomain(string $domain) { return $this->paths[$domain] ?? false; } /** * sets the default domain. * * @param string $domain Domain name */ public function setDomain(string $domain): void { $this->domain = $domain; } /** * return current set domain name * * @return string */ public function getDomain(): string { return $this->domain; } /** * sets a requested locale. * * @param string $locale Locale name * @return string Set or current locale */ public function setLocale(string $locale): string { if (!empty($locale)) { $this->locale = $locale; } return $this->locale; } /** * get current set locale (want locale) * * @return string */ public function getLocale(): string { return $this->locale; } /** * current set locale where mo file is located * * @return string */ public function getLocaleSet(): string { return $this->locale_set; } /** * Set override encoding * * @param string $encoding * @return void */ public function setOverrideEncoding(string $encoding): void { $this->override_encoding = $encoding; } /** * return current set override encoding * * @return string */ public function getOverrideEncoding(): string { return $this->override_encoding; } /** * Current set encoding * * @return string */ public function getEncoding(): string { return $this->encoding; } /** * get current set language * * @return string current set language string * @deprecated Use getLocale() */ public function __getLang(): string { return $this->getLocale(); } /** * get current set mo file * * @return string current set mo language file */ public function getMoFile(): string { return $this->mofile; } /** * get current set mo file * * @return string current set mo language file * @deprecated Use getMoFile() */ public function __getMoFile(): string { return $this->getMoFile(); } /** * get the current base path in which we search * * @return string */ public function getBaseLocalePath(): string { return $this->base_locale_path; } /** * the path below the base path to where the mo file is located * * @return string */ public function getBaseContentPath(): string { return $this->base_content_path; } /** * get the current load error status * if true then the mo file failed to load * * @return bool */ public function getLoadError(): bool { return $this->load_failure; } /************ * TRANSLATION METHODS */ /** * translates a string and returns translated text * * @param string $text text to translate * @return string translated text */ public function __(string $text): string { // fallback passthrough if ($this->l10n === null) { return $text; } return $this->l10n->translate($text); } /** * prints translated string out to the screen * @param string $text text to translate * @return void has no return * @deprecated use echo __() instead */ public function __e(string $text): void { // fallback passthrough if ($this->l10n === null) { echo $text; return; } echo $this->l10n->translate($text); } /** * Return the plural form. * * @param string $single string for single word * @param string $plural string for plural word * @param int $number number value * @return string translated plural string */ public function __n(string $single, string $plural, int $number): string { // in case nothing got set yet, this is fallback if ($this->l10n === null) { return $number > 1 ? $plural : $single; } return $this->l10n->ngettext($single, $plural, $number); } /** * context translation via msgctxt * * @param string $context context string * @param string $text text to translate * @return string */ public function __p(string $context, string $text): string { if ($this->l10n === null) { return $text; } return $this->l10n->pgettext($context, $text); } /** * context translation via msgctxt * * @param string $context context string * @param string $single string for single word * @param string $plural string for plural word * @param int $number number value * @return string */ public function __np(string $context, string $single, string $plural, int $number): string { if ($this->l10n === null) { return $number > 1 ? $plural : $single; } return $this->l10n->npgettext($context, $single, $plural, $number); } // alias functions to mimic gettext calls /** * alias for gettext, * calls __ * * @param string $text * @return string * @deprecated Use __() */ public function gettext(string $text): string { return $this->__($text); } /** * alias for ngettext * calls __n * * @param string $single * @param string $plural * @param int $number * @return string * @deprecated Use __n() */ public function ngettext(string $single, string $plural, int $number): string { return $this->__n($single, $plural, $number); } // TODO: dgettext(string $domain, string $message): string // TODO: dngettext(string $domain, string $singular, string $plural, int $count): string // TODO: dpgettext(string $domain, string $message, int $category): string // TODO: dpngettext(string $domain, string $singular, string $plural, int $count, int $category): string } // __END__