Merge pull request #4376 from Icinga/utilize-ipl-i18n-4366
Utilize ipl-i18n
This commit is contained in:
commit
229e24519a
|
@ -105,9 +105,7 @@ jobs:
|
|||
extensions: mysql, pgsql, ldap
|
||||
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
sudo locale-gen en_US.UTF-8 de_DE.UTF-8 fr_FR.UTF-8
|
||||
composer require -n --no-progress mockery/mockery
|
||||
run: composer require -n --no-progress mockery/mockery ipl/i18n:@dev
|
||||
|
||||
- name: PHPUnit
|
||||
env:
|
||||
|
|
|
@ -12,12 +12,13 @@ use Icinga\Authentication\Auth;
|
|||
use Icinga\User\Preferences;
|
||||
use Icinga\User\Preferences\PreferencesStore;
|
||||
use Icinga\Util\TimezoneDetect;
|
||||
use Icinga\Util\Translator;
|
||||
use Icinga\Web\Cookie;
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Web\Notification;
|
||||
use Icinga\Web\Session;
|
||||
use Icinga\Web\StyleSheet;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\Locale;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
|
||||
/**
|
||||
* Form class to adjust user preferences
|
||||
|
@ -201,14 +202,18 @@ class PreferenceForm extends Form
|
|||
}
|
||||
}
|
||||
|
||||
$languages = array();
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$locale = $this->getLocale();
|
||||
$languages = array();
|
||||
$availableLocales = $translator->listLocales();
|
||||
|
||||
$locale = $this->getLocale($availableLocales);
|
||||
if ($locale !== null) {
|
||||
$languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $locale);
|
||||
}
|
||||
|
||||
foreach (Translator::getAvailableLocaleCodes() as $language) {
|
||||
foreach ($availableLocales as $language) {
|
||||
$languages[$language] = $language;
|
||||
}
|
||||
|
||||
|
@ -410,10 +415,10 @@ class PreferenceForm extends Form
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getLocale()
|
||||
protected function getLocale($availableLocales)
|
||||
{
|
||||
return isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])
|
||||
? Translator::getPreferredLocaleCode($_SERVER['HTTP_ACCEPT_LANGUAGE'])
|
||||
? (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $availableLocales)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
<?php
|
||||
|
||||
use Icinga\Util\Translator;
|
||||
use ipl\I18n\Locale;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
|
||||
$lang = Translator::splitLocaleCode()->language;
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$lang = (new Locale())->parseLocale($translator->getLocale())->language;
|
||||
$showFullscreen = $this->layout()->showFullscreen;
|
||||
$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Icinga\Util\Translator;
|
||||
use ipl\I18n\Locale;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use ipl\Web\Widget\Icon;
|
||||
|
||||
if (array_key_exists('_dev', $_GET)) {
|
||||
|
@ -11,8 +13,11 @@ if (array_key_exists('_dev', $_GET)) {
|
|||
$cssfile = 'css/icinga.min.css';
|
||||
}
|
||||
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$lang = (new Locale())->parseLocale($translator->getLocale())->language;
|
||||
$timezone = date_default_timezone_get();
|
||||
$lang = Translator::splitLocaleCode()->language;
|
||||
$isIframe = $this->layout()->isIframe;
|
||||
$showFullscreen = $this->layout()->showFullscreen;
|
||||
$iframeClass = $isIframe ? ' iframe' : '';
|
||||
|
|
|
@ -6,13 +6,14 @@ namespace Icinga\Application;
|
|||
use DirectoryIterator;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use LogicException;
|
||||
use Icinga\Application\Modules\Manager as ModuleManager;
|
||||
use Icinga\Authentication\User\UserBackend;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
use Icinga\Exception\NotReadableError;
|
||||
use Icinga\Util\Translator;
|
||||
use Icinga\Exception\IcingaException;
|
||||
|
||||
/**
|
||||
|
@ -699,6 +700,19 @@ abstract class ApplicationBootstrap
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare internationalization using gettext
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function prepareInternationalization()
|
||||
{
|
||||
StaticTranslator::$instance = (new GettextTranslator())
|
||||
->setDefaultDomain('icinga');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up internationalization using gettext
|
||||
*
|
||||
|
@ -706,17 +720,20 @@ abstract class ApplicationBootstrap
|
|||
*/
|
||||
final protected function setupInternationalization()
|
||||
{
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
if ($this->hasLocales()) {
|
||||
Translator::registerDomain(Translator::DEFAULT_DOMAIN, $this->getLocaleDir());
|
||||
$translator->addTranslationDirectory($this->getLocaleDir(), 'icinga');
|
||||
}
|
||||
|
||||
$locale = $this->detectLocale();
|
||||
if ($locale === null) {
|
||||
$locale = Translator::DEFAULT_LOCALE;
|
||||
$locale = $translator->getDefaultLocale();
|
||||
}
|
||||
|
||||
try {
|
||||
Translator::setupLocale($locale);
|
||||
$translator->setLocale($locale);
|
||||
} catch (Exception $error) {
|
||||
Logger::error($error);
|
||||
}
|
||||
|
@ -753,27 +770,15 @@ abstract class ApplicationBootstrap
|
|||
/**
|
||||
* List all available locales
|
||||
*
|
||||
* NOTE: Might be a candidate for a static function in Translator
|
||||
* @return array Locale list
|
||||
*
|
||||
* return array Locale list
|
||||
* @deprecated Use {@see \ipl\I18n\GettextTranslator::listLocales()} instead
|
||||
*/
|
||||
public function listLocales()
|
||||
{
|
||||
$locales = array();
|
||||
if (! $this->hasLocales()) {
|
||||
return $locales;
|
||||
}
|
||||
$localedir = $this->getLocaleDir();
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$dh = opendir($localedir);
|
||||
while (false !== ($file = readdir($dh))) {
|
||||
$filename = $localedir . DIRECTORY_SEPARATOR . $file;
|
||||
if (preg_match('/^[a-z]{2}_[A-Z]{2}$/', $file) && is_dir($filename)) {
|
||||
$locales[] = $file;
|
||||
}
|
||||
}
|
||||
closedir($dh);
|
||||
sort($locales);
|
||||
return $locales;
|
||||
return $translator->listLocales();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ class Cli extends ApplicationBootstrap
|
|||
->loadLibraries()
|
||||
->loadConfig()
|
||||
->setupTimezone()
|
||||
->prepareInternationalization()
|
||||
->setupInternationalization()
|
||||
->parseBasicParams()
|
||||
->setupLogger()
|
||||
|
|
|
@ -7,6 +7,8 @@ require_once dirname(__FILE__) . '/ApplicationBootstrap.php';
|
|||
|
||||
use Icinga\Web\Request;
|
||||
use Icinga\Web\Response;
|
||||
use ipl\I18n\NoopTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
|
||||
/**
|
||||
* Use this if you want to make use of Icinga functionality in other web projects
|
||||
|
@ -72,6 +74,7 @@ class EmbeddedWeb extends ApplicationBootstrap
|
|||
->setupRequest()
|
||||
->setupResponse()
|
||||
->setupTimezone()
|
||||
->prepareFakeInternationalization()
|
||||
->setupModuleManager()
|
||||
->loadEnabledModules();
|
||||
}
|
||||
|
@ -97,4 +100,16 @@ class EmbeddedWeb extends ApplicationBootstrap
|
|||
$this->response = new Response();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare fake internationalization
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function prepareFakeInternationalization()
|
||||
{
|
||||
StaticTranslator::$instance = new NoopTranslator();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,11 @@ use Icinga\Exception\IcingaException;
|
|||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Module\Setup\SetupWizard;
|
||||
use Icinga\Util\File;
|
||||
use Icinga\Util\Translator;
|
||||
use Icinga\Web\Navigation\Navigation;
|
||||
use Icinga\Web\Widget;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use ipl\I18n\Translation;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use Zend_Controller_Router_Route;
|
||||
|
@ -29,6 +31,11 @@ use Zend_Controller_Router_Route_Regex;
|
|||
*/
|
||||
class Module
|
||||
{
|
||||
use Translation {
|
||||
translate as protected;
|
||||
translatePlural as protected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module name
|
||||
*
|
||||
|
@ -300,6 +307,8 @@ class Module
|
|||
$this->runScript = $basedir . '/run.php';
|
||||
$this->configScript = $basedir . '/configuration.php';
|
||||
$this->metadataFile = $basedir . '/module.info';
|
||||
|
||||
$this->translationDomain = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1418,9 +1427,10 @@ class Module
|
|||
*/
|
||||
protected function registerLocales()
|
||||
{
|
||||
if ($this->hasLocales()) {
|
||||
Translator::registerDomain($this->name, $this->localedir);
|
||||
if ($this->hasLocales() && StaticTranslator::$instance instanceof GettextTranslator) {
|
||||
StaticTranslator::$instance->addTranslationDirectory($this->localedir, $this->name);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -1633,22 +1643,4 @@ class Module
|
|||
$this->routes[$name] = $route;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translate() For the function documentation.
|
||||
*/
|
||||
protected function translate($string, $context = null)
|
||||
{
|
||||
return mt($this->name, $string, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translatePlural() For the function documentation.
|
||||
*/
|
||||
protected function translatePlural($textSingular, $textPlural, $number, $context = null)
|
||||
{
|
||||
return mtp($this->name, $textSingular, $textPlural, $number, $context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ namespace Icinga\Application;
|
|||
require_once __DIR__ . '/EmbeddedWeb.php';
|
||||
|
||||
use ErrorException;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\Locale;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use Zend_Controller_Action_HelperBroker;
|
||||
use Zend_Controller_Front;
|
||||
use Zend_Controller_Router_Route;
|
||||
|
@ -16,7 +19,6 @@ use Icinga\Authentication\Auth;
|
|||
use Icinga\User;
|
||||
use Icinga\Util\DirectoryIterator;
|
||||
use Icinga\Util\TimezoneDetect;
|
||||
use Icinga\Util\Translator;
|
||||
use Icinga\Web\Controller\Dispatcher;
|
||||
use Icinga\Web\Menu;
|
||||
use Icinga\Web\Navigation\Navigation;
|
||||
|
@ -91,6 +93,7 @@ class Web extends EmbeddedWeb
|
|||
->setupNotifications()
|
||||
->setupResponse()
|
||||
->setupZendMvc()
|
||||
->prepareInternationalization()
|
||||
->setupModuleManager()
|
||||
->loadSetupModuleIfNecessary()
|
||||
->loadEnabledModules()
|
||||
|
@ -493,9 +496,7 @@ class Web extends EmbeddedWeb
|
|||
*
|
||||
* Uses the preferred user language or the browser suggested language or our default.
|
||||
*
|
||||
* @return string Detected locale code
|
||||
*
|
||||
* @see Translator::DEFAULT_LOCALE For the default locale code.
|
||||
* @return string Detected locale code
|
||||
*/
|
||||
protected function detectLocale()
|
||||
{
|
||||
|
@ -505,9 +506,14 @@ class Web extends EmbeddedWeb
|
|||
) {
|
||||
return $locale;
|
||||
}
|
||||
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
return Translator::getPreferredLocaleCode($_SERVER['HTTP_ACCEPT_LANGUAGE']);
|
||||
return (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $translator->listLocales());
|
||||
}
|
||||
return Translator::DEFAULT_LOCALE;
|
||||
|
||||
return $translator->getDefaultLocale();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
|
||||
|
||||
use Icinga\Util\Translator;
|
||||
use ipl\Stdlib\Contract\Translator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
|
||||
/**
|
||||
* No-op translate
|
||||
|
@ -27,45 +28,46 @@ if (function_exists('t')) {
|
|||
if (extension_loaded('gettext')) {
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translate() For the function documentation.
|
||||
*/
|
||||
function t($messageId, $context = null)
|
||||
{
|
||||
return Translator::translate($messageId, Translator::DEFAULT_DOMAIN, $context);
|
||||
return StaticTranslator::$instance->translate($messageId, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translate() For the function documentation.
|
||||
* @see Translator::translateInDomain() For the function documentation.
|
||||
*/
|
||||
function mt($domain, $messageId, $context = null)
|
||||
{
|
||||
return Translator::translate($messageId, $domain, $context);
|
||||
return StaticTranslator::$instance->translateInDomain($domain, $messageId, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translatePlural() For the function documentation.
|
||||
*/
|
||||
function tp($messageId, $messageId2, $number, $context = null)
|
||||
{
|
||||
return Translator::translatePlural($messageId, $messageId2, $number, Translator::DEFAULT_DOMAIN, $context);
|
||||
return StaticTranslator::$instance->translatePlural($messageId, $messageId2, $number, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translatePlural() For the function documentation.
|
||||
* @see Translator::translatePluralInDomain() For the function documentation.
|
||||
*/
|
||||
function mtp($domain, $messageId, $messageId2, $number, $context = null)
|
||||
{
|
||||
return Translator::translatePlural($messageId, $messageId2, $number, $domain, $context);
|
||||
return StaticTranslator::$instance->translatePluralInDomain(
|
||||
$domain,
|
||||
$messageId,
|
||||
$messageId2,
|
||||
$number,
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translate() For the function documentation.
|
||||
*/
|
||||
function t($messageId, $context = null)
|
||||
|
@ -74,7 +76,6 @@ if (extension_loaded('gettext')) {
|
|||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translate() For the function documentation.
|
||||
*/
|
||||
function mt($domain, $messageId, $context = null)
|
||||
|
@ -83,7 +84,6 @@ if (extension_loaded('gettext')) {
|
|||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translatePlural() For the function documentation.
|
||||
*/
|
||||
function tp($messageId, $messageId2, $number, $context = null)
|
||||
|
@ -91,11 +91,11 @@ if (extension_loaded('gettext')) {
|
|||
if ((int) $number !== 1) {
|
||||
return $messageId2;
|
||||
}
|
||||
|
||||
return $messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPDoc)
|
||||
* @see Translator::translatePlural() For the function documentation.
|
||||
*/
|
||||
function mtp($domain, $messageId, $messageId2, $number, $context = null)
|
||||
|
@ -103,6 +103,7 @@ if (extension_loaded('gettext')) {
|
|||
if ((int) $number !== 1) {
|
||||
return $messageId2;
|
||||
}
|
||||
|
||||
return $messageId;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@ use Icinga\Application\Config;
|
|||
use Icinga\Application\Logger;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Exception\NotReadableError;
|
||||
use Icinga\Util\Translator;
|
||||
use ipl\I18n\Translation;
|
||||
|
||||
abstract class Command
|
||||
{
|
||||
use Translation;
|
||||
|
||||
protected $app;
|
||||
protected $docs;
|
||||
|
||||
|
@ -61,6 +63,8 @@ abstract class Command
|
|||
$this->isDebugging = $this->params->shift('debug', false);
|
||||
$this->configs = [];
|
||||
|
||||
$this->translationDomain = $moduleName ?: 'icinga';
|
||||
|
||||
if ($this->loadEnabledModules) {
|
||||
try {
|
||||
$app->getModuleManager()->loadEnabledModules();
|
||||
|
@ -134,21 +138,6 @@ abstract class Command
|
|||
return $this->trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a string
|
||||
*
|
||||
* Autoselects the module domain, if any, and falls back to the global one if no translation could be found.
|
||||
*
|
||||
* @param string $text The string to translate
|
||||
*
|
||||
* @return string The translated string
|
||||
*/
|
||||
public function translate($text)
|
||||
{
|
||||
$domain = $this->moduleName === null ? 'icinga' : $this->moduleName;
|
||||
return Translator::translate($text, $domain);
|
||||
}
|
||||
|
||||
public function fail($msg)
|
||||
{
|
||||
throw new IcingaException('%s', $msg);
|
||||
|
|
|
@ -21,6 +21,8 @@ namespace {
|
|||
namespace Icinga\Test {
|
||||
|
||||
use Exception;
|
||||
use ipl\I18n\NoopTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use RuntimeException;
|
||||
use Mockery;
|
||||
use Icinga\Application\Icinga;
|
||||
|
@ -140,6 +142,8 @@ namespace Icinga\Test {
|
|||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
StaticTranslator::$instance = new NoopTranslator();
|
||||
$this->setupIcingaMock();
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
|
||||
namespace Icinga\Util;
|
||||
|
||||
use Icinga\Exception\IcingaException;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\Locale;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
|
||||
/**
|
||||
* Helper class to ease internationalization when using gettext
|
||||
*
|
||||
* @deprecated Use {@see \ipl\I18n\StaticTranslator::$instance} or {@see \ipl\I18n\Translation} instead
|
||||
*/
|
||||
class Translator
|
||||
{
|
||||
|
@ -20,13 +24,6 @@ class Translator
|
|||
*/
|
||||
const DEFAULT_LOCALE = 'en_US';
|
||||
|
||||
/**
|
||||
* Known gettext domains and directories
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $knownDomains = array();
|
||||
|
||||
/**
|
||||
* Translate a string
|
||||
*
|
||||
|
@ -40,19 +37,7 @@ class Translator
|
|||
*/
|
||||
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;
|
||||
return StaticTranslator::$instance->translateInDomain($domain, $text, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,19 +55,13 @@ class Translator
|
|||
*/
|
||||
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;
|
||||
return StaticTranslator::$instance->translatePluralInDomain(
|
||||
$domain,
|
||||
$textSingular,
|
||||
$textPlural,
|
||||
$number,
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,19 +77,7 @@ class Translator
|
|||
*/
|
||||
public static function pgettext($text, $domain, $context)
|
||||
{
|
||||
$contextString = "{$context}\004{$text}";
|
||||
|
||||
$translation = dcgettext(
|
||||
$domain,
|
||||
$contextString,
|
||||
defined('LC_MESSAGES') ? LC_MESSAGES : LC_ALL
|
||||
);
|
||||
|
||||
if ($translation == $contextString) {
|
||||
return $text;
|
||||
} else {
|
||||
return $translation;
|
||||
}
|
||||
return StaticTranslator::$instance->translateInDomain($domain, $text, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,21 +95,13 @@ class Translator
|
|||
*/
|
||||
public static function pngettext($textSingular, $textPlural, $number, $domain, $context)
|
||||
{
|
||||
$contextString = "{$context}\004{$textSingular}";
|
||||
|
||||
$translation = dcngettext(
|
||||
return StaticTranslator::$instance->translatePluralInDomain(
|
||||
$domain,
|
||||
$contextString,
|
||||
$textSingular,
|
||||
$textPlural,
|
||||
$number,
|
||||
defined('LC_MESSAGES') ? LC_MESSAGES : LC_ALL
|
||||
$context
|
||||
);
|
||||
|
||||
if ($translation == $contextString || $translation == $textPlural) {
|
||||
return ($number == 1 ? $textSingular : $textPlural);
|
||||
} else {
|
||||
return $translation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,19 +110,14 @@ class Translator
|
|||
* @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
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$translator->addTranslationDirectory($directory, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,26 +125,14 @@ class Translator
|
|||
*
|
||||
* @param string $localeName The name of the locale to use
|
||||
*
|
||||
* @throws IcingaException In case the locale's name is invalid
|
||||
* @return void
|
||||
*/
|
||||
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
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
// https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html
|
||||
putenv('LANGUAGE=' . $localeName . ':' . getenv('LANGUAGE'));
|
||||
}
|
||||
$translator->setLocale($localeName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,18 +144,14 @@ class Translator
|
|||
*/
|
||||
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})@i', $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;
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
if ($locale === null) {
|
||||
$locale = $translator->getLocale();
|
||||
}
|
||||
|
||||
return (object) array('language' => $languageCode, 'country' => $countryCode);
|
||||
return (new Locale())->parseLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -223,21 +161,10 @@ class Translator
|
|||
*/
|
||||
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);
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
return $codes;
|
||||
return $translator->listLocales();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -249,68 +176,9 @@ class Translator
|
|||
*/
|
||||
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) {
|
||||
$tagA = explode(';', $a[0], 2);
|
||||
$tagB = explode(';', $b[0], 2);
|
||||
$qValA = (float) (strpos($a[0], ';') > 0 ? substr(array_pop($tagA), 2) : 1);
|
||||
$qValB = (float) (strpos($b[0], ';') > 0 ? substr(array_pop($tagB), 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);
|
||||
}
|
||||
$requestedLocales = array_combine(
|
||||
array_map('strtolower', array_values($requestedLocales)),
|
||||
array_values($requestedLocales)
|
||||
);
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$availableLocales = static::getAvailableLocaleCodes();
|
||||
$availableLocales = array_combine(
|
||||
array_map('strtolower', array_values($availableLocales)),
|
||||
array_values($availableLocales)
|
||||
);
|
||||
|
||||
$similarMatch = null;
|
||||
|
||||
foreach ($requestedLocales as $requestedLocaleLowered => $requestedLocale) {
|
||||
$localeObj = static::splitLocaleCode($requestedLocaleLowered);
|
||||
|
||||
if (isset($availableLocales[$requestedLocaleLowered])
|
||||
&& (! $similarMatch || static::splitLocaleCode($similarMatch)->language === $localeObj->language)
|
||||
) {
|
||||
// Prefer perfect match only if no similar match has been found yet or the perfect match is more precise
|
||||
// than the similar match
|
||||
return $availableLocales[$requestedLocaleLowered];
|
||||
}
|
||||
|
||||
if (! $similarMatch) {
|
||||
foreach ($availableLocales as $availableLocaleLowered => $availableLocale) {
|
||||
if (static::splitLocaleCode($availableLocaleLowered)->language === $localeObj->language) {
|
||||
$similarMatch = $availableLocaleLowered;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $similarMatch ? $availableLocales[$similarMatch] : static::DEFAULT_LOCALE;
|
||||
return (new Locale())->getPreferred($header, $translator->listLocales());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
namespace Icinga\Web\Controller;
|
||||
|
||||
use ipl\I18n\Translation;
|
||||
use Zend_Controller_Action;
|
||||
use Zend_Controller_Action_HelperBroker;
|
||||
use Zend_Controller_Request_Abstract;
|
||||
|
@ -16,7 +17,6 @@ use Icinga\Exception\ProgrammingError;
|
|||
use Icinga\File\Pdf;
|
||||
use Icinga\Forms\AutoRefreshForm;
|
||||
use Icinga\Security\SecurityException;
|
||||
use Icinga\Util\Translator;
|
||||
use Icinga\Web\Session;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Web\UrlParams;
|
||||
|
@ -40,6 +40,8 @@ use Icinga\Web\Window;
|
|||
*/
|
||||
class ActionController extends Zend_Controller_Action
|
||||
{
|
||||
use Translation;
|
||||
|
||||
/**
|
||||
* The login route to use when requiring authentication
|
||||
*/
|
||||
|
@ -131,7 +133,8 @@ class ActionController extends Zend_Controller_Action
|
|||
|
||||
$moduleName = $this->getModuleName();
|
||||
$this->view->defaultTitle = static::DEFAULT_TITLE;
|
||||
$this->view->translationDomain = $moduleName !== 'default' ? $moduleName : 'icinga';
|
||||
$this->translationDomain = $moduleName !== 'default' ? $moduleName : 'icinga';
|
||||
$this->view->translationDomain = $this->translationDomain;
|
||||
$this->_helper->layout()->isIframe = $request->getUrl()->shift('isIframe');
|
||||
$this->_helper->layout()->showFullscreen = $request->getUrl()->shift('showFullscreen');
|
||||
$this->_helper->layout()->moduleName = $moduleName;
|
||||
|
@ -305,42 +308,6 @@ class ActionController extends Zend_Controller_Action
|
|||
return $this->view->tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a string
|
||||
*
|
||||
* Autoselects the module domain, if any, and falls back to the global one if no translation could be found.
|
||||
*
|
||||
* @param string $text The string to translate
|
||||
* @param string|null $context Optional parameter for context based translation
|
||||
*
|
||||
* @return string The translated string
|
||||
*/
|
||||
public function translate($text, $context = null)
|
||||
{
|
||||
return Translator::translate($text, $this->view->translationDomain, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a plural string
|
||||
*
|
||||
* @param string $textSingular The string in singular form to translate
|
||||
* @param string $textPlural The string in plural form to translate
|
||||
* @param string $number The number to get the plural or singular string
|
||||
* @param string|null $context Optional parameter for context based translation
|
||||
*
|
||||
* @return string The translated string
|
||||
*/
|
||||
public function translatePlural($textSingular, $textPlural, $number, $context = null)
|
||||
{
|
||||
return Translator::translatePlural(
|
||||
$textSingular,
|
||||
$textPlural,
|
||||
$number,
|
||||
$this->view->translationDomain,
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
protected function ignoreXhrBody()
|
||||
{
|
||||
if ($this->isXhr()) {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
namespace Icinga\Web;
|
||||
|
||||
use Icinga\Web\Form\Element\DateTimePicker;
|
||||
use ipl\I18n\Translation;
|
||||
use Zend_Config;
|
||||
use Zend_Form;
|
||||
use Zend_Form_Element;
|
||||
|
@ -12,7 +13,6 @@ use Icinga\Application\Icinga;
|
|||
use Icinga\Authentication\Auth;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Security\SecurityException;
|
||||
use Icinga\Util\Translator;
|
||||
use Icinga\Web\Form\ErrorLabeller;
|
||||
use Icinga\Web\Form\Decorator\Autosubmit;
|
||||
use Icinga\Web\Form\Element\CsrfCounterMeasure;
|
||||
|
@ -27,6 +27,11 @@ use Icinga\Web\Form\Element\CsrfCounterMeasure;
|
|||
*/
|
||||
class Form extends Zend_Form
|
||||
{
|
||||
use Translation {
|
||||
translate as i18nTranslate;
|
||||
translatePlural as i18nTranslatePlural;
|
||||
}
|
||||
|
||||
/**
|
||||
* The suffix to append to a field's hidden default field name
|
||||
*/
|
||||
|
@ -1519,7 +1524,9 @@ class Form extends Zend_Form
|
|||
*/
|
||||
protected function translate($text, $context = null)
|
||||
{
|
||||
return Translator::translate($text, $this->getTranslationDomain(), $context);
|
||||
$this->translationDomain = $this->getTranslationDomain();
|
||||
|
||||
return $this->i18nTranslate($text, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1534,13 +1541,9 @@ class Form extends Zend_Form
|
|||
*/
|
||||
protected function translatePlural($textSingular, $textPlural, $number, $context = null)
|
||||
{
|
||||
return Translator::translatePlural(
|
||||
$textSingular,
|
||||
$textPlural,
|
||||
$number,
|
||||
$this->getTranslationDomain(),
|
||||
$context
|
||||
);
|
||||
$this->translationDomain = $this->getTranslationDomain();
|
||||
|
||||
return $this->i18nTranslatePlural($textSingular, $textPlural, $number, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,10 +5,10 @@ namespace Icinga\Web;
|
|||
|
||||
use Closure;
|
||||
use Icinga\Application\Icinga;
|
||||
use ipl\I18n\Translation;
|
||||
use Zend_View_Abstract;
|
||||
use Icinga\Authentication\Auth;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Util\Translator;
|
||||
|
||||
/**
|
||||
* Icinga view
|
||||
|
@ -54,6 +54,8 @@ use Icinga\Util\Translator;
|
|||
*/
|
||||
class View extends Zend_View_Abstract
|
||||
{
|
||||
use Translation;
|
||||
|
||||
/**
|
||||
* Charset to be used - we only support UTF-8
|
||||
*/
|
||||
|
@ -178,21 +180,6 @@ class View extends Zend_View_Abstract
|
|||
);
|
||||
}
|
||||
|
||||
public function translate($text, $context = null)
|
||||
{
|
||||
return Translator::translate($text, $this->translationDomain, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a plural string
|
||||
*
|
||||
* @see Translator::translatePlural()
|
||||
*/
|
||||
public function translatePlural($textSingular, $textPlural, $number, $context = null)
|
||||
{
|
||||
return Translator::translatePlural($textSingular, $textPlural, $number, $this->translationDomain, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load helpers
|
||||
*/
|
||||
|
|
|
@ -9,8 +9,8 @@ use Icinga\Application\Icinga;
|
|||
use Icinga\Application\Modules\DashboardContainer;
|
||||
use Icinga\Cli\Command;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Util\Translator;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use ReflectionClass;
|
||||
|
||||
class DashboardCommand extends Command
|
||||
|
@ -34,25 +34,23 @@ class DashboardCommand extends Command
|
|||
// Calling getDashboard() results in Url::fromPath() getting called as well == the CLI's death
|
||||
$paneItemsProperty = $moduleReflection->getProperty('paneItems');
|
||||
$paneItemsProperty->setAccessible(true);
|
||||
// Again, no direct way to access this nor to let the module setup its own translation domain
|
||||
$localeDirProperty = $moduleReflection->getProperty('localedir');
|
||||
$localeDirProperty->setAccessible(true);
|
||||
|
||||
$locales = Translator::getAvailableLocaleCodes();
|
||||
$modules = Icinga::app()->getModuleManager()->loadEnabledModules()->getLoadedModules();
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
|
||||
$locales = $translator->listLocales();
|
||||
$modules = Icinga::app()->getModuleManager()->getLoadedModules();
|
||||
foreach ($this->listDashboardConfigs() as $path) {
|
||||
Logger::info('Migrating dashboard config: %s', $path);
|
||||
|
||||
$config = Config::fromIni($path);
|
||||
foreach ($modules as $module) {
|
||||
$localePath = $localeDirProperty->getValue($module);
|
||||
if (! is_dir($localePath)) {
|
||||
if (! $module->hasLocales()) {
|
||||
// Modules without any translations are not affected
|
||||
continue;
|
||||
}
|
||||
|
||||
$launchConfigScriptMethod->invoke($module);
|
||||
Translator::registerDomain($module->getName(), $localePath);
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
if ($locale === 'en_US') {
|
||||
|
@ -60,8 +58,8 @@ class DashboardCommand extends Command
|
|||
}
|
||||
|
||||
try {
|
||||
Translator::setupLocale($locale);
|
||||
} catch (IcingaException $e) {
|
||||
$translator->setLocale($locale);
|
||||
} catch (Exception $e) {
|
||||
Logger::debug('Ignoring locale "%s". Reason: %s', $locale, $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -4,48 +4,21 @@
|
|||
namespace Icinga\Module\Translation\Clicommands;
|
||||
|
||||
use Icinga\Module\Translation\Cli\TranslationCommand;
|
||||
use Icinga\Module\Translation\Util\GettextTranslationHelper;
|
||||
|
||||
/**
|
||||
* Translation compiler
|
||||
*
|
||||
* This command will compile the PO-file of a domain. The actions below allow
|
||||
* you to select a particular domain for which the PO-file should be compiled.
|
||||
* This command will compile gettext catalogs of modules.
|
||||
*
|
||||
* Domains are the global one 'icinga' and all available and enabled modules
|
||||
* identified by their name.
|
||||
*
|
||||
* Once a PO-file is compiled its content is used by Icinga Web 2 to display
|
||||
* Once a catalog is compiled its content is used by Icinga Web 2 to display
|
||||
* messages in the configured language.
|
||||
*/
|
||||
class CompileCommand extends TranslationCommand
|
||||
{
|
||||
/**
|
||||
* Compile the global domain
|
||||
* Compile a module gettext catalog
|
||||
*
|
||||
* This will compile the PO-file of the global 'icinga' domain.
|
||||
*
|
||||
* USAGE:
|
||||
*
|
||||
* icingacli translation compile icinga <locale>
|
||||
*
|
||||
* EXAMPLES:
|
||||
*
|
||||
* icingacli translation compile icinga de_DE
|
||||
* icingacli translation compile icinga fr_FR
|
||||
*/
|
||||
public function icingaAction()
|
||||
{
|
||||
$locale = $this->validateLocaleCode($this->params->shift());
|
||||
|
||||
$helper = $this->getTranslationHelper($locale);
|
||||
$helper->compileIcingaTranslation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a module domain
|
||||
*
|
||||
* This will compile the PO-file of the given module domain.
|
||||
* This will compile the catalog of the given module and locale.
|
||||
*
|
||||
* USAGE:
|
||||
*
|
||||
|
@ -53,8 +26,8 @@ class CompileCommand extends TranslationCommand
|
|||
*
|
||||
* EXAMPLES:
|
||||
*
|
||||
* icingacli translation compile monitoring de_DE
|
||||
* icingacli trnslations compile monitoring de_DE
|
||||
* icingacli translation compile demo de_DE
|
||||
* icingacli translation compile demo fr_FR
|
||||
*/
|
||||
public function moduleAction()
|
||||
{
|
||||
|
|
|
@ -4,48 +4,21 @@
|
|||
namespace Icinga\Module\Translation\Clicommands;
|
||||
|
||||
use Icinga\Module\Translation\Cli\TranslationCommand;
|
||||
use Icinga\Module\Translation\Util\GettextTranslationHelper;
|
||||
|
||||
/**
|
||||
* Translation updater
|
||||
*
|
||||
* This command will create a new or update any existing PO-file of a domain. The
|
||||
* actions below allow to select a particular domain for whom to touch the PO-file.
|
||||
* This command will create a new or update any existing gettext catalog of a module.
|
||||
*
|
||||
* Domains are the global one 'icinga' and all available and enabled modules
|
||||
* identified by their name.
|
||||
*
|
||||
* Once a PO-file has been created/updated one can open it with a editor for
|
||||
* Once a catalog has been created/updated one can open it with a editor for
|
||||
* PO-files and start with the actual translation.
|
||||
*/
|
||||
class RefreshCommand extends TranslationCommand
|
||||
{
|
||||
/**
|
||||
* Touch the global domain
|
||||
* Generate or update a module gettext catalog
|
||||
*
|
||||
* This will create/update the PO-file of the global 'icinga' domain.
|
||||
*
|
||||
* USAGE:
|
||||
*
|
||||
* icingacli translation refresh icinga <locale>
|
||||
*
|
||||
* EXAMPLES:
|
||||
*
|
||||
* icingacli translation refresh icinga de_DE
|
||||
* icingacli translation refresh icinga fr_FR
|
||||
*/
|
||||
public function icingaAction()
|
||||
{
|
||||
$locale = $this->validateLocaleCode($this->params->shift());
|
||||
|
||||
$helper = $this->getTranslationHelper($locale);
|
||||
$helper->updateIcingaTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch a module domain
|
||||
*
|
||||
* This will create/update the PO-file of the given module domain.
|
||||
* This will create/update the PO-file of the given module and locale.
|
||||
*
|
||||
* USAGE:
|
||||
*
|
||||
|
@ -53,8 +26,8 @@ class RefreshCommand extends TranslationCommand
|
|||
*
|
||||
* EXAMPLES:
|
||||
*
|
||||
* icingacli translation refresh module monitoring de_DE
|
||||
* icingacli translation refresh module monitoring fr_FR
|
||||
* icingacli translation refresh module demo de_DE
|
||||
* icingacli translation refresh module demo fr_FR
|
||||
*/
|
||||
public function moduleAction()
|
||||
{
|
||||
|
|
|
@ -6,7 +6,8 @@ namespace Icinga\Module\Translation\Clicommands;
|
|||
use Icinga\Date\DateFormatter;
|
||||
use Icinga\Module\Translation\Cli\ArrayToTextTableHelper;
|
||||
use Icinga\Module\Translation\Cli\TranslationCommand;
|
||||
use Icinga\Util\Translator;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
|
||||
/**
|
||||
* Timestamp test helper
|
||||
|
@ -89,12 +90,19 @@ class TestCommand extends TranslationCommand
|
|||
foreach ($this->params->getAllStandalone() as $l) {
|
||||
$this->locales[] = $l;
|
||||
}
|
||||
// TODO: get from to environment by default?
|
||||
|
||||
if (empty($this->locales)) {
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
$this->locales = $translator->listLocales();
|
||||
}
|
||||
}
|
||||
|
||||
protected function callTranslated($callback, $arguments, $locale = 'C')
|
||||
protected function callTranslated($callback, $arguments, $locale = 'en_US')
|
||||
{
|
||||
Translator::setupLocale($locale);
|
||||
/** @var GettextTranslator $translator */
|
||||
$translator = StaticTranslator::$instance;
|
||||
$translator->setLocale($locale);
|
||||
return call_user_func_array($callback, $arguments);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
# Introduction <a id="module-translation-introduction"></a>
|
||||
|
||||
Icinga Web 2 provides localization out of the box - for the core application and the modules, that means
|
||||
that you can with a lightness use existent localizations, update or even create you own localizations.
|
||||
Icinga Web 2 provides localization out of the box - for itself and the core modules.
|
||||
This module is for third party module developers to aid them to localize their work.
|
||||
|
||||
The chapters [Translation for Developers](03-Translation.md#module-translation-developers),
|
||||
[Translation for Translators](03-Translation.md#module-translation-translators) and
|
||||
[Testing Translations](03-Translation.md#module-translation-tests) will introduce and explain you, how to take
|
||||
part on localizing Icinga Web 2 for different languages and how to use the
|
||||
`translation module` to make your life much easier.
|
||||
[Testing Translations](03-Translation.md#module-translation-tests) will introduce and
|
||||
explain you, how to take part on localizing modules to different languages.
|
||||
|
||||
## Translation for Developers <a id="module-translation-developers"></a>
|
||||
|
||||
|
|
|
@ -65,13 +65,6 @@ class GettextTranslationHelper
|
|||
*/
|
||||
private $locale;
|
||||
|
||||
/**
|
||||
* The path to the Zend application root
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $appDir;
|
||||
|
||||
/**
|
||||
* The path to the module, if any
|
||||
*
|
||||
|
@ -79,13 +72,6 @@ class GettextTranslationHelper
|
|||
*/
|
||||
private $moduleDir;
|
||||
|
||||
/**
|
||||
* Path to the Icinga library
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $libDir;
|
||||
|
||||
/**
|
||||
* The path to the file catalog
|
||||
*
|
||||
|
@ -115,9 +101,7 @@ class GettextTranslationHelper
|
|||
*/
|
||||
public function __construct(ApplicationBootstrap $bootstrap, $locale)
|
||||
{
|
||||
$this->moduleMgr = $bootstrap->getModuleManager()->loadEnabledModules();
|
||||
$this->appDir = $bootstrap->getApplicationDir();
|
||||
$this->libDir = $bootstrap->getLibraryDir('Icinga');
|
||||
$this->moduleMgr = $bootstrap->getModuleManager();
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
|
@ -158,32 +142,6 @@ class GettextTranslationHelper
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the translation table for the main application
|
||||
*/
|
||||
public function updateIcingaTranslations()
|
||||
{
|
||||
$this->catalogPath = tempnam(sys_get_temp_dir(), 'IcingaTranslation_');
|
||||
$this->templatePath = tempnam(sys_get_temp_dir(), 'IcingaPot_');
|
||||
$this->version = 'None'; // TODO: Access icinga version from a file or property
|
||||
|
||||
$this->moduleDir = null;
|
||||
$this->tablePath = implode(
|
||||
DIRECTORY_SEPARATOR,
|
||||
array(
|
||||
$this->appDir,
|
||||
'locale',
|
||||
$this->locale,
|
||||
'LC_MESSAGES',
|
||||
'icinga.po'
|
||||
)
|
||||
);
|
||||
|
||||
$this->createFileCatalog();
|
||||
$this->createTemplateFile();
|
||||
$this->updateTranslationTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the translation table for a particular module
|
||||
*
|
||||
|
@ -214,25 +172,6 @@ class GettextTranslationHelper
|
|||
$this->updateTranslationTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the translation table for the main application
|
||||
*/
|
||||
public function compileIcingaTranslation()
|
||||
{
|
||||
$this->tablePath = implode(
|
||||
DIRECTORY_SEPARATOR,
|
||||
array(
|
||||
$this->appDir,
|
||||
'locale',
|
||||
$this->locale,
|
||||
'LC_MESSAGES',
|
||||
'icinga.po'
|
||||
)
|
||||
);
|
||||
|
||||
$this->compileTranslationTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the translation table for a particular module
|
||||
*
|
||||
|
@ -296,8 +235,12 @@ class GettextTranslationHelper
|
|||
'--language=PHP',
|
||||
'--keyword=translate',
|
||||
'--keyword=translate:1,2c',
|
||||
'--keyword=translateInDomain:2',
|
||||
'--keyword=translateInDomain:2,3c',
|
||||
'--keyword=translatePlural:1,2',
|
||||
'--keyword=translatePlural:1,2,4c',
|
||||
'--keyword=translatePluralInDomain:2,3',
|
||||
'--keyword=translatePluralInDomain:2,3,5c',
|
||||
'--keyword=mt:2',
|
||||
'--keyword=mt:2,3c',
|
||||
'--keyword=mtp:2,3',
|
||||
|
@ -326,15 +269,15 @@ class GettextTranslationHelper
|
|||
private function updateHeader($path)
|
||||
{
|
||||
$headerInfo = array(
|
||||
'title' => 'Icinga Web 2 - Head for multiple monitoring backends',
|
||||
'copyright_holder' => 'Icinga Development Team',
|
||||
'title' => $this->moduleMgr->getModule($this->moduleName)->getTitle(),
|
||||
'copyright_holder' => 'TEAM NAME',
|
||||
'copyright_year' => date('Y'),
|
||||
'author_name' => 'FIRST AUTHOR',
|
||||
'author_mail' => 'EMAIL@ADDRESS',
|
||||
'author_year' => 'YEAR',
|
||||
'project_name' => $this->moduleName ? ucfirst($this->moduleName) . ' Module' : 'Icinga Web 2',
|
||||
'project_name' => ucfirst($this->moduleName) . ' Module',
|
||||
'project_version' => $this->version,
|
||||
'project_bug_mail' => 'dev@icinga.com',
|
||||
'project_bug_mail' => 'ISSUE TRACKER',
|
||||
'pot_creation_date' => date('Y-m-d H:iO'),
|
||||
'po_revision_date' => 'YEAR-MO-DA HO:MI+ZONE',
|
||||
'translator_name' => 'FULL NAME',
|
||||
|
@ -418,7 +361,7 @@ class GettextTranslationHelper
|
|||
{
|
||||
shell_exec(sprintf(
|
||||
"sed -i 's;%s;../../../..;g' %s",
|
||||
$this->moduleDir ?: dirname($this->appDir),
|
||||
$this->moduleDir,
|
||||
$path
|
||||
));
|
||||
}
|
||||
|
@ -433,12 +376,7 @@ class GettextTranslationHelper
|
|||
$catalog = new File($this->catalogPath, 'w');
|
||||
|
||||
try {
|
||||
if ($this->moduleDir) {
|
||||
$this->getSourceFileNames($this->moduleDir, $catalog);
|
||||
} else {
|
||||
$this->getSourceFileNames($this->appDir, $catalog);
|
||||
$this->getSourceFileNames($this->libDir, $catalog);
|
||||
}
|
||||
$this->getSourceFileNames($this->moduleDir, $catalog);
|
||||
} catch (Exception $error) {
|
||||
throw $error;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Module: translation
|
||||
Version: 2.8.2
|
||||
Description: Translation module
|
||||
This module allows developers and translators to translate Icinga Web 2 and
|
||||
its modules for multiple languages. You do not need this module to run an
|
||||
internationalized web frontend. This is only for people who want to contribute
|
||||
translations or translate just their own modules.
|
||||
This module allows developers and translators to translate modules for multiple
|
||||
languages. You do not need this module to run an internationalized web frontend.
|
||||
This is only for people who want to contribute translations or translate just
|
||||
their own modules.
|
||||
|
|
|
@ -1,286 +0,0 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Tests\Icinga\Util;
|
||||
|
||||
use Exception;
|
||||
use Icinga\Test\BaseTestCase;
|
||||
use Icinga\Util\Translator;
|
||||
|
||||
class TranslatorWithHardcodedLocaleCodes extends Translator
|
||||
{
|
||||
public static function getAvailableLocaleCodes()
|
||||
{
|
||||
return array('en_US', 'de_DE', 'de_AT');
|
||||
}
|
||||
}
|
||||
|
||||
class TranslatorTest extends BaseTestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Translator::registerDomain('icingatest', BaseTestCase::$testDir . '/res/locale');
|
||||
}
|
||||
|
||||
public function testWhetherGetAvailableLocaleCodesReturnsAllAvailableLocaleCodes()
|
||||
{
|
||||
$expected = array(Translator::DEFAULT_LOCALE, 'de_DE', 'fr_FR');
|
||||
$result = Translator::getAvailableLocaleCodes();
|
||||
|
||||
sort($expected);
|
||||
sort($result);
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$result,
|
||||
'Translator::getAvailableLocaleCodes does not return all available locale codes'
|
||||
);
|
||||
}
|
||||
|
||||
public function testWhetherSetupLocaleSetsUpTheGivenLocale()
|
||||
{
|
||||
Translator::setupLocale('de_DE');
|
||||
$this->assertContains(
|
||||
setlocale(LC_ALL, 0),
|
||||
array('de_DE', 'de_DE.UTF-8'),
|
||||
'Translator::setupLocale does not properly set up a given locale'
|
||||
);
|
||||
}
|
||||
|
||||
public function testWhetherSetupLocaleThrowsAnExceptionWhenGivenAnInvalidLocale()
|
||||
{
|
||||
$this->expectException(\Icinga\Exception\IcingaException::class);
|
||||
|
||||
Translator::setupLocale('foobar');
|
||||
}
|
||||
|
||||
public function testWhetherSetupLocaleSetsCAsLocaleWhenGivenAnInvalidLocale()
|
||||
{
|
||||
try {
|
||||
Translator::setupLocale('foobar');
|
||||
$this->fail('Translator::setupLocale does not throw an exception when given an invalid locale');
|
||||
} catch (Exception $e) {
|
||||
$this->assertEquals(
|
||||
'C',
|
||||
setlocale(LC_ALL, 0),
|
||||
'Translator::setupLocale does not set C as locale in case the given one is invalid'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testWhetherTranslateReturnsTheCorrectMessageForTheCurrentLocale()
|
||||
{
|
||||
Translator::setupLocale('de_DE');
|
||||
|
||||
$this->assertEquals(
|
||||
'Lorem ipsum dolor sit amet!',
|
||||
Translator::translate('Lorem ipsum dolor sit amet', 'icingatest'),
|
||||
'Translator::translate does not translate the given message correctly to German'
|
||||
);
|
||||
|
||||
Translator::setupLocale('fr_FR');
|
||||
|
||||
$this->assertEquals(
|
||||
'Lorem ipsum dolor sit amet?',
|
||||
Translator::translate('Lorem ipsum dolor sit amet', 'icingatest'),
|
||||
'Translator::translate does not translate the given message correctly to French'
|
||||
);
|
||||
}
|
||||
|
||||
public function testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly()
|
||||
{
|
||||
$localeObj = Translator::splitLocaleCode('de_DE');
|
||||
$this->assertEquals(
|
||||
'de',
|
||||
$localeObj->language,
|
||||
'Translator::splitLocaleCode does not split the language code correctly'
|
||||
);
|
||||
$this->assertEquals(
|
||||
'DE',
|
||||
$localeObj->country,
|
||||
'Translator::splitLocaleCode does not split the country code correctly'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly
|
||||
*/
|
||||
public function testWhetherSplitLocaleCodeCanHandleEncodingSuffixes()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'US',
|
||||
Translator::splitLocaleCode('en_US.UTF-8')->country,
|
||||
'Translator::splitLocaleCode does not handle encoding suffixes correctly'
|
||||
);
|
||||
}
|
||||
|
||||
public function testWhetherSplitLocaleCodeInterpretsInvalidLocaleCodesAsLanguageCodes()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'de',
|
||||
Translator::splitLocaleCode('de')->language,
|
||||
'Translator::splitLocaleCode does not interpret invalid locale codes as language codes'
|
||||
);
|
||||
$this->assertEquals(
|
||||
'en~US',
|
||||
Translator::splitLocaleCode('en~US')->language,
|
||||
'Translator::splitLocaleCode does not interpret invalid locale codes as language codes'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly
|
||||
*/
|
||||
public function testWhetherSplitLocaleCodeReturnsTheDefaultLocaleWhenGivenCAsLocale()
|
||||
{
|
||||
$cLocaleObj = Translator::splitLocaleCode('C');
|
||||
$defaultLocaleObj = Translator::splitLocaleCode(Translator::DEFAULT_LOCALE);
|
||||
$this->assertEquals(
|
||||
$defaultLocaleObj->language,
|
||||
$cLocaleObj->language,
|
||||
'Translator::splitLocaleCode does not return the default language code when given C as locale'
|
||||
);
|
||||
$this->assertEquals(
|
||||
$defaultLocaleObj->country,
|
||||
$cLocaleObj->country,
|
||||
'Translator::splitLocaleCode does not return the default country code when given C as locale'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly
|
||||
* @depends testWhetherSplitLocaleCodeInterpretsInvalidLocaleCodesAsLanguageCodes
|
||||
*/
|
||||
public function testWhetherGetPreferredLocaleCodeFavorsPerfectMatches()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'de_DE',
|
||||
TranslatorWithHardcodedLocaleCodes::getPreferredLocaleCode('jp,de_DE;q=0.8,de;q=0.6'),
|
||||
'Translator::getPreferredLocaleCode does not favor perfect matches'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly
|
||||
* @depends testWhetherSplitLocaleCodeInterpretsInvalidLocaleCodesAsLanguageCodes
|
||||
*/
|
||||
public function testWhetherGetPreferredLocaleCodeReturnsThePreferredSimilarMatchEvenThoughAPerfectMatchWasFound()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'de_DE',
|
||||
TranslatorWithHardcodedLocaleCodes::getPreferredLocaleCode('de_CH,en_US;q=0.8'),
|
||||
'Translator::getPreferredLocaleCode does not return the preferred similar match'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly
|
||||
* @depends testWhetherSplitLocaleCodeInterpretsInvalidLocaleCodesAsLanguageCodes
|
||||
*/
|
||||
public function testWhetherGetPreferredLocaleCodeReturnsAPerfectMatchEvenThoughASimilarMatchWasFound()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'de_AT',
|
||||
TranslatorWithHardcodedLocaleCodes::getPreferredLocaleCode('de,de_AT;q=0.5'),
|
||||
'Translator::getPreferredLocaleCode does not return a perfect '
|
||||
. 'match if a similar match with higher priority was found'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeInterpretsInvalidLocaleCodesAsLanguageCodes
|
||||
*/
|
||||
public function testWhetherGetPreferredLocaleCodeReturnsASimilarMatchIfNoPerfectMatchCouldBeFound()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'de_DE',
|
||||
TranslatorWithHardcodedLocaleCodes::getPreferredLocaleCode('de,en'),
|
||||
'Translator::getPreferredLocaleCode does not return the most preferred similar match'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSplitLocaleCodeSplitsValidLocalesCorrectly
|
||||
*/
|
||||
public function testWhetherGetPreferredLocaleCodeReturnsTheDefaultLocaleIfNoMatchCouldBeFound()
|
||||
{
|
||||
$this->assertEquals(
|
||||
Translator::DEFAULT_LOCALE,
|
||||
TranslatorWithHardcodedLocaleCodes::getPreferredLocaleCode('fr_FR,jp_JP'),
|
||||
'Translator::getPreferredLocaleCode does not return the default locale if no match could be found'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSetupLocaleSetsUpTheGivenLocale
|
||||
*/
|
||||
public function testWhetherTranslatePluralReturnsTheSingularForm()
|
||||
{
|
||||
Translator::setupLocale('de_DE');
|
||||
|
||||
$result = Translator::translatePlural('test service', 'test services', 1, 'icingatest');
|
||||
|
||||
$expected = 'test dienst';
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$result,
|
||||
'Translator::translatePlural() could not return the translated singular form'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSetupLocaleSetsUpTheGivenLocale
|
||||
*/
|
||||
public function testWhetherTranslatePluralReturnsThePluralForm()
|
||||
{
|
||||
Translator::setupLocale('de_DE');
|
||||
|
||||
$result = Translator::translatePlural('test service', 'test services', 2, 'icingatest');
|
||||
|
||||
$expected = 'test dienste';
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$result,
|
||||
'Translator::translatePlural() could not return the translated plural form'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSetupLocaleSetsUpTheGivenLocale
|
||||
*/
|
||||
public function testWhetherTranslateReturnsTheContextForm()
|
||||
{
|
||||
Translator::setupLocale('de_DE');
|
||||
|
||||
$result = Translator::translate('context service', 'icingatest', 'test2');
|
||||
|
||||
$expected = 'context dienst test2';
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$result,
|
||||
'Translator::translate() could not return the translated context form'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherSetupLocaleSetsUpTheGivenLocale
|
||||
*/
|
||||
public function testWhetherTranslatePluralReturnsTheContextForm()
|
||||
{
|
||||
Translator::setupLocale('de_DE');
|
||||
|
||||
$result = Translator::translatePlural('context service', 'context services', 3, 'icingatest', 'test-context');
|
||||
|
||||
$expected = 'context plural dienste';
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$result,
|
||||
'Translator::translatePlural() could not return the translated context form'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Tests\Icinga\Regression;
|
||||
|
||||
use Icinga\Test\BaseTestCase;
|
||||
use Icinga\Util\Translator;
|
||||
|
||||
/**
|
||||
* Regression-Test for bug #6432
|
||||
*
|
||||
* Translating strings must not throw an exception even if the given domain is not valid.
|
||||
*
|
||||
* @see https://dev.icinga.com/issues/6432
|
||||
*/
|
||||
class Bug6432Test extends BaseTestCase
|
||||
{
|
||||
public function testWhetherTranslateReturnsTheInputStringInCaseTheGivenDomainIsNotValid()
|
||||
{
|
||||
$this->assertEquals('test', Translator::translate('test', 'invalid_domain'));
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,42 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Icinga Web 2 Test (0.0.1)\n"
|
||||
"Report-Msgid-Bugs-To: dev@icinga.com\n"
|
||||
"POT-Creation-Date: 2014-09-16 13:29+0200\n"
|
||||
"PO-Revision-Date: 2014-09-16 16:08+0100\n"
|
||||
"Last-Translator: Alexander Fuhr <alexander.fuhr@netways.de>\n"
|
||||
"Language: de_DE\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.5.4\n"
|
||||
|
||||
msgid "Lorem ipsum dolor sit amet"
|
||||
msgstr "Lorem ipsum dolor sit amet!"
|
||||
|
||||
msgid "test service"
|
||||
msgid_plural "test services"
|
||||
msgstr[0] "test dienst"
|
||||
msgstr[1] "test dienste"
|
||||
|
||||
msgctxt "test"
|
||||
msgid "context service"
|
||||
msgstr "context dienst test"
|
||||
|
||||
msgctxt "test2"
|
||||
msgid "context service"
|
||||
msgstr "context dienst test2"
|
||||
|
||||
msgctxt "test-contextu"
|
||||
msgid "context service"
|
||||
msgid_plural "context services"
|
||||
msgstr[0] "context plural dienstu"
|
||||
msgstr[1] "context plural diensteu"
|
||||
|
||||
msgctxt "test-context"
|
||||
msgid "context service"
|
||||
msgid_plural "context services"
|
||||
msgstr[0] "context plural dienst"
|
||||
msgstr[1] "context plural dienste"
|
Binary file not shown.
|
@ -1,2 +0,0 @@
|
|||
msgid "Lorem ipsum dolor sit amet"
|
||||
msgstr "Lorem ipsum dolor sit amet?"
|
Loading…
Reference in New Issue