summaryrefslogtreecommitdiffstats
path: root/modules/translation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:39:39 +0000
commit8ca6cc32b2c789a3149861159ad258f2cb9491e3 (patch)
tree2492de6f1528dd44eaa169a5c1555026d9cb75ec /modules/translation
parentInitial commit. (diff)
downloadicingaweb2-a598b28402ead15d3701df32e198513e7b1f299c.tar.xz
icingaweb2-a598b28402ead15d3701df32e198513e7b1f299c.zip
Adding upstream version 2.11.4.upstream/2.11.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--modules/translation/application/clicommands/CompileCommand.php40
-rw-r--r--modules/translation/application/clicommands/RefreshCommand.php40
-rw-r--r--modules/translation/application/clicommands/TestCommand.php140
-rw-r--r--modules/translation/doc/01-About.md6
-rw-r--r--modules/translation/doc/02-Installation.md15
-rw-r--r--modules/translation/doc/03-Translation.md204
-rw-r--r--modules/translation/doc/img/poedit_001.pngbin0 -> 24252 bytes
-rw-r--r--modules/translation/doc/img/poedit_002.pngbin0 -> 40936 bytes
-rw-r--r--modules/translation/doc/img/poedit_003.pngbin0 -> 21482 bytes
-rw-r--r--modules/translation/doc/img/poedit_004.pngbin0 -> 40052 bytes
-rw-r--r--modules/translation/doc/img/poedit_005.pngbin0 -> 23752 bytes
-rw-r--r--modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php232
-rw-r--r--modules/translation/library/Translation/Cli/TranslationCommand.php73
-rw-r--r--modules/translation/library/Translation/Util/GettextTranslationHelper.php442
-rw-r--r--modules/translation/module.info7
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
new file mode 100644
index 0000000..2d07b8e
--- /dev/null
+++ b/modules/translation/doc/img/poedit_001.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_002.png b/modules/translation/doc/img/poedit_002.png
new file mode 100644
index 0000000..d31e5ba
--- /dev/null
+++ b/modules/translation/doc/img/poedit_002.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_003.png b/modules/translation/doc/img/poedit_003.png
new file mode 100644
index 0000000..5f285f9
--- /dev/null
+++ b/modules/translation/doc/img/poedit_003.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_004.png b/modules/translation/doc/img/poedit_004.png
new file mode 100644
index 0000000..2c85dd9
--- /dev/null
+++ b/modules/translation/doc/img/poedit_004.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_005.png b/modules/translation/doc/img/poedit_005.png
new file mode 100644
index 0000000..3ae59ba
--- /dev/null
+++ b/modules/translation/doc/img/poedit_005.png
Binary files differ
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.