diff options
Diffstat (limited to '')
15 files changed, 1199 insertions, 0 deletions
diff --git a/modules/translation/application/clicommands/CompileCommand.php b/modules/translation/application/clicommands/CompileCommand.php new file mode 100644 index 0000000..8408009 --- /dev/null +++ b/modules/translation/application/clicommands/CompileCommand.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Clicommands; + +use Icinga\Module\Translation\Cli\TranslationCommand; + +/** + * Translation compiler + * + * This command will compile gettext catalogs of modules. + * + * 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 a module gettext catalog + * + * This will compile the catalog of the given module and locale. + * + * USAGE: + * + * icingacli translation compile <module> <locale> + * + * EXAMPLES: + * + * icingacli translation compile demo de_DE + * icingacli translation compile demo fr_FR + */ + public function moduleAction() + { + $module = $this->validateModuleName($this->params->shift()); + $locale = $this->validateLocaleCode($this->params->shift()); + + $helper = $this->getTranslationHelper($locale); + $helper->compileModuleTranslation($module); + } +} diff --git a/modules/translation/application/clicommands/RefreshCommand.php b/modules/translation/application/clicommands/RefreshCommand.php new file mode 100644 index 0000000..b4b2dc0 --- /dev/null +++ b/modules/translation/application/clicommands/RefreshCommand.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Clicommands; + +use Icinga\Module\Translation\Cli\TranslationCommand; + +/** + * Translation updater + * + * This command will create a new or update any existing gettext catalog of a module. + * + * 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 +{ + /** + * Generate or update a module gettext catalog + * + * This will create/update the PO-file of the given module and locale. + * + * USAGE: + * + * icingacli translation refresh module <module> <locale> + * + * EXAMPLES: + * + * icingacli translation refresh module demo de_DE + * icingacli translation refresh module demo fr_FR + */ + public function moduleAction() + { + $module = $this->validateModuleName($this->params->shift()); + $locale = $this->validateLocaleCode($this->params->shift()); + + $helper = $this->getTranslationHelper($locale); + $helper->updateModuleTranslations($module); + } +} diff --git a/modules/translation/application/clicommands/TestCommand.php b/modules/translation/application/clicommands/TestCommand.php new file mode 100644 index 0000000..347c2c9 --- /dev/null +++ b/modules/translation/application/clicommands/TestCommand.php @@ -0,0 +1,140 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Clicommands; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Translation\Cli\ArrayToTextTableHelper; +use Icinga\Module\Translation\Cli\TranslationCommand; +use ipl\I18n\GettextTranslator; +use ipl\I18n\StaticTranslator; + +/** + * Timestamp test helper + * + * + */ +class TestCommand extends TranslationCommand +{ + protected $locales = array(); + + /** + * Get translation examples for DateFormatter + * + * To help you check if the values got translated correctly + * + * USAGE: + * + * icingacli translation test dateformatter <locale> + * + * EXAMPLES: + * + * icingacli translation test dateformatter de_DE + * icingacli translation test dateformatter fr_FR + */ + public function dateformatterAction() + { + $time = time(); + + /** @uses DateFormatter::timeAgo */ + $this->printTable($this->getMultiTranslated( + 'Time Ago', + array('Icinga\Date\DateFormatter', 'timeAgo'), + array( + "15 sec" => $time - 15, + "62 sec" => $time - 62, + "10 min" => $time - 600, + "1h" => $time - 1 * 3600, + "3h" => $time - 3 * 3600, + "25h" => $time - 25 * 3600, + "31d" => $time - 31 * 24 * 3600, + ) + )); + + $this->printTable($this->getMultiTranslated( + 'Time Since', + array('Icinga\Date\DateFormatter', 'timeSince'), + array( + "15 sec" => $time - 15, + "62 sec" => $time - 62, + "10 min" => $time - 600, + "1h" => $time - 1 * 3600, + "3h" => $time - 3 * 3600, + "25h" => $time - 25 * 3600, + "31d" => $time - 31 * 24 * 3600, + ) + )); + + $this->printTable($this->getMultiTranslated( + 'Time Until', + array('Icinga\Date\DateFormatter', 'timeUntil'), + array( + "15 sec" => $time + 15, + "62 sec" => $time + 62, + "10 min" => $time + 600, + "1h" => $time + 1 * 3600, + "3h" => $time + 3 * 3600, + "25h" => $time + 25 * 3600, + "31d" => $time + 31 * 24 * 3600, + ) + )); + } + + public function defaultAction() + { + $this->dateformatterAction(); + } + + public function init() + { + foreach ($this->params->getAllStandalone() as $l) { + $this->locales[] = $l; + } + + if (empty($this->locales)) { + /** @var GettextTranslator $translator */ + $translator = StaticTranslator::$instance; + $this->locales = $translator->listLocales(); + } + } + + protected function callTranslated($callback, $arguments, $locale = 'en_US') + { + /** @var GettextTranslator $translator */ + $translator = StaticTranslator::$instance; + $translator->setLocale($locale); + return call_user_func_array($callback, $arguments); + } + + protected function getMultiTranslated($name, $callback, $arguments, $locales = null) + { + if ($locales === null) { + $locales = $this->locales; + } + array_unshift($locales, 'C'); + + $rows = array(); + + foreach ($arguments as $k => $args) { + $row = array($name => $k); + + if (! is_array($args)) { + $args = array($args); + } + foreach ($locales as $locale) { + $row[$locale] = $this->callTranslated($callback, $args, $locale); + } + $rows[] = $row; + } + + return $rows; + } + + protected function printTable($rows) + { + $tt = new ArrayToTextTableHelper($rows); + $tt->showHeaders(true); + $tt->render(); + echo "\n\n"; + } +} diff --git a/modules/translation/doc/01-About.md b/modules/translation/doc/01-About.md new file mode 100644 index 0000000..2eaacfa --- /dev/null +++ b/modules/translation/doc/01-About.md @@ -0,0 +1,6 @@ +# About the Translation Module <a id="translation-module-about"></a> + +Please read the following chapters for more insights on this module: + +* [Installation](02-Installation.md#translation-module-installation) +* [Translations](03-Translation.md#module-translation-introduction) diff --git a/modules/translation/doc/02-Installation.md b/modules/translation/doc/02-Installation.md new file mode 100644 index 0000000..04f85c8 --- /dev/null +++ b/modules/translation/doc/02-Installation.md @@ -0,0 +1,15 @@ +# Translation Module Installation <a id="translation-module-installation"></a> + +This module is provided with the Icinga Web 2 package and does +not need any extra installation step. + +## Enable the Module <a id="translation-module-enable"></a> + +Navigate to `Configuration` -> `Modules` -> `translation` and enable +the module. + +You can also enable the module during the setup wizard, or on the CLI: + +``` +icingacli module enable translation +``` diff --git a/modules/translation/doc/03-Translation.md b/modules/translation/doc/03-Translation.md new file mode 100644 index 0000000..14e2e88 --- /dev/null +++ b/modules/translation/doc/03-Translation.md @@ -0,0 +1,204 @@ +# Introduction <a id="module-translation-introduction"></a> + +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 modules to different languages. + +## Translation for Developers <a id="module-translation-developers"></a> + +To make use of the built-in translations in your module's code or views, you should use the method +`$this->translate('String to be translated')`, let's have a look at an example: + +```php +<?php + +class ExampleController extends Controller +{ + public function indexAction() + { + $this->view->title = $this->translate('Hello World'); + } +} +``` + +So if there a translation available for the `Hello World` string you will get an translated output, depends on the +language which is set in your configuration as the default language, if it is `de_DE` the output would be +`Hallo Welt`. + +The same works also for views: + +``` +<h1><?= $this->title ?></h1> +<p> + <?= $this->translate('Hello World') ?> + <?= $this->translate('String to be translated') ?> +</p> +``` + +If you need to provide placeholders in your messages, you should wrap the `$this->translate()` with `sprintf()` for e.g. + sprintf($this->translate('Hello User: (%s)'), $user->getName()) + +### Translating plural forms <a id="module-translation-plural-forms"></a> + +To provide a plural translation, just use the `translatePlural()` function. + +```php +<?php + +class ExampleController extends Controller +{ + public function indexAction() + { + $this->view->message = $this->translatePlural('Service', 'Services', 3); + } +} +``` + +### Context based translation <a id="module-translation-context-based"></a> + +If you want to provide context based translations, you can easily do it with an extra parameter in both methods +`translate()` and `translatePlural()`. + +```php +<?php + +class ExampleController extends Controller +{ + public function indexAction() + { + $this->view->title = $this->translate('My Title', 'mycontext'); + $this->view->message = $this->translatePlural('Service', 'Services', 3, 'mycontext'); + } +} +``` + +## Translation for Translators <a id="module-translation-translators"></a> + +> **Note**: +> +> If you want to translate Icinga Web 2 or any module made by Icinga, please head over to +> [translate.icinga.com](https://translate.icinga.com) instead. We won't accept any contributions +> in this regard other than those made there. + +Icinga Web 2 internally uses the UNIX standard gettext tool to perform internationalization, this means translation +files in the .po file format are supplied for text strings used in the code. + +There are a lot of tools and techniques to work with .po localization files, you can choose what ever you prefer. We +won't let you alone on your first steps and therefore we'll introduce you a nice tool, called Poedit. + +### Poedit <a id="module-translation-translators-poedit"></a> + +First of all, you have to download and install [Poedit](http://poedit.net). +When you are done, you have to configure Poedit. + +#### Configuration <a id="module-translation-translators-poedit-configuration"></a> + +`Personalize`: Please provide your Name and E-Mail under Identity. + +![Personalize](img/poedit_001.png) + +`Editor`: Under the `Behavior` the Automatically compile .mo files on save, should be disabled. + +![Editor](img/poedit_002.png) + +`Translations Memory`: Under the `Database` please add your languages, for which are you writing translations. + +![Translations Memory](img/poedit_003.png) + +When you are done, just save your new settings. + +#### Editing .po files <a id="module-translation-translators-poedit-edit-po-files"></a> + +> **Note** +> +> ll_CC stands for ll=language and CC=country code for e.g `de_DE`, `fr_FR`, `ru_RU`, `it_IT` etc. + +To work with .po files, open or create the one for your language located under +`application/locale/ll_CC/LC_MESSAGES/yourmodule.po`. As shown below, you will +get then a full list of all available translation strings for the module. Each +module names its translation files `%module_name%.po`. + +![Full list of strings](img/poedit_004.png) + +Now you can make changes and when there is no translation available, Poedit would mark it with a blue color, as shown +below. + +![Untranslated strings](img/poedit_005.png) + +And when you want to test your changes, please read more about under the chapter +[Testing Translations](Testing Translations). + +## Testing Translations <a id="module-translation-tests"></a> + +If you want to try out your translation changes in Icinga Web 2, you can make use of the CLI translations commands. + +> **Note**: +> +> Please make sure that the gettext package is installed + +To get an easier development with translations, you can activate the `translation module` which provides CLI commands, +after that you would be able to refresh and compile your .po files. + +Let's assume, we want to provide German translations for our just new created module `yourmodule`. + +If we haven't yet any translations strings in our .po file or even the .po file, we can use the CLI command, to do the +job for us: + +``` +icingacli translation refresh module yourmodule de_DE +``` + +This will go through all .php and .phtml files inside the module and a look after `$this->translate()` if there is +something to translate - if there is something and is not available in the `yourmodule.po` it will update this file +for us with new strings. + +Now you can open the `application/locale/de_DE/LC_MESSAGES/yourmodule.po` and you will see something similar: + +``` +# Icinga Web 2 - Head for multiple monitoring backends. +# Copyright (C) 2014 Icinga Development Team +# This file is distributed under the same license as Development Module. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Development Module (0.0.1)\n" +"Report-Msgid-Bugs-To: dev@icinga.com\n" +"POT-Creation-Date: 2014-09-09 10:12+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language: ll_CC\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" + +#: /modules/yourmodule/configuration.php:6 +msgid "yourmodule" +msgstr "" +``` + +Great, now you can adjust the file and provide the German `msgstr` for `yourmodule`. + +``` +#: /modules/yourmodule/configuration.php:6 +msgid "Dummy" +msgstr "Attrappe" +``` + +The last step is to compile the `yourmodule.po` to the `yourmodule.mo`: + +``` +icingacli translation compile module yourmodule de_DE +``` + +> **Note** +> +> After compiling it you need to restart the web server to get new translations available in your module. + +At this moment, everywhere in the module where the `Dummy` should be translated, it would return the translated +string `Attrappe`. diff --git a/modules/translation/doc/img/poedit_001.png b/modules/translation/doc/img/poedit_001.png Binary files differnew file mode 100644 index 0000000..2d07b8e --- /dev/null +++ b/modules/translation/doc/img/poedit_001.png diff --git a/modules/translation/doc/img/poedit_002.png b/modules/translation/doc/img/poedit_002.png Binary files differnew file mode 100644 index 0000000..d31e5ba --- /dev/null +++ b/modules/translation/doc/img/poedit_002.png diff --git a/modules/translation/doc/img/poedit_003.png b/modules/translation/doc/img/poedit_003.png Binary files differnew file mode 100644 index 0000000..5f285f9 --- /dev/null +++ b/modules/translation/doc/img/poedit_003.png diff --git a/modules/translation/doc/img/poedit_004.png b/modules/translation/doc/img/poedit_004.png Binary files differnew file mode 100644 index 0000000..2c85dd9 --- /dev/null +++ b/modules/translation/doc/img/poedit_004.png diff --git a/modules/translation/doc/img/poedit_005.png b/modules/translation/doc/img/poedit_005.png Binary files differnew file mode 100644 index 0000000..3ae59ba --- /dev/null +++ b/modules/translation/doc/img/poedit_005.png diff --git a/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php new file mode 100644 index 0000000..af01d5f --- /dev/null +++ b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php @@ -0,0 +1,232 @@ +<?php + +namespace Icinga\Module\Translation\Cli; + +/** + * Array to Text Table Generation Class + * + * @author Tony Landis <tony@tonylandis.com> + * @link http://www.tonylandis.com/ + * @copyright Copyright (C) 2006-2009 Tony Landis + * @license http://www.opensource.org/licenses/bsd-license.php + */ +class ArrayToTextTableHelper +{ + /** + * @var array The array for processing + */ + protected $rows; + + /** + * @var int The column width settings + */ + protected $cs = array(); + + /** + * @var int The Row lines settings + */ + protected $rs = array(); + + /** + * @var int The Column index of keys + */ + protected $keys = array(); + + /** + * @var int Max Column Height (returns) + */ + protected $mH = 2; + + /** + * @var int Max Row Width (chars) + */ + protected $mW = 30; + + protected $head = false; + protected $pcen = "+"; + protected $prow = "-"; + protected $pcol = "|"; + + + /** + * Prepare array into textual format + * + * @param array $rows The input array + * @param bool $head Show heading + * @param int $maxWidth Max Column Height (returns) + * @param int $maxHeight Max Row Width (chars) + */ + public function __construct($rows) + { + $this->rows =& $rows; + $this->cs = array(); + $this->rs = array(); + + if (! $xc = count($this->rows)) { + return false; + } + + $this->keys = array_keys($this->rows[0]); + $columns = count($this->keys); + + for ($x = 0; $x < $xc; $x++) { + for ($y = 0; $y < $columns; $y++) { + $this->setMax($x, $y, $this->rows[$x][$this->keys[$y]]); + } + } + + return $this; + } + + /** + * Show the headers using the key values of the array for the titles + * + * @param bool $bool + */ + public function showHeaders($bool) + { + if ($bool) { + $this->setHeading(); + } + } + + /** + * Set the maximum width (number of characters) per column before truncating + * + * @param int $maxWidth + */ + public function setMaxWidth($maxWidth) + { + $this->mW = (int) $maxWidth; + } + + /** + * Set the maximum height (number of lines) per row before truncating + * + * @param int $maxHeight + */ + public function setMaxHeight($maxHeight) + { + $this->mH = (int) $maxHeight; + } + + /** + * Prints the data to a text table + * + * @param bool $return Set to 'true' to return text rather than printing + * + * @return mixed + */ + public function render($return = false) + { + if ($return) { + ob_start(null, 0, true); + } + + $this->printLine(); + $this->printHeading(); + + $rc = count($this->rows); + for ($i = 0; $i < $rc; $i++) { + $this->printRow($i); + } + + $this->printLine(false); + + if ($return) { + $contents = ob_get_contents(); + ob_end_clean(); + return $contents; + } + return null; + } + + protected function setHeading() + { + $data = array(); + foreach ($this->keys as $colKey => $value) { + $this->setMax(false, $colKey, $value); + $data[$colKey] = strtoupper($value); + } + if (! is_array($data)) { + return false; + } + $this->head = $data; + + return $this; + } + + protected function printLine($nl = true) + { + print $this->pcen; + foreach ($this->cs as $key => $val) { + print $this->prow . + str_pad('', $val, $this->prow, STR_PAD_RIGHT) . + $this->prow . + $this->pcen; + } + if ($nl) { + print "\n"; + } + } + + protected function printHeading() + { + if (! is_array($this->head)) { + return false; + } + + print $this->pcol; + foreach ($this->cs as $key => $val) { + print ' ' . + str_pad($this->head[$key], $val, ' ', STR_PAD_BOTH) . + ' ' . + $this->pcol; + } + + print "\n"; + $this->printLine(); + + return $this; + } + + protected function printRow($rowKey) + { + // loop through each line + for ($line = 1; $line <= $this->rs[$rowKey]; $line++) { + print $this->pcol; + for ($colKey = 0; $colKey < count($this->keys); $colKey++) { + print " "; + print str_pad( + substr($this->rows[$rowKey][$this->keys[$colKey]], ($this->mW * ($line - 1)), $this->mW), + $this->cs[$colKey], + ' ', + STR_PAD_RIGHT + ); + print " " . $this->pcol; + } + print "\n"; + } + } + + protected function setMax($rowKey, $colKey, &$colVal) + { + $w = mb_strlen($colVal); + $h = 1; + if ($w > $this->mW) { + $h = ceil($w % $this->mW); + if ($h > $this->mH) { + $h = $this->mH; + } + $w = $this->mW; + } + + if (! isset($this->cs[$colKey]) || $this->cs[$colKey] < $w) { + $this->cs[$colKey] = $w; + } + + if ($rowKey !== false && (! isset($this->rs[$rowKey]) || $this->rs[$rowKey] < $h)) { + $this->rs[$rowKey] = $h; + } + } +} diff --git a/modules/translation/library/Translation/Cli/TranslationCommand.php b/modules/translation/library/Translation/Cli/TranslationCommand.php new file mode 100644 index 0000000..af3582c --- /dev/null +++ b/modules/translation/library/Translation/Cli/TranslationCommand.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Cli; + +use Exception; +use Icinga\Cli\Command; +use Icinga\Exception\IcingaException; +use Icinga\Module\Translation\Util\GettextTranslationHelper; + +/** + * Base class for translation commands + */ +class TranslationCommand extends Command +{ + /** + * Get the gettext translation helper + * + * @param string $locale + * + * @return GettextTranslationHelper + */ + public function getTranslationHelper($locale) + { + $helper = new GettextTranslationHelper($this->app, $locale); + $helper->setConfig($this->Config()); + return $helper; + } + + /** + * Check whether the given locale code is valid + * + * @param string $code The locale code to validate + * + * @return string The validated locale code + * + * @throws Exception In case the locale code is invalid + */ + public function validateLocaleCode($code) + { + if (! preg_match('@[a-z]{2}_[A-Z]{2}@', $code)) { + throw new IcingaException( + 'Locale code \'%s\' is not valid. Expected format is: ll_CC', + $code + ); + } + + return $code; + } + + /** + * Check whether the given module is available and enabled + * + * @param string $name The module name to validate + * + * @return string The validated module name + * + * @throws Exception In case the given module is not available or not enabled + */ + public function validateModuleName($name) + { + $enabledModules = $this->app->getModuleManager()->listEnabledModules(); + + if (! in_array($name, $enabledModules)) { + throw new IcingaException( + 'Module with name \'%s\' not found or is not enabled', + $name + ); + } + + return $name; + } +} diff --git a/modules/translation/library/Translation/Util/GettextTranslationHelper.php b/modules/translation/library/Translation/Util/GettextTranslationHelper.php new file mode 100644 index 0000000..d1e6ac2 --- /dev/null +++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php @@ -0,0 +1,442 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Util; + +use Exception; +use Icinga\Application\ApplicationBootstrap; +use Icinga\Application\Config; +use Icinga\Application\Modules\Manager; +use Icinga\Exception\IcingaException; +use Icinga\Util\File; + +/** + * This class provides some useful utility functions to handle gettext translations + */ +class GettextTranslationHelper +{ + /** + * All project files are supposed to have the same/this encoding + */ + const FILE_ENCODING = 'UTF-8'; + + /** + * Config + * + * @var Config + */ + protected $config; + + /** + * The source files to parse + * + * @var array + */ + private $sourceExtensions = array( + 'php', + 'phtml' + ); + + /** + * The module manager of the application's bootstrap + * + * @var Manager + */ + private $moduleMgr; + + /** + * The current version of Icingaweb 2 or of the module the catalog is being created for + * + * @var string + */ + private $version; + + /** + * The name of the module if any + * + * @var string + */ + private $moduleName; + + /** + * The locale used by this helper + * + * @var string + */ + private $locale; + + /** + * The path to the module, if any + * + * @var string + */ + private $moduleDir; + + /** + * The path to the file catalog + * + * @var string + */ + private $catalogPath; + + /** + * The path to the *.pot file + * + * @var string + */ + private $templatePath; + + /** + * The path to the *.po file + * + * @var string + */ + private $tablePath; + + /** + * Create a new TranslationHelper object + * + * @param ApplicationBootstrap $bootstrap The application's bootstrap object + * @param string $locale The locale to be used by this helper + */ + public function __construct(ApplicationBootstrap $bootstrap, $locale) + { + $this->moduleMgr = $bootstrap->getModuleManager(); + $this->locale = $locale; + } + + /** + * Cleanup temporary files + */ + public function __destruct() + { + if ($this->catalogPath !== null && file_exists($this->catalogPath)) { + unlink($this->catalogPath); + } + + if ($this->templatePath !== null && file_exists($this->templatePath)) { + unlink($this->templatePath); + } + } + + /** + * Get the config + * + * @return Config + */ + public function getConfig() + { + return $this->config; + } + + /** + * Set the config + * + * @param Config $config + * + * @return $this + */ + public function setConfig(Config $config) + { + $this->config = $config; + return $this; + } + + /** + * Update the translation table for a particular module + * + * @param string $module The name of the module for which to update the translation table + */ + public function updateModuleTranslations($module) + { + $this->catalogPath = tempnam(sys_get_temp_dir(), 'IcingaTranslation_'); + $this->templatePath = tempnam(sys_get_temp_dir(), 'IcingaPot_'); + $this->version = $this->moduleMgr->getModule($module)->getVersion(); + $this->moduleName = $this->moduleMgr->getModule($module)->getName(); + + $this->moduleDir = $this->moduleMgr->getModuleDir($module); + $this->tablePath = implode( + DIRECTORY_SEPARATOR, + array( + $this->moduleDir, + 'application', + 'locale', + $this->locale, + 'LC_MESSAGES', + $module . '.po' + ) + ); + + $this->createFileCatalog(); + $this->createTemplateFile(); + $this->updateTranslationTable(); + } + + /** + * Compile the translation table for a particular module + * + * @param string $module The name of the module for which to compile the translation table + */ + public function compileModuleTranslation($module) + { + $this->moduleDir = $this->moduleMgr->getModuleDir($module); + $this->tablePath = implode( + DIRECTORY_SEPARATOR, + array( + $this->moduleDir, + 'application', + 'locale', + $this->locale, + 'LC_MESSAGES', + $module . '.po' + ) + ); + + $this->compileTranslationTable(); + } + + /** + * Update any existing or create a new translation table using the gettext tools + * + * @throws Exception In case the translation table does not yet exist and cannot be created + */ + private function updateTranslationTable() + { + if (is_file($this->tablePath)) { + shell_exec(sprintf( + '%s --update --backup=none %s %s 2>&1', + $this->getConfig()->get('translation', 'msgmerge', '/usr/bin/env msgmerge'), + $this->tablePath, + $this->templatePath + )); + } else { + if ((!is_dir(dirname($this->tablePath)) && !@mkdir(dirname($this->tablePath), 0755, true)) || + !rename($this->templatePath, $this->tablePath)) { + throw new IcingaException( + 'Unable to create %s', + $this->tablePath + ); + } + } + $this->updateHeader($this->tablePath); + $this->fixSourceLocations($this->tablePath); + } + + /** + * Create the template file using the gettext tools + */ + private function createTemplateFile() + { + shell_exec( + implode( + ' ', + array( + $this->getConfig()->get('translation', 'xgettext', '/usr/bin/env xgettext'), + '--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', + '--keyword=mtp:2,3,5c', + '--keyword=t', + '--keyword=t:1,2c', + '--keyword=tp:1,2', + '--keyword=tp:1,2,4c', + '--keyword=N_', + '--sort-output', + '--force-po', + '--omit-header', + '--from-code=' . self::FILE_ENCODING, + '--files-from="' . $this->catalogPath . '"', + '--output="' . $this->templatePath . '"' + ) + ) + ); + } + + /** + * Create or update a gettext conformant header in the given file + * + * @param string $path The path to the file + */ + private function updateHeader($path) + { + $headerInfo = array( + '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' => ucfirst($this->moduleName) . ' Module', + 'project_version' => $this->version, + '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', + 'translator_mail' => 'EMAIL@ADDRESS', + 'language' => $this->locale, + 'language_team_name' => 'LANGUAGE', + 'language_team_url' => 'LL@li.org', + 'charset' => self::FILE_ENCODING + ); + + $content = file_get_contents($path); + if (strpos($content, '# ') === 0) { + $authorInfo = array(); + if (preg_match('@# (.+) <(.+)>, (\d+|YEAR)\.@', $content, $authorInfo)) { + $headerInfo['author_name'] = $authorInfo[1]; + $headerInfo['author_mail'] = $authorInfo[2]; + $headerInfo['author_year'] = $authorInfo[3]; + } + $revisionInfo = array(); + if (preg_match('@Revision-Date: (\d{4}-\d{2}-\d{2} \d{2}:\d{2}\+\d{4})@', $content, $revisionInfo)) { + $headerInfo['po_revision_date'] = $revisionInfo[1]; + } + $translatorInfo = array(); + if (preg_match('@Last-Translator: (.+) <(.+)>@', $content, $translatorInfo)) { + $headerInfo['translator_name'] = $translatorInfo[1]; + $headerInfo['translator_mail'] = $translatorInfo[2]; + } + $languageTeamInfo = array(); + if (preg_match('@Language-Team: (.+) <(.+)>@', $content, $languageTeamInfo)) { + $headerInfo['language_team_name'] = $languageTeamInfo[1]; + $headerInfo['language_team_url'] = $languageTeamInfo[2]; + } + $languageInfo = array(); + if (preg_match('@Language: ([a-z]{2}_[A-Z]{2})@', $content, $languageInfo)) { + $headerInfo['language'] = $languageInfo[1]; + } + } + + file_put_contents( + $path, + implode( + PHP_EOL, + array( + '# ' . $headerInfo['title'] . '.', + '# Copyright (C) ' . $headerInfo['copyright_year'] . ' ' . $headerInfo['copyright_holder'], + '# This file is distributed under the same license as ' . $headerInfo['project_name'] . '.', + '# ' . $headerInfo['author_name'] . ' <' . $headerInfo['author_mail'] + . '>, ' . $headerInfo['author_year'] . '.', + '# ', + '#, fuzzy', + 'msgid ""', + 'msgstr ""', + '"Project-Id-Version: ' . $headerInfo['project_name'] . ' (' + . $headerInfo['project_version'] . ')\n"', + '"Report-Msgid-Bugs-To: ' . $headerInfo['project_bug_mail'] . '\n"', + '"POT-Creation-Date: ' . $headerInfo['pot_creation_date'] . '\n"', + '"PO-Revision-Date: ' . $headerInfo['po_revision_date'] . '\n"', + '"Last-Translator: ' . $headerInfo['translator_name'] . ' <' + . $headerInfo['translator_mail'] . '>\n"', + '"Language: ' . $headerInfo['language'] . '\n"', + '"Language-Team: ' . $headerInfo['language_team_name'] . ' <' + . $headerInfo['language_team_url'] . '>\n"', + '"MIME-Version: 1.0\n"', + '"Content-Type: text/plain; charset=' . $headerInfo['charset'] . '\n"', + '"Content-Transfer-Encoding: 8bit\n"', + '"Plural-Forms: nplurals=2; plural=(n != 1);\n"', + '"X-Poedit-Basepath: .\n"', + '"X-Poedit-SearchPath-0: .\n"', + '' + ) + ) . PHP_EOL . substr($content, strpos($content, '#: ')) + ); + } + + /** + * Adjust all absolute source file paths so that they're all relative to the catalog's location + * + * @param string $path + */ + protected function fixSourceLocations($path) + { + shell_exec(sprintf( + "sed -i 's;%s;../../../..;g' %s", + $this->moduleDir, + $path + )); + } + + /** + * Create the file catalog + * + * @throws Exception In case the catalog-file cannot be created + */ + private function createFileCatalog() + { + $catalog = new File($this->catalogPath, 'w'); + + try { + $this->getSourceFileNames($this->moduleDir, $catalog); + } catch (Exception $error) { + throw $error; + } + + $catalog->fflush(); + } + + /** + * Recursively scan the given directory for translatable source files + * + * @param string $directory The directory where to search for sources + * @param File $file The file where to write the results + * @param array $blacklist A list of directories to omit + * + * @throws Exception In case the given directory is not readable + */ + private function getSourceFileNames($directory, File $file) + { + $directoryHandle = opendir($directory); + if (!$directoryHandle) { + throw new IcingaException( + 'Unable to read files from %s', + $directory + ); + } + + $subdirs = array(); + while (($filename = readdir($directoryHandle)) !== false) { + if ($filename[0] === '.' || $filename === 'vendor') { + continue; + } + $filepath = $directory . DIRECTORY_SEPARATOR . $filename; + if (preg_match('@^[^\.].+\.(' . implode('|', $this->sourceExtensions) . ')$@', $filename)) { + $file->fwrite($filepath . PHP_EOL); + } elseif (! is_link($filepath) && is_dir($filepath)) { + $subdirs[] = $filepath; + } + } + closedir($directoryHandle); + + foreach ($subdirs as $subdir) { + $this->getSourceFileNames($subdir, $file); + } + } + + /** + * Compile the translation table + */ + private function compileTranslationTable() + { + $targetPath = substr($this->tablePath, 0, strrpos($this->tablePath, '.')) . '.mo'; + shell_exec( + implode( + ' ', + array( + $this->getConfig()->get('translation', 'msgfmt', '/usr/bin/env msgfmt'), + '-o ' . $targetPath, + $this->tablePath + ) + ) + ); + } +} diff --git a/modules/translation/module.info b/modules/translation/module.info new file mode 100644 index 0000000..57a0dd2 --- /dev/null +++ b/modules/translation/module.info @@ -0,0 +1,7 @@ +Module: translation +Version: 2.11.4 +Description: Translation module + 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. |