icingaweb2/library/Icinga/Util/Translator.php

300 lines
10 KiB
PHP

<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Util;
use Exception;
use Icinga\Exception\IcingaException;
/**
* Helper class to ease internationalization when using gettext
*/
class Translator
{
/**
* The default gettext domain used as fallback
*/
const DEFAULT_DOMAIN = 'icinga';
/**
* The locale code that is used in the project
*/
const DEFAULT_LOCALE = 'en_US';
/**
* Known gettext domains and directories
*
* @var array
*/
private static $knownDomains = array();
/**
* Translate a string
*
* Falls back to the default domain in case the string cannot be translated using the given domain
*
* @param string $text The string to translate
* @param string $domain The primary domain to use
* @param string|null $context Optional parameter for context based translation
*
* @return string The translated string
*/
public static function translate($text, $domain, $context = null)
{
if ($context !== null) {
$res = self::pgettext($text, $domain, $context);
if ($res === $text && $domain !== self::DEFAULT_DOMAIN) {
$res = self::pgettext($text, self::DEFAULT_DOMAIN, $context);
}
return $res;
}
$res = dgettext($domain, $text);
if ($res === $text && $domain !== self::DEFAULT_DOMAIN) {
return dgettext(self::DEFAULT_DOMAIN, $text);
}
return $res;
}
/**
* Translate a plural string
*
* Falls back to the default domain in case the string cannot be translated using the given domain
*
* @param string $textSingular The string in singular form to translate
* @param string $textPlural The string in plural form to translate
* @param integer $number The number to get the plural or singular string
* @param string $domain The primary domain to use
* @param string|null $context Optional parameter for context based translation
*
* @return string The translated string
*/
public static function translatePlural($textSingular, $textPlural, $number, $domain, $context = null)
{
if ($context !== null) {
$res = self::pngettext($textSingular, $textPlural, $number, $domain, $context);
if (($res === $textSingular || $res === $textPlural) && $domain !== self::DEFAULT_DOMAIN) {
$res = self::pngettext($textSingular, $textPlural, $number, self::DEFAULT_DOMAIN, $context);
}
return $res;
}
$res = dngettext($domain, $textSingular, $textPlural, $number);
if (($res === $textSingular || $res === $textPlural) && $domain !== self::DEFAULT_DOMAIN) {
$res = dngettext(self::DEFAULT_DOMAIN, $textSingular, $textPlural, $number);
}
return $res;
}
/**
* Emulated pgettext()
*
* @link http://php.net/manual/de/book.gettext.php#89975
*
* @param $text
* @param $domain
* @param $context
*
* @return string
*/
public static function pgettext($text, $domain, $context)
{
$contextString = "{$context}\004{$text}";
$translation = dcgettext($domain, $contextString, LC_MESSAGES);
if ($translation == $contextString) {
return $text;
} else {
return $translation;
}
}
/**
* Emulated pngettext()
*
* @link http://php.net/manual/de/book.gettext.php#89975
*
* @param $textSingular
* @param $textPlural
* @param $number
* @param $domain
* @param $context
*
* @return string
*/
public static function pngettext($textSingular, $textPlural, $number, $domain, $context)
{
$contextString = "{$context}\004{$textSingular}";
$translation = dcngettext($domain, $contextString, $textPlural, $number, LC_MESSAGES);
if ($translation == $contextString || $translation == $textPlural) {
return ($number == 1 ? $textSingular : $textPlural);
} else {
return $translation;
}
}
/**
* Register a new gettext domain
*
* @param string $name The name of the domain to register
* @param string $directory The directory where message catalogs can be found
*
* @throws IcingaException In case the domain was not successfully registered
*/
public static function registerDomain($name, $directory)
{
if (bindtextdomain($name, $directory) === false) {
throw new IcingaException(
'Cannot register domain \'%s\' with path \'%s\'',
$name,
$directory
);
}
bind_textdomain_codeset($name, 'UTF-8');
self::$knownDomains[$name] = $directory;
}
/**
* Set the locale to use
*
* @param string $localeName The name of the locale to use
*
* @throws IcingaException In case the locale's name is invalid
*/
public static function setupLocale($localeName)
{
if (setlocale(LC_ALL, $localeName . '.UTF-8') === false && setlocale(LC_ALL, $localeName) === false) {
setlocale(LC_ALL, 'C'); // C == "use whatever is hardcoded"
if ($localeName !== self::DEFAULT_LOCALE) {
throw new IcingaException(
'Cannot set locale \'%s\' for category \'LC_ALL\'',
$localeName
);
}
} else {
$locale = setlocale(LC_ALL, 0);
putenv('LC_ALL=' . $locale); // Failsafe, Win and Unix
putenv('LANG=' . $locale); // Windows fix, untested
}
}
/**
* Split and return the language code and country code of the given locale or the current locale
*
* @param string $locale The locale code to split, or null to split the current locale
*
* @return stdClass An object with a 'language' and 'country' attribute
*/
public static function splitLocaleCode($locale = null)
{
$matches = array();
$locale = $locale !== null ? $locale : setlocale(LC_ALL, 0);
if (preg_match('@([a-z]{2})[_-]([A-Z]{2})@', $locale, $matches)) {
list($languageCode, $countryCode) = array_slice($matches, 1);
} elseif ($locale === 'C') {
list($languageCode, $countryCode) = preg_split('@[_-]@', static::DEFAULT_LOCALE, 2);
} else {
$languageCode = $locale;
$countryCode = null;
}
return (object) array('language' => $languageCode, 'country' => $countryCode);
}
/**
* Return a list of all locale codes currently available in the known domains
*
* @return array
*/
public static function getAvailableLocaleCodes()
{
$codes = array(static::DEFAULT_LOCALE);
foreach (array_values(self::$knownDomains) as $directory) {
$dh = opendir($directory);
while (false !== ($name = readdir($dh))) {
if (substr($name, 0, 1) !== '.'
&& false === in_array($name, $codes)
&& is_dir($directory . DIRECTORY_SEPARATOR . $name)
) {
$codes[] = $name;
}
}
}
sort($codes);
return $codes;
}
/**
* Return the preferred locale based on the given HTTP header and the available translations
*
* @param string $header The HTTP "Accept-Language" header
*
* @return string The browser's preferred locale code
*/
public static function getPreferredLocaleCode($header)
{
$headerValues = explode(',', $header);
for ($i = 0; $i < count($headerValues); $i++) {
// In order to accomplish a stable sort we need to take the original
// index into account as well during element comparison
$headerValues[$i] = array($headerValues[$i], $i);
}
usort( // Sort DESC but keep equal elements ASC
$headerValues,
function ($a, $b) {
$qValA = (float) (strpos($a[0], ';') > 0 ? substr(array_pop((explode(';', $a[0], 2))), 2) : 1);
$qValB = (float) (strpos($b[0], ';') > 0 ? substr(array_pop((explode(';', $b[0], 2))), 2) : 1);
return $qValA < $qValB ? 1 : ($qValA > $qValB ? -1 : ($a[1] > $b[1] ? 1 : ($a[1] < $b[1] ? -1 : 0)));
}
);
for ($i = 0; $i < count($headerValues); $i++) {
// We need to reset the array to its original structure once it's sorted
$headerValues[$i] = $headerValues[$i][0];
}
$requestedLocales = array();
foreach ($headerValues as $headerValue) {
if (strpos($headerValue, ';') > 0) {
$parts = explode(';', $headerValue, 2);
$headerValue = $parts[0];
}
$requestedLocales[] = str_replace('-', '_', $headerValue);
}
$similarMatch = null;
$availableLocales = static::getAvailableLocaleCodes();
$perfectMatch = array_shift((array_intersect($requestedLocales, $availableLocales)));
foreach ($requestedLocales as $requestedLocale) {
if ($perfectMatch === $requestedLocale) {
// The perfect match must be preferred when reached before a similar match is found
return $perfectMatch;
}
$similarMatches = array();
$localeObj = static::splitLocaleCode($requestedLocale);
foreach ($availableLocales as $availableLocale) {
if (static::splitLocaleCode($availableLocale)->language === $localeObj->language) {
$similarMatches[] = $availableLocale;
}
}
if (!empty($similarMatches)) {
$similarMatch = array_shift($similarMatches); // There is no "best" similar match, just use the first
break;
}
}
if (!$perfectMatch && $similarMatch) {
return $similarMatch;
} elseif ($similarMatch && static::splitLocaleCode($similarMatch)->language === static::splitLocaleCode($perfectMatch)->language) {
return $perfectMatch;
} elseif ($similarMatch) {
return $similarMatch;
}
return static::DEFAULT_LOCALE;
}
}