summaryrefslogtreecommitdiffstats
path: root/modules/translation/library/Translation/Util/GettextTranslationHelper.php
diff options
context:
space:
mode:
Diffstat (limited to 'modules/translation/library/Translation/Util/GettextTranslationHelper.php')
-rw-r--r--modules/translation/library/Translation/Util/GettextTranslationHelper.php441
1 files changed, 441 insertions, 0 deletions
diff --git a/modules/translation/library/Translation/Util/GettextTranslationHelper.php b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
new file mode 100644
index 0000000..043b2b7
--- /dev/null
+++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
@@ -0,0 +1,441 @@
+<?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
+ *
+ * @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
+ )
+ )
+ );
+ }
+}