PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,396 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*
*/
namespace Piwik\Plugins\LanguagesManager;
use Piwik\Db;
use Piwik\Development;
use Piwik\Filesystem;
use Piwik\Piwik;
use Piwik\Cache as PiwikCache;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\Manager;
use Piwik\Translation\Loader\DevelopmentLoader;
/**
* The LanguagesManager API lets you access existing Matomo translations, and change Users languages preferences.
*
* "getTranslationsForLanguage" will return all translation strings for a given language,
* so you can leverage Matomo translations in your application (and automatically benefit from the <a href='https://matomo.org/translations/' rel='noreferrer' target='_blank'>40+ translations</a>!).
* This is mostly useful to developers who integrate Matomo API results in their own application.
*
* You can also request the default language to load for a user via "getLanguageForUser",
* or update it via "setLanguageForUser".
*
* @method static \Piwik\Plugins\LanguagesManager\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
protected $availableLanguageNames = null;
protected $languageNames = null;
/**
* Returns true if specified language is available
*
* @param string $languageCode
* @return bool true if language available; false otherwise
*/
public function isLanguageAvailable($languageCode)
{
return $languageCode !== false
&& Filesystem::isValidFilename($languageCode)
&& in_array($languageCode, $this->getAvailableLanguages());
}
/**
* Return array of available languages
*
* @return array Array of strings, each containing its ISO language code
*/
public function getAvailableLanguages()
{
if (!is_null($this->languageNames)) {
return $this->languageNames;
}
$path = PIWIK_INCLUDE_PATH . "/lang/";
$languagesPath = _glob($path . "*.json");
$pathLength = strlen($path);
$languages = array();
if ($languagesPath) {
foreach ($languagesPath as $language) {
$languages[] = substr($language, $pathLength, -strlen('.json'));
}
}
$this->enableDevelopmentLanguageInDevEnvironment($languages);
/**
* Hook called after loading available language files.
*
* Use this hook to customise the list of languagesPath available in Matomo.
*
* @param array
*/
Piwik::postEvent('LanguagesManager.getAvailableLanguages', array(&$languages));
/**
* Hook called after loading available language files.
*
* @param array
*
* @deprecated since v3.9.0 use LanguagesManager.getAvailableLanguages instead. Will be removed in Matomo 4.0.0
*/
Piwik::postEvent('LanguageManager.getAvailableLanguages', array(&$languages));
$this->languageNames = $languages;
return $languages;
}
/**
* Return information on translations (code, language, % translated, etc)
*
* @param boolean $excludeNonCorePlugins excludes non core plugin from percentage calculation
*
* @return array Array of arrays
*/
public function getAvailableLanguagesInfo($excludeNonCorePlugins=true)
{
$data = file_get_contents(PIWIK_INCLUDE_PATH . '/lang/en.json');
$englishTranslation = json_decode($data, true);
$pluginDirectories = Manager::getPluginsDirectories();
// merge with plugin translations if any
$pluginFiles = array();
foreach ($pluginDirectories as $pluginsDir) {
$pluginFiles = array_merge($pluginFiles, glob(sprintf('%s*/lang/en.json', $pluginsDir)));
}
foreach ($pluginFiles as $file) {
$fileWithoutPluginDir = str_replace($pluginDirectories, '', $file);
preg_match('/([^\/]+)\/lang/i', $fileWithoutPluginDir, $matches);
$plugin = $matches[1];
if (!$excludeNonCorePlugins || Manager::getInstance()->isPluginBundledWithCore($plugin)) {
$data = file_get_contents($file);
$pluginTranslations = json_decode($data, true);
$englishTranslation = array_merge_recursive($englishTranslation, $pluginTranslations);
}
}
$filenames = $this->getAvailableLanguages();
$languagesInfo = array();
foreach ($filenames as $filename) {
$data = file_get_contents(sprintf('%s/lang/%s.json', PIWIK_INCLUDE_PATH, $filename));
$translations = json_decode($data, true);
// merge with plugin translations if any
$pluginFiles = array();
foreach ($pluginDirectories as $pluginsDir) {
$pluginFiles = array_merge($pluginFiles, glob(sprintf('%s*/lang/%s.json', $pluginsDir, $filename)));
}
foreach ($pluginFiles as $file) {
$fileWithoutPluginDir = str_replace($pluginDirectories, '', $file);
preg_match('/([^\/]+)\/lang/i', $fileWithoutPluginDir, $matches);
$plugin = $matches[1];
if (!$excludeNonCorePlugins || Manager::getInstance()->isPluginBundledWithCore($plugin)) {
$data = file_get_contents($file);
$pluginTranslations = json_decode($data, true);
$translations = array_merge_recursive($translations, $pluginTranslations);
}
}
$intersect = function ($array, $array2) {
$res = $array;
foreach ($array as $module => $keys) {
if (!isset($array2[$module])) {
unset($res[$module]);
} else {
$res[$module] = array_intersect_key($res[$module], array_filter($array2[$module], 'strlen'));
}
}
return $res;
};
// Skip languages not having Intl translations
if (empty($translations['Intl'])) {
continue;
}
$translationStringsDone = $intersect($englishTranslation, $translations);
$percentageComplete = count($translationStringsDone, COUNT_RECURSIVE) / count($englishTranslation, COUNT_RECURSIVE);
$percentageComplete = round(100 * $percentageComplete, 0);
$languageInfo = array('code' => $filename,
'name' => $translations['Intl']['OriginalLanguageName'],
'english_name' => $translations['Intl']['EnglishLanguageName'],
'translators' => $translations['General']['TranslatorName'],
'percentage_complete' => $percentageComplete . '%',
);
$languagesInfo[] = $languageInfo;
}
return $languagesInfo;
}
/**
* Return array of available languages
*
* @return array Arry of array, each containing its ISO language code and name of the language
*/
public function getAvailableLanguageNames()
{
$this->loadAvailableLanguages();
return $this->availableLanguageNames;
}
/**
* Returns translation strings by language
*
* @param string $languageCode ISO language code
* @return array|false Array of arrays, each containing 'label' (translation index) and 'value' (translated string); false if language unavailable
*/
public function getTranslationsForLanguage($languageCode)
{
if (!$this->isLanguageAvailable($languageCode)) {
return false;
}
$data = file_get_contents(PIWIK_INCLUDE_PATH . "/lang/$languageCode.json");
$translations = json_decode($data, true);
$languageInfo = array();
foreach ($translations as $module => $keys) {
foreach ($keys as $key => $value) {
$languageInfo[] = array(
'label' => sprintf("%s_%s", $module, $key),
'value' => $value
);
}
}
foreach (PluginManager::getInstance()->getLoadedPluginsName() as $pluginName) {
$translations = $this->getPluginTranslationsForLanguage($pluginName, $languageCode);
if (!empty($translations)) {
foreach ($translations as $keys) {
$languageInfo[] = $keys;
}
}
}
return $languageInfo;
}
/**
* Returns translation strings by language for given plugin
*
* @param string $pluginName name of plugin
* @param string $languageCode ISO language code
* @return array|false Array of arrays, each containing 'label' (translation index) and 'value' (translated string); false if language unavailable
*
* @ignore
*/
public function getPluginTranslationsForLanguage($pluginName, $languageCode)
{
if (!$this->isLanguageAvailable($languageCode)) {
return false;
}
$languageFile = Manager::getPluginDirectory($pluginName) . "/lang/$languageCode.json";
if (!file_exists($languageFile)) {
return false;
}
$data = file_get_contents($languageFile);
$translations = json_decode($data, true);
$languageInfo = array();
foreach ($translations as $module => $keys) {
foreach ($keys as $key => $value) {
$languageInfo[] = array(
'label' => sprintf("%s_%s", $module, $key),
'value' => $value
);
}
}
return $languageInfo;
}
/**
* Returns the language for the user
*
* @param string $login
* @return string
*/
public function getLanguageForUser($login)
{
if ($login == 'anonymous') {
return false;
}
Piwik::checkUserHasSuperUserAccessOrIsTheUser($login);
$lang = $this->getModel()->getLanguageForUser($login);
return $lang;
}
private function getModel()
{
return new Model();
}
/**
* Sets the language for the user
*
* @param string $login
* @param string $languageCode
* @return bool
*/
public function setLanguageForUser($login, $languageCode)
{
Piwik::checkUserHasSuperUserAccessOrIsTheUser($login);
Piwik::checkUserIsNotAnonymous();
if (!$this->isLanguageAvailable($languageCode)) {
return false;
}
$this->getModel()->setLanguageForUser($login, $languageCode);
return true;
}
/**
* Returns whether the user uses 12 hour clock
*
* @param string $login
* @return string
*/
public function uses12HourClockForUser($login)
{
if ($login == 'anonymous') {
return false;
}
Piwik::checkUserHasSuperUserAccessOrIsTheUser($login);
$lang = $this->getModel()->uses12HourClock($login);
return $lang;
}
/**
* Returns whether the user uses 12 hour clock
*
* @param string $login
* @param bool $use12HourClock
* @return string
*/
public function set12HourClockForUser($login, $use12HourClock)
{
if ($login == 'anonymous') {
return false;
}
Piwik::checkUserHasSuperUserAccessOrIsTheUser($login);
$lang = $this->getModel()->set12HourClock($login, $use12HourClock);
return $lang;
}
private function loadAvailableLanguages()
{
if (!is_null($this->availableLanguageNames)) {
return;
}
$cacheId = 'availableLanguages';
$cache = PiwikCache::getEagerCache();
if ($cache->contains($cacheId)) {
$languagesInfo = $cache->fetch($cacheId);
} else {
$languages = $this->getAvailableLanguages();
$languagesInfo = array();
foreach ($languages as $languageCode) {
$data = @file_get_contents(PIWIK_INCLUDE_PATH . "/plugins/Intl/lang/$languageCode.json");
// Skip languages not having Intl translations
if (empty($data)) {
continue;
}
$translations = json_decode($data, true);
$languagesInfo[] = array(
'code' => $languageCode,
'name' => $translations['Intl']['OriginalLanguageName'],
'english_name' => $translations['Intl']['EnglishLanguageName']
);
}
$cache->save($cacheId, $languagesInfo);
}
$this->availableLanguageNames = $languagesInfo;
}
private function enableDevelopmentLanguageInDevEnvironment(&$languages)
{
if (!Development::isEnabled()) {
$key = array_search(DevelopmentLoader::LANGUAGE_ID, $languages);
if ($key) {
unset($languages[$key]);
}
}
}
}

View File

@ -0,0 +1,233 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Plugins\LanguagesManager\API;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class CreatePull extends TranslationBase
{
const GIT_BASE_BRANCH = '3.x-dev';
protected function configure()
{
$this->setName('translations:createpull')
->setDescription('Updates translation files')
->addOption('username', 'u', InputOption::VALUE_OPTIONAL, 'Transifex username')
->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'Transifex password')
->addOption('slug', 's', InputOption::VALUE_OPTIONAL, 'Transifex project slug')
->addOption('plugin', 'P', InputOption::VALUE_OPTIONAL, 'optional name of plugin to update translations for');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$changes = shell_exec('git status --porcelain -uno');
if (!empty($changes)) {
$output->writeln("You have uncommited changes. Creating pull request is only available with a clean working directory");
return;
}
$unpushedCommits = shell_exec('git log origin/' . self::GIT_BASE_BRANCH . '..HEAD');
if (!empty($unpushedCommits)) {
$output->writeln("You have unpushed commits. Creating pull request is only available with a clean working directory");
return;
}
chdir(PIWIK_DOCUMENT_ROOT);
shell_exec('
git checkout -f ' . self::GIT_BASE_BRANCH . ' > /dev/null 2>&1
git pull > /dev/null 2>&1
git submodule init > /dev/null 2>&1
git submodule update > /dev/null 2>&1
');
$plugin = $input->getOption('plugin');
if (!empty($plugin)) {
chdir(PIWIK_DOCUMENT_ROOT.DIRECTORY_SEPARATOR.'plugins'.DIRECTORY_SEPARATOR.$plugin);
shell_exec('
git checkout ' . self::GIT_BASE_BRANCH . ' > /dev/null 2>&1
git pull > /dev/null 2>&1
');
}
// check if branch exists localy and track it if not
$branch = shell_exec('git branch | grep translationupdates');
if (empty($branch)) {
shell_exec('git checkout -b translationupdates origin/translationupdates');
}
// switch to branch and update it to latest $GIT_BASE_BRANCH
shell_exec('
git checkout -f translationupdates > /dev/null 2>&1
git reset --hard origin/' . self::GIT_BASE_BRANCH . ' > /dev/null 2>&1
git push origin translationupdates > /dev/null 2>&1
');
// update translation files
$command = $this->getApplication()->find('translations:update');
$arguments = array(
'command' => 'translations:update',
'--username' => $input->getOption('username'),
'--password' => $input->getOption('password'),
'--slug' => $input->getOption('slug'),
'--plugin' => $plugin
);
$inputObject = new ArrayInput($arguments);
$inputObject->setInteractive($input->isInteractive());
$command->run($inputObject, $output);
shell_exec('git add lang/. > /dev/null 2>&1');
if (empty($plugin)) {
foreach (Update::getPluginsInCore() as $pluginName) {
shell_exec(sprintf('git add plugins/%s/lang/. > /dev/null 2>&1', $pluginName));
}
}
$changes = shell_exec('git status --porcelain -uno');
if (empty($changes)) {
$output->writeln("Nothing changed. Everything is already up to date.");
shell_exec('git checkout ' . self::GIT_BASE_BRANCH . ' > /dev/null 2>&1');
return;
}
API::unsetInstance(); // reset languagemanager api (to force refresh of data)
$stats = shell_exec('git diff --numstat HEAD');
preg_match_all('/([0-9]+)\t([0-9]+)\t[a-zA-Z\/]*lang\/([a-z]{2,3}(?:-[a-z]{2,3})?)\.json/', $stats, $lineChanges);
$addedLinesSum = 0;
if (!empty($lineChanges[1])) {
$addedLinesSum = array_sum($lineChanges[1]);
}
$linesSumByLang = array();
$lineChangesCount = count($lineChanges[0]);
for ($i = 0; $i < $lineChangesCount; $i++) {
@$linesSumByLang[$lineChanges[3][$i]] += $lineChanges[1][$i];
}
preg_match_all('/M [a-zA-Z\/]*lang\/([a-z]{2,3}(?:-[a-z]{2,3})?)\.json/', $changes, $modifiedFiles);
preg_match_all('/A [a-zA-Z\/]*lang\/([a-z]{2,3}(?:-[a-z]{2,3})?)\.json/', $changes, $addedFiles);
$messages = array();
$languageCodesTouched = array();
if (!empty($addedFiles[1])) {
foreach ($addedFiles[1] as $addedFile) {
$languageInfo = $this->getLanguageInfoByIsoCode($addedFile);
$messages[$addedFile] = sprintf('- Added %s (%s changes / %s translated)\n', $languageInfo['english_name'], $linesSumByLang[$addedFile], $languageInfo['percentage_complete']);
}
$languageCodesTouched = array_merge($languageCodesTouched, $addedFiles[1]);
}
if (!empty($modifiedFiles[1])) {
foreach ($modifiedFiles[1] as $modifiedFile) {
if ($linesSumByLang[$modifiedFile]) {
$languageInfo = $this->getLanguageInfoByIsoCode($modifiedFile);
$messages[$modifiedFile] = sprintf(
'- Updated %s (%s changes / %s translated)\n',
$languageInfo['english_name'],
$linesSumByLang[$modifiedFile],
$languageInfo['percentage_complete']
);
$languageCodesTouched[] = $modifiedFile;
}
}
$languageCodesTouched = array_unique($languageCodesTouched);
}
$message = implode('', $messages);
$message .= '\n\nHelp us translate Matomo in your language!\nSignup at https://www.transifex.com/matomo/matomo/\nIf you have any questions, get in touch with us at translations@matomo.org';
$languageCodesTouched = array_unique($languageCodesTouched, SORT_REGULAR);
$title = sprintf(
'Updated %s strings in %u languages (%s)',
$addedLinesSum,
count($languageCodesTouched),
implode(', ', $languageCodesTouched)
);
shell_exec('git commit -m "language update ${pluginName}"');
shell_exec('git push');
shell_exec('git checkout ' . self::GIT_BASE_BRANCH . ' > /dev/null 2>&1');
$this->createPullRequest($output, $title, $message);
}
private function getLanguageInfoByIsoCode($isoCode)
{
$languages = API::getInstance()->getAvailableLanguagesInfo();
foreach ($languages as $languageInfo) {
if ($languageInfo['code'] == $isoCode) {
return $languageInfo;
}
}
return array();
}
private function createPullRequest(OutputInterface $output, $title, $message)
{
$dialog = $this->getHelperSet()->get('dialog');
while (true) {
$username = $dialog->ask($output, 'Please provide your GitHub username (to create a pull request using GitHub API): ');
$returnCode = shell_exec('curl \
-X POST \
-k \
--silent \
--write-out %{http_code} \
--stderr /dev/null \
-o /dev/null \
-u '.$username.' \
--data "{\"title\":\"[automatic translation update] '.$title.'\",\"body\":\"'.$message.'\",\"head\":\"translationupdates\",\"base\":\"' . self::GIT_BASE_BRANCH . '\"}" \
-H "Accept: application/json" \
https://api.github.com/repos/matomo-org/matomo/pulls');
switch ($returnCode) {
case 401:
$output->writeln("Pull request failed. Bad credentials... Please try again");
continue 2;
case 422:
$output->writeln("Pull request failed. Unprocessable Entity. Maybe a pull request was already created before.");
return;
case 201:
case 200:
$output->writeln("Pull request successfully created.");
return;
default:
$output->writeln("Pull request failed... Please try again");
}
}
}
}

View File

@ -0,0 +1,128 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Container\StaticContainer;
use Piwik\Exception\AuthenticationFailedException;
use Piwik\Plugins\LanguagesManager\API as LanguagesManagerApi;
use Piwik\Translation\Transifex\API;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class FetchTranslations extends TranslationBase
{
const DOWNLOAD_PATH = '/transifex';
protected function configure()
{
$path = StaticContainer::get('path.tmp') . self::DOWNLOAD_PATH;
$this->setName('translations:fetch')
->setDescription('Fetches translations files from Transifex to ' . $path)
->addOption('username', 'u', InputOption::VALUE_OPTIONAL, 'Transifex username')
->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'Transifex password')
->addOption('lastupdate', 'l', InputOption::VALUE_OPTIONAL, 'Last time update ran', time()-30*24*3600)
->addOption('slug', 's', InputOption::VALUE_OPTIONAL, 'project slug on transifex', 'matomo')
->addOption('plugin', 'r', InputOption::VALUE_OPTIONAL, 'Plugin to update');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->setDecorated(true);
$username = $input->getOption('username');
$password = $input->getOption('password');
$plugin = $input->getOption('plugin');
$lastUpdate = $input->getOption('lastupdate');
$slug = $input->getOption('slug');
$resource = 'matomo-'. ($plugin ? 'plugin-'.strtolower($plugin) : 'base');
$transifexApi = new API($username, $password, $slug);
// remove all existing translation files in download path
$files = glob($this->getDownloadPath() . DIRECTORY_SEPARATOR . '*.json');
array_map('unlink', $files);
if (!$transifexApi->resourceExists($resource)) {
$output->writeln("Skipping resource $resource as it doesn't exist on Transifex");
return;
}
$output->writeln("Fetching translations from Transifex for resource $resource");
$availableLanguages = LanguagesManagerApi::getInstance()->getAvailableLanguageNames();
$languageCodes = array();
foreach ($availableLanguages as $languageInfo) {
$languageCodes[] = $languageInfo['code'];
}
$languageCodes = array_filter($languageCodes, function($code) {
return !in_array($code, array('en', 'dev'));
});
try {
$languages = $transifexApi->getAvailableLanguageCodes();
if (!empty($plugin)) {
$languages = array_filter($languages, function ($language) {
return LanguagesManagerApi::getInstance()->isLanguageAvailable(str_replace('_', '-', strtolower($language)));
});
}
} catch (AuthenticationFailedException $e) {
$languages = $languageCodes;
}
/** @var ProgressBar $progress */
$progress = new ProgressBar($output, count($languages));
$progress->start();
$statistics = $transifexApi->getStatistics($resource);
foreach ($languages as $language) {
try {
// if we have modification date given from statistics api compare it with given last update time to ignore not update resources
if (LanguagesManagerApi::getInstance()->isLanguageAvailable(str_replace('_', '-', strtolower($language))) && isset($statistics->$language)) {
$lastupdated = strtotime($statistics->$language->last_update);
if ($lastUpdate > $lastupdated) {
$progress->advance();
continue;
}
}
$translations = $transifexApi->getTranslations($resource, $language, true);
file_put_contents($this->getDownloadPath() . DIRECTORY_SEPARATOR . str_replace('_', '-', strtolower($language)) . '.json', $translations);
} catch (\Exception $e) {
$output->writeln("Error fetching language file $language: " . $e->getMessage());
}
$progress->advance();
}
$progress->finish();
$output->writeln('');
}
public static function getDownloadPath()
{
$path = StaticContainer::get('path.tmp') . self::DOWNLOAD_PATH;
if (!is_dir($path)) {
mkdir($path);
}
return $path;
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Plugins\LanguagesManager\API;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class LanguageCodes extends TranslationBase
{
protected function configure()
{
$this->setName('translations:languagecodes')
->setDescription('Shows available language codes');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$languages = API::getInstance()->getAvailableLanguageNames();
$languageCodes = array();
foreach ($languages as $languageInfo) {
$languageCodes[] = $languageInfo['code'];
}
sort($languageCodes);
$output->writeln("Currently available languages:");
$output->writeln(implode("\n", $languageCodes));
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Plugins\LanguagesManager\API;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class LanguageNames extends TranslationBase
{
protected function configure()
{
$this->setName('translations:languagenames')
->setDescription('Shows available language names');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$languages = API::getInstance()->getAvailableLanguageNames();
$languageNames = array();
foreach ($languages as $languageInfo) {
$languageNames[] = $languageInfo['english_name'];
}
sort($languageNames);
$output->writeln("Currently available languages:");
$output->writeln(implode("\n", $languageNames));
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Plugin\Manager;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class PluginsWithTranslations extends TranslationBase
{
protected function configure()
{
$this->setName('translations:plugins')
->setDescription('Shows all plugins that have own translation files');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln("Following plugins contain their own translation files:");
$pluginFiles = array();
foreach (Manager::getPluginsDirectories() as $pluginsDir) {
$pluginFiles = array_merge($pluginsDir, glob(sprintf('%s*/lang/en.json', $pluginsDir)));
}
$pluginFiles = array_map(function($elem){
$replace = Manager::getPluginsDirectories();
$replace[] = '/lang/en.json';
return str_replace($replace, '', $elem);
}, $pluginFiles);
$output->writeln(join("\n", $pluginFiles));
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Plugins\LanguagesManager\API;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByBaseTranslations;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByParameterCount;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EmptyTranslations;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EncodedEntities;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\UnnecassaryWhitespaces;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\CoreTranslations;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\NoScripts;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Writer;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class SetTranslations extends TranslationBase
{
protected function configure()
{
$this->setName('translations:set')
->setDescription('Sets new translations for a given language')
->addOption('code', 'c', InputOption::VALUE_REQUIRED, 'code of the language to set translations for')
->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'json file to load new translations from')
->addOption('plugin', 'pl', InputOption::VALUE_OPTIONAL, 'optional name of plugin to set translations for');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var DialogHelper $dialog */
$dialog = $this->getHelperSet()->get('dialog');
$languageCode = $input->getOption('code');
$filename = $input->getOption('file');
$languageCodes = (new API())->getAvailableLanguages();
if (empty($languageCode) || !in_array($languageCode, $languageCodes)) {
$languageCode = $dialog->askAndValidate($output, 'Please provide a valid language code: ', function ($code) use ($languageCodes) {
if (!in_array($code, array_values($languageCodes))) {
throw new \InvalidArgumentException(sprintf('Language code "%s" is invalid.', $code));
}
return $code;
});
}
if (empty($filename) || !file_exists($filename)) {
$filename = $dialog->askAndValidate($output, 'Please provide a file to load translations from: ', function ($file) {
if (!file_exists($file)) {
throw new \InvalidArgumentException(sprintf('File "%s" does not exist.', $file));
}
return $file;
});
}
$output->writeln("Starting to import data from '$filename' to language '$languageCode'");
$plugin = $input->getOption('plugin');
$translationWriter = new Writer($languageCode, $plugin);
$baseTranslations = $translationWriter->getTranslations("en");
$translationWriter->addValidator(new NoScripts());
if (empty($plugin)) {
$translationWriter->addValidator(new CoreTranslations($baseTranslations));
}
$translationWriter->addFilter(new ByBaseTranslations($baseTranslations));
$translationWriter->addFilter(new EmptyTranslations());
$translationWriter->addFilter(new ByParameterCount($baseTranslations));
$translationWriter->addFilter(new UnnecassaryWhitespaces($baseTranslations));
$translationWriter->addFilter(new EncodedEntities($baseTranslations));
$translationData = file_get_contents($filename);
$translations = json_decode($translationData, true);
$translationWriter->setTranslations($translations);
if (!$translationWriter->isValid()) {
$output->writeln("Failed setting translations:" . $translationWriter->getValidationMessage());
return;
}
if (!$translationWriter->hasTranslations()) {
$output->writeln("No translations available");
return;
}
$translationWriter->save();
$output->writeln("Finished.");
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Development;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
abstract class TranslationBase extends ConsoleCommand
{
public function isEnabled()
{
return Development::isEnabled();
}
}

View File

@ -0,0 +1,255 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\Commands;
use Piwik\Cache;
use Piwik\Plugin\Manager;
use Piwik\Plugins\LanguagesManager\API;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\ProgressBar;
/**
*/
class Update extends TranslationBase
{
protected function configure()
{
$this->setName('translations:update')
->setDescription('Updates translation files')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force update of all language files')
->addOption('username', 'u', InputOption::VALUE_OPTIONAL, 'Transifex username')
->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'Transifex password')
->addOption('slug', 's', InputOption::VALUE_OPTIONAL, 'Transifex project slug')
->addOption('all', 'a', InputOption::VALUE_NONE, 'Force to update all plugins (even non core). Can not be used with plugin option')
->addOption('plugin', 'P', InputOption::VALUE_OPTIONAL, 'optional name of plugin to update translations for');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->setDecorated(true);
$start = microtime(true);
/** @var DialogHelper $dialog */
$dialog = $this->getHelperSet()->get('dialog');
$languages = API::getInstance()->getAvailableLanguageNames();
$languageCodes = array();
foreach ($languages as $languageInfo) {
$languageCodes[] = $languageInfo['code'];
}
$plugin = $input->getOption('plugin');
$forceAllPlugins = $input->getOption('all');
if (!$input->isInteractive()) {
$output->writeln("(!) Non interactive mode: New languages will be skipped");
}
$pluginList = array($plugin);
if (empty($plugin)) {
$pluginList = $forceAllPlugins ? self::getAllPlugins() : self::getPluginsInCore();
array_unshift($pluginList, '');
} else {
$input->setOption('force', true); // force plugin only updates
}
foreach ($pluginList as $plugin) {
$output->writeln("");
// fetch base or specific plugin
$this->fetchTranslations($input, $output, $plugin);
$files = _glob(FetchTranslations::getDownloadPath() . DIRECTORY_SEPARATOR . '*.json');
if (count($files) == 0) {
$output->writeln("No translation updates available! Skipped.");
continue;
}
$output->writeln("Starting to import new language files");
/** @var ProgressBar $progress */
$progress = new ProgressBar($output, count($files));
$progress->start();
foreach ($files as $filename) {
$progress->advance();
$code = basename($filename, '.json');
if (!in_array($code, $languageCodes)) {
if (!empty($plugin)) {
continue; # never create a new language for plugin only
}
$createNewFile = false;
if ($input->isInteractive()) {
$createNewFile = $dialog->askConfirmation($output, "\nLanguage $code does not exist. Should it be added? ", false);
}
if (!$createNewFile) {
continue; # do not create a new file for the language
}
@touch(PIWIK_DOCUMENT_ROOT . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $code . '.json');
API::unsetAllInstances(); // unset language manager instance, so valid names are refetched
$command = $this->getApplication()->find('translations:generate-intl-data');
$arguments = array(
'command' => 'translations:generate-intl-data',
'--language' => $code,
);
$inputObject = new ArrayInput($arguments);
$inputObject->setInteractive($input->isInteractive());
$command->run($inputObject, $output->isVeryVerbose() ? $output : new NullOutput());
API::unsetAllInstances(); // unset language manager instance, so valid names are refetched
Cache::flushAll();
$languageCodes[] = $code;
}
$command = $this->getApplication()->find('translations:set');
$arguments = array(
'command' => 'translations:set',
'--code' => $code,
'--file' => $filename,
'--plugin' => $plugin
);
$inputObject = new ArrayInput($arguments);
$inputObject->setInteractive($input->isInteractive());
$command->run($inputObject, $output->isVeryVerbose() ? $output : new NullOutput());
}
$progress->finish();
$output->writeln('');
}
$output->writeln("Finished in " . round(microtime(true)-$start, 3) . "s");
}
/**
* Returns all plugins having their own translations that are bundled in core
* @return array
*/
public static function getAllPlugins()
{
static $pluginsWithTranslations;
if (!empty($pluginsWithTranslations)) {
return $pluginsWithTranslations;
}
$pluginsWithTranslations = array();
foreach (Manager::getPluginsDirectories() as $pluginsDir) {
$pluginsWithTranslations = array_merge($pluginsWithTranslations, glob(sprintf('%s*/lang/en.json', $pluginsDir)));
}
$pluginsWithTranslations = array_map(function ($elem) {
$replace = Manager::getPluginsDirectories();
$replace[] = '/lang/en.json';
return str_replace($replace, '', $elem);
}, $pluginsWithTranslations);
return $pluginsWithTranslations;
}
/**
* Returns all plugins having their own translations that are bundled in core
* @return array
*/
public static function getPluginsInCore()
{
static $pluginsInCore;
if (!empty($pluginsInCore)) {
return $pluginsInCore;
}
$submodules = shell_exec('git submodule status');
preg_match_all('/plugins\/([a-zA-z]+) /', $submodules, $matches);
$submodulePlugins = $matches[1];
// ignore complete new plugins as well
$changes = shell_exec('git status');
preg_match_all('/plugins\/([a-zA-z]+)\/\n/', $changes, $matches);
$newPlugins = $matches[1];
$pluginsNotInCore = array_merge($submodulePlugins, $newPlugins);
$pluginsWithTranslations = array();
foreach (Manager::getPluginsDirectories() as $pluginsDir) {
$pluginsWithTranslations = array_merge($pluginsWithTranslations, glob(sprintf('%s*/lang/en.json', $pluginsDir)));
}
$pluginsWithTranslations = array_map(function ($elem) {
$replace = Manager::getPluginsDirectories();
$replace[] = '/lang/en.json';
return str_replace($replace, '', $elem);
}, $pluginsWithTranslations);
$pluginsInCore = array_diff($pluginsWithTranslations, $pluginsNotInCore);
return $pluginsInCore;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param string $plugin
* @throws \Exception
*/
protected function fetchTranslations(InputInterface $input, OutputInterface $output, $plugin)
{
$command = $this->getApplication()->find('translations:fetch');
$arguments = array(
'command' => 'translations:fetch',
'--username' => $input->getOption('username'),
'--password' => $input->getOption('password'),
'--slug' => $input->getOption('slug'),
'--plugin' => $plugin
);
if ($input->getOption('force')) {
$arguments['--lastupdate'] = 1;
} else {
$lastModDate = strtotime('2015-01-04 00:00:00'); // date of initial transifex setup
try {
// try to find the language file (of given plugin) with the newest modification date in git log
$path = ($plugin ? 'plugins/' . $plugin . '/' : '') . 'lang';
$files = explode("\n", trim(shell_exec('git ls-tree -r --name-only HEAD ' . $path)));
foreach ($files as $file) {
$fileModDate = shell_exec('git log -1 --format="%at" -- ' . $file);
if (basename($file) != 'en.json' && $fileModDate > $lastModDate) {
$lastModDate = $fileModDate;
}
}
} catch (\Exception $e) {
}
if ($lastModDate != 0) {
$arguments['--lastupdate'] = $lastModDate;
}
}
$inputObject = new ArrayInput($arguments);
$inputObject->setInteractive($input->isInteractive());
$command->run($inputObject, $output);
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*
*/
namespace Piwik\Plugins\LanguagesManager;
use Piwik\Common;
use Piwik\DbHelper;
use Piwik\Piwik;
use Piwik\Url;
use Piwik\View;
/**
*/
class Controller extends \Piwik\Plugin\ControllerAdmin
{
/**
* anonymous = in the session
* authenticated user = in the session
*/
public function saveLanguage()
{
$language = Common::getRequestVar('language');
// Prevent CSRF only when piwik is not installed yet (During install user can change language)
if (DbHelper::isInstalled()) {
$this->checkTokenInUrl();
}
LanguagesManager::setLanguageForSession($language);
Url::redirectToReferrer();
}
public function searchTranslation()
{
Piwik::checkUserHasSomeAdminAccess();
return $this->renderTemplate('searchTranslation');
}
}

View File

@ -0,0 +1,230 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*
*/
namespace Piwik\Plugins\LanguagesManager;
use Exception;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\Cookie;
use Piwik\Db;
use Piwik\Intl\Locale;
use Piwik\Piwik;
use Piwik\ProxyHttp;
use Piwik\Translate;
use Piwik\Translation\Translator;
use Piwik\View;
/**
*
*/
class LanguagesManager extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::registerEvents
*/
public function registerEvents()
{
return array(
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Config.NoConfigurationFile' => 'initLanguage',
'Request.dispatchCoreAndPluginUpdatesScreen' => 'initLanguage',
'Request.dispatch' => 'initLanguage',
'Platform.initialized' => 'initLanguage',
'UsersManager.deleteUser' => 'deleteUserLanguage',
'Template.topBar' => 'addLanguagesManagerToOtherTopBar',
'Template.jsGlobalVariables' => 'jsGlobalVariables'
);
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/LanguagesManager/angularjs/languageselector/languageselector.directive.js";
$jsFiles[] = "plugins/LanguagesManager/angularjs/translationsearch/translationsearch.controller.js";
$jsFiles[] = "plugins/LanguagesManager/angularjs/translationsearch/translationsearch.directive.js";
}
/**
* Adds the languages drop-down list to topbars other than the main one rendered
* in CoreHome/templates/top_bar.twig. The 'other' topbars are on the Installation
* and CoreUpdater screens.
*/
public function addLanguagesManagerToOtherTopBar(&$str)
{
// piwik object & scripts aren't loaded in 'other' topbars
$str .= "<script type='text/javascript'>if (!window.piwik) window.piwik={};</script>";
$str .= "<script type='text/javascript' src='plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js'></script>";
$str .= "<script type='text/javascript' src='plugins/LanguagesManager/angularjs/languageselector/languageselector.directive.js'></script>";
$str .= $this->getLanguagesSelector();
}
/**
* Adds the languages drop-down list to topbars other than the main one rendered
* in CoreHome/templates/top_bar.twig. The 'other' topbars are on the Installation
* and CoreUpdater screens.
*/
public function jsGlobalVariables(&$str)
{
// piwik object & scripts aren't loaded in 'other' topbars
$str .= "piwik.languageName = '" . self::getLanguageNameForCurrentUser() . "';";
}
/**
* Renders and returns the language selector HTML.
*
* @return string
*/
public function getLanguagesSelector()
{
$view = new View("@LanguagesManager/getLanguagesSelector");
$view->languages = API::getInstance()->getAvailableLanguageNames();
$view->currentLanguageCode = self::getLanguageCodeForCurrentUser();
$view->currentLanguageName = self::getLanguageNameForCurrentUser();
return $view->render();
}
public function initLanguage()
{
/** @var Translator $translator */
$translator = StaticContainer::get('Piwik\Translation\Translator');
$language = Common::getRequestVar('language', '', 'string');
if (empty($language)) {
$userLanguage = self::getLanguageCodeForCurrentUser();
if (API::getInstance()->isLanguageAvailable($userLanguage)) {
$language = $userLanguage;
}
}
if (!empty($language) && API::getInstance()->isLanguageAvailable($language)) {
$translator->setCurrentLanguage($language);
}
$locale = $translator->translate('General_Locale');
Locale::setLocale($locale);
}
public function deleteUserLanguage($userLogin)
{
$model = new Model();
$model->deleteUserLanguage($userLogin);
}
/**
* @throws Exception if non-recoverable error
*/
public function install()
{
Model::install();
}
/**
* @throws Exception if non-recoverable error
*/
public function uninstall()
{
Model::uninstall();
}
/**
* @return boolean
*/
public static function uses12HourClockForCurrentUser()
{
try {
$currentUser = Piwik::getCurrentUserLogin();
return Request::processRequest('LanguagesManager.uses12HourClockForUser', array('login' => $currentUser));
} catch (Exception $e) {
return false;
}
}
/**
* @return string Two letters language code, eg. "fr"
*/
public static function getLanguageCodeForCurrentUser()
{
$languageCode = self::getLanguageFromPreferences();
if (!API::getInstance()->isLanguageAvailable($languageCode)) {
$languageCode = Common::extractLanguageCodeFromBrowserLanguage(Common::getBrowserLanguage(), API::getInstance()->getAvailableLanguages());
}
if (!API::getInstance()->isLanguageAvailable($languageCode)) {
$languageCode = Translate::getLanguageDefault();
}
return $languageCode;
}
/**
* @return string Full english language string, eg. "French"
*/
public static function getLanguageNameForCurrentUser()
{
$languageCode = self::getLanguageCodeForCurrentUser();
$languages = API::getInstance()->getAvailableLanguageNames();
foreach ($languages as $language) {
if ($language['code'] === $languageCode) {
return $language['name'];
}
}
return false;
}
/**
* @return string|false if language preference could not be loaded
*/
protected static function getLanguageFromPreferences()
{
if (($language = self::getLanguageForSession()) != null) {
return $language;
}
try {
$currentUser = Piwik::getCurrentUserLogin();
return API::getInstance()->getLanguageForUser($currentUser);
} catch (Exception $e) {
return false;
}
}
/**
* Returns the language for the session
*
* @return string|null
*/
public static function getLanguageForSession()
{
$cookieName = Config::getInstance()->General['language_cookie_name'];
$cookie = new Cookie($cookieName);
if ($cookie->isCookieFound()) {
return $cookie->get('language');
}
return null;
}
/**
* Set the language for the session
*
* @param string $languageCode ISO language code
* @return bool
*/
public static function setLanguageForSession($languageCode)
{
if (!API::getInstance()->isLanguageAvailable($languageCode)) {
return false;
}
$cookieName = Config::getInstance()->General['language_cookie_name'];
$cookie = new Cookie($cookieName, 0);
$cookie->set('language', $languageCode);
$cookie->setSecure(ProxyHttp::isHttps());
$cookie->save();
return true;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager;
use Piwik\Development;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Piwik;
use Piwik\SettingsPiwik;
class Menu extends \Piwik\Plugin\Menu
{
public function configureTopMenu(MenuTop $menu)
{
if (Piwik::isUserIsAnonymous() || !SettingsPiwik::isPiwikInstalled()) {
$langManager = new LanguagesManager();
$menu->addHtml('LanguageSelector', $langManager->getLanguagesSelector(), true, $order = 30, false);
}
}
public function configureAdminMenu(MenuAdmin $menu)
{
if (Development::isEnabled() && Piwik::isUserHasSomeAdminAccess()) {
$menu->addDevelopmentItem('LanguagesManager_TranslationSearch',
$this->urlForAction('searchTranslation'));
}
}
}

View File

@ -0,0 +1,103 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*
*/
namespace Piwik\Plugins\LanguagesManager;
use Piwik\Common;
use Piwik\Db;
use Piwik\DbHelper;
class Model
{
private static $rawPrefix = 'user_language';
private $table;
public function __construct()
{
$this->table = Common::prefixTable(self::$rawPrefix);
}
public function deleteUserLanguage($userLogin)
{
Db::query('DELETE FROM ' . $this->table . ' WHERE login = ?', $userLogin);
}
/**
* Returns the language for the user
*
* @param string $userLogin
* @return string
*/
public function getLanguageForUser($userLogin)
{
return Db::fetchOne('SELECT language FROM ' . $this->table .
' WHERE login = ? ', array($userLogin));
}
/**
* Sets the language for the user
*
* @param string $login
* @param string $languageCode
* @return bool
*/
public function setLanguageForUser($login, $languageCode)
{
$query = 'INSERT INTO ' . $this->table .
' (login, language) VALUES (?,?) ON DUPLICATE KEY UPDATE language=?';
$bind = array($login, $languageCode, $languageCode);
Db::query($query, $bind);
return true;
}
/**
* Returns whether the given user has chosen to use 12 hour clock
*
* @param $userLogin
* @return bool
* @throws \Exception
*/
public function uses12HourClock($userLogin)
{
return (bool) Db::fetchOne('SELECT use_12_hour_clock FROM ' . $this->table .
' WHERE login = ? ', array($userLogin));
}
/**
* Sets whether the given user wants to use 12 hout clock
*
* @param string $login
* @param string $use12HourClock
* @return bool
*/
public function set12HourClock($login, $use12HourClock)
{
$query = 'INSERT INTO ' . $this->table .
' (login, use_12_hour_clock) VALUES (?,?) ON DUPLICATE KEY UPDATE use_12_hour_clock=?';
$bind = array($login, $use12HourClock, $use12HourClock);
Db::query($query, $bind);
return true;
}
public static function install()
{
$userLanguage = "login VARCHAR( 100 ) NOT NULL ,
language VARCHAR( 10 ) NOT NULL ,
use_12_hour_clock TINYINT(1) NOT NULL DEFAULT 0 ,
PRIMARY KEY ( login )";
DbHelper::createTable(self::$rawPrefix, $userLanguage);
}
public static function uninstall()
{
Db::dropTables(Common::prefixTable(self::$rawPrefix));
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
class ByBaseTranslations extends FilterAbstract
{
protected $baseTranslations = array();
/**
* Sets base translations
*
* @param array $baseTranslations
*/
public function __construct($baseTranslations = array())
{
$this->baseTranslations = $baseTranslations;
}
/**
* Removes all translations that aren't present in the base translations set in constructor
*
* @param array $translations
*
* @return array filtered translations
*/
public function filter($translations)
{
$cleanedTranslations = array();
foreach ($translations as $pluginName => $pluginTranslations) {
if (empty($this->baseTranslations[$pluginName])) {
$this->filteredData[$pluginName] = $pluginTranslations;
continue;
}
foreach ($pluginTranslations as $key => $translation) {
if (isset($this->baseTranslations[$pluginName][$key])) {
$cleanedTranslations[$pluginName][$key] = $translation;
}
}
if (!empty($cleanedTranslations[$pluginName])) {
$diff = array_diff($translations[$pluginName], $cleanedTranslations[$pluginName]);
} else {
$diff = $translations[$pluginName];
}
if (!empty($diff)) {
$this->filteredData[$pluginName] = $diff;
}
}
return $cleanedTranslations;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
class ByParameterCount extends FilterAbstract
{
protected $baseTranslations = array();
/**
* Sets base translations
*
* @param array $baseTranslations
*/
public function __construct($baseTranslations = array())
{
$this->baseTranslations = $baseTranslations;
}
/**
* Removes all translations where the placeholder parameter count differs to base translation
*
* @param array $translations
*
* @return array filtered translations
*/
public function filter($translations)
{
$cleanedTranslations = array();
foreach ($translations as $pluginName => $pluginTranslations) {
foreach ($pluginTranslations as $key => $translation) {
if (isset($this->baseTranslations[$pluginName][$key])) {
$baseTranslation = $this->baseTranslations[$pluginName][$key];
} else {
// english string was deleted, do not error
continue;
}
// ensure that translated strings have the same number of %s as the english source strings
$baseCount = $this->_getParametersCountToReplace($baseTranslation);
$translationCount = $this->_getParametersCountToReplace($translation);
if ($baseCount != $translationCount) {
$this->filteredData[$pluginName][$key] = $translation;
continue;
}
$cleanedTranslations[$pluginName][$key] = $translation;
}
}
return $cleanedTranslations;
}
/**
* Counts the placeholder parameters n given string
*
* @param string $string
* @return array
*/
protected function _getParametersCountToReplace($string)
{
$sprintfParameters = array('%s', '%1$s', '%2$s', '%3$s', '%4$s', '%5$s', '%6$s', '%7$s', '%8$s', '%9$s');
$count = array();
foreach ($sprintfParameters as $parameter) {
$placeholderCount = substr_count($string, $parameter);
if ($placeholderCount > 0) {
$count[$parameter] = $placeholderCount;
}
}
return $count;
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
class EmptyTranslations extends FilterAbstract
{
/**
* Removes all empty translations
*
* @param array $translations
*
* @return array filtered translations
*/
public function filter($translations)
{
$translationsBefore = $translations;
foreach ($translations as $plugin => &$pluginTranslations) {
$pluginTranslations = array_filter($pluginTranslations, function ($value) {
return !empty($value) && '' != trim($value);
});
$diff = array_diff($translationsBefore[$plugin], $pluginTranslations);
if (!empty($diff)) {
$this->filteredData[$plugin] = $diff;
}
}
// remove plugins without translations
$translations = array_filter($translations, function ($value) {
return !empty($value) && count($value);
});
return $translations;
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
use Piwik\Translate;
class EncodedEntities extends FilterAbstract
{
protected $baseTranslations = array();
/**
* Sets base translations
*
* @param array $baseTranslations
*/
public function __construct($baseTranslations = array())
{
$this->baseTranslations = $baseTranslations;
}
/**
* Decodes all encoded entities in the given translations
*
* @param array $translations
*
* @return array filtered translations
*/
public function filter($translations)
{
foreach ($translations as $pluginName => $pluginTranslations) {
foreach ($pluginTranslations as $key => $translation) {
if (isset($this->baseTranslations[$pluginName][$key]) &&
$this->baseTranslations[$pluginName][$key] != Translate::clean($this->baseTranslations[$pluginName][$key])) {
continue; // skip if base translation already contains encoded entities
}
// remove encoded entities
$decoded = Translate::clean($translation);
if ($translation != $decoded) {
$this->filteredData[$pluginName][$key] = $translation;
$translations[$pluginName][$key] = $decoded;
continue;
}
}
}
return $translations;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
abstract class FilterAbstract
{
protected $filteredData = array();
/**
* Filter the given translations
*
* @param array $translations
*
* @return array filtered translations
*/
abstract public function filter($translations);
/**
* Returnes the data filtered out by the filter
*
* @return array
*/
public function getFilteredData()
{
return $this->filteredData;
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
class UnnecassaryWhitespaces extends FilterAbstract
{
protected $baseTranslations = array();
/**
* Sets base translations
*
* @param array $baseTranslations
*/
public function __construct($baseTranslations = array())
{
$this->baseTranslations = $baseTranslations;
}
/**
* Removes all unnecassary whitespaces and newlines from the given translations
*
* @param array $translations
*
* @return array filtered translations
*/
public function filter($translations)
{
foreach ($translations as $pluginName => $pluginTranslations) {
foreach ($pluginTranslations as $key => $translation) {
$baseTranslation = '';
if (isset($this->baseTranslations[$pluginName][$key])) {
$baseTranslation = $this->baseTranslations[$pluginName][$key];
}
// remove excessive line breaks (and leading/trailing whitespace) from translations
$stringNoLineBreak = trim($translation);
$stringNoLineBreak = str_replace("\r", "", $stringNoLineBreak); # remove useless carrige renturns
$stringNoLineBreak = preg_replace('/(\n[ ]+)/', "\n", $stringNoLineBreak); # remove useless white spaces after line breaks
$stringNoLineBreak = preg_replace('/([\n]{2,})/', "\n\n", $stringNoLineBreak); # remove excessive line breaks
if (empty($baseTranslation) || !substr_count($baseTranslation, "\n")) {
$stringNoLineBreak = preg_replace("/[\n]+/", " ", $stringNoLineBreak); # remove all line breaks if english string doesn't contain any
}
$stringNoLineBreak = preg_replace('/([ ]{2,})/', " ", $stringNoLineBreak); # remove excessive white spaces again as there might be any now, after removing line breaks
if ($translation !== $stringNoLineBreak) {
$this->filteredData[$pluginName][$key] = $translation;
$translations[$pluginName][$key] = $stringNoLineBreak;
continue;
}
}
}
return $translations;
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Validate;
use Piwik\Container\StaticContainer;
use Piwik\Intl\Data\Provider\LanguageDataProvider;
use Piwik\Intl\Data\Provider\RegionDataProvider;
class CoreTranslations extends ValidateAbstract
{
/**
* Error States
*/
const ERRORSTATE_LOCALEREQUIRED = 'Locale required';
const ERRORSTATE_TRANSLATORINFOREQUIRED = 'Translator info required';
const ERRORSTATE_LOCALEINVALID = 'Locale is invalid';
const ERRORSTATE_LOCALEINVALIDLANGUAGE = 'Locale is invalid - invalid language code';
const ERRORSTATE_LOCALEINVALIDCOUNTRY = 'Locale is invalid - invalid country code';
protected $baseTranslations = array();
/**
* Sets base translations
*
* @param array $baseTranslations
*/
public function __construct($baseTranslations = array())
{
$this->baseTranslations = $baseTranslations;
}
/**
* Validates the given translations
* * There need to be more than 250 translations present
* * Locale and TranslatorName needs to be set in plugin General
* * Locale must be valid (format, language & country)
*
* @param array $translations
*
* @return boolean
*/
public function isValid($translations)
{
$this->message = null;
if (empty($translations['General']['Locale'])) {
$this->message = self::ERRORSTATE_LOCALEREQUIRED;
return false;
}
if (empty($translations['General']['TranslatorName'])) {
$this->message = self::ERRORSTATE_TRANSLATORINFOREQUIRED;
return false;
}
/** @var LanguageDataProvider $languageDataProvider */
$languageDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
/** @var RegionDataProvider $regionDataProvider */
$regionDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
$allLanguages = $languageDataProvider->getLanguageList();
$allCountries = $regionDataProvider->getCountryList();
if ('eo.UTF-8' === $translations['General']['Locale']) {
return true;
}
if (!preg_match('/^([a-z]{2})_([A-Z]{2})\.UTF-8$/', $translations['General']['Locale'], $matches)) {
$this->message = self::ERRORSTATE_LOCALEINVALID;
return false;
} else if (!array_key_exists($matches[1], $allLanguages)) {
$this->message = self::ERRORSTATE_LOCALEINVALIDLANGUAGE;
return false;
} else if (!array_key_exists(strtolower($matches[2]), $allCountries)) {
$this->message = self::ERRORSTATE_LOCALEINVALIDCOUNTRY;
return false;
}
return true;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Validate;
class NoScripts extends ValidateAbstract
{
/**
* Validates the given translations
* * No script like parts should be present in any part of the translations
*
* @param array $translations
*
* @return boolean
*/
public function isValid($translations)
{
$this->message = null;
// check if any translation contains restricted script tags
$serializedStrings = serialize($translations);
$invalids = array("<script", 'document.', 'javascript:', 'src=', 'background=', 'onload=');
foreach ($invalids as $invalid) {
if (stripos($serializedStrings, $invalid) !== false) {
$this->message = 'script tags restricted for language files';
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Validate;
abstract class ValidateAbstract
{
protected $message = null;
/**
* Returns if the given translations are valid
*
* @param array $translations
*
* @return boolean
*/
abstract public function isValid($translations);
/**
* Returns an array of messages that explain why the most recent isValid()
* call returned false.
*
* @return array
*/
public function getMessage()
{
return $this->message;
}
}

View File

@ -0,0 +1,388 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\LanguagesManager\TranslationWriter;
use Exception;
use Piwik\Container\StaticContainer;
use Piwik\Filesystem;
use Piwik\Piwik;
use Piwik\Plugin\Manager;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\FilterAbstract;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\ValidateAbstract;
/**
* Writes translations to file.
*/
class Writer
{
/**
* current language to write files for
*
* @var string
*/
protected $language = '';
/**
* Name of a plugin (if set in constructor)
*
* @var string|null
*/
protected $pluginName = null;
/**
* translations to write to file
*
* @var array
*/
protected $translations = array();
/**
* Validators to check translations with
*
* @var ValidateAbstract[]
*/
protected $validators = array();
/**
* Message why validation failed
*
* @var string|null
*/
protected $validationMessage = null;
/**
* Filters to to apply to translations
*
* @var FilterAbstract[]
*/
protected $filters = array();
/**
* Messages which filter changed the data
*
* @var array
*/
protected $filterMessages = array();
const UNFILTERED = 'unfiltered';
const FILTERED = 'filtered';
protected $currentState = self::UNFILTERED;
/**
* If $pluginName is given, Writer will be initialized for the given plugin if it exists
* Otherwise it will be initialized for core translations
*
* @param string $language ISO 639-1 alpha-2 language code
* @param string $pluginName optional plugin name
* @throws \Exception
*/
public function __construct($language, $pluginName = null)
{
$this->setLanguage($language);
if (!empty($pluginName)) {
$installedPlugins = \Piwik\Plugin\Manager::getInstance()->readPluginsDirectory();
if (!in_array($pluginName, $installedPlugins)) {
throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($pluginName)));
}
$this->pluginName = $pluginName;
}
}
/**
* @param string $language ISO 639-1 alpha-2 language code
*
* @throws \Exception
*/
public function setLanguage($language)
{
if (!preg_match('/^([a-z]{2,3}(-[a-z]{2,3})?)$/i', $language)) {
throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($language)));
}
$this->language = strtolower($language);
}
/**
* @return string ISO 639-1 alpha-2 language code
*/
public function getLanguage()
{
return $this->language;
}
/**
* Returns if there are translations available or not
* @return bool
*/
public function hasTranslations()
{
return !empty($this->translations);
}
/**
* Set the translations to write (and cleans them)
*
* @param $translations
*/
public function setTranslations($translations)
{
$this->currentState = self::UNFILTERED;
$this->translations = $translations;
$this->applyFilters();
}
/**
* Get translations from file
*
* @param string $lang ISO 639-1 alpha-2 language code
* @throws Exception
* @return array Array of translations ( plugin => ( key => translated string ) )
*/
public function getTranslations($lang)
{
$path = $this->getTranslationPathBaseDirectory('lang', $lang);
if (!is_readable($path)) {
return array();
}
$data = file_get_contents($path);
$translations = json_decode($data, true);
return $translations;
}
/**
* Returns the temporary path for translations
*
* @return string
*/
public function getTemporaryTranslationPath()
{
return $this->getTranslationPathBaseDirectory('tmp');
}
/**
* Returns the path to translation files
*
* @return string
*/
public function getTranslationPath()
{
return $this->getTranslationPathBaseDirectory('lang');
}
/**
* Get translation file path based on given params
*
* @param string $base Optional base directory (either 'lang' or 'tmp')
* @param string|null $lang forced language
* @throws \Exception
* @return string path
*/
protected function getTranslationPathBaseDirectory($base, $lang = null)
{
if (empty($lang)) {
$lang = $this->getLanguage();
}
if (!empty($this->pluginName)) {
if ($base == 'tmp') {
return sprintf('%s/plugins/%s/lang/%s.json', StaticContainer::get('path.tmp'), $this->pluginName, $lang);
} else {
return sprintf('%s/lang/%s.json', Manager::getPluginDirectory($this->pluginName), $lang);
}
}
if ($base == 'tmp') {
return sprintf('%s/%s.json', StaticContainer::get('path.tmp'), $lang);
}
return sprintf('%s/%s/%s.json', PIWIK_INCLUDE_PATH, $base, $lang);
}
/**
* Converts translations to a string that can be written to a file
*
* @return string
*/
public function __toString()
{
/*
* Use JSON_UNESCAPED_UNICODE and JSON_PRETTY_PRINT for PHP >= 5.4
*/
$options = 0;
if (defined('JSON_UNESCAPED_UNICODE')) {
$options |= JSON_UNESCAPED_UNICODE;
}
if (defined('JSON_PRETTY_PRINT')) {
$options |= JSON_PRETTY_PRINT;
}
return json_encode($this->translations, $options);
}
/**
* Save translations to file; translations should already be cleaned.
*
* @throws \Exception
* @return bool|int False if failure, or number of bytes written
*/
public function save()
{
$this->applyFilters();
if (!$this->hasTranslations() || !$this->isValid()) {
throw new Exception('unable to save empty or invalid translations');
}
$path = $this->getTranslationPath();
Filesystem::mkdir(dirname($path));
return file_put_contents($path, $this->__toString());
}
/**
* Save translations to temporary file; translations should already be cleansed.
*
* @throws \Exception
* @return bool|int False if failure, or number of bytes written
*/
public function saveTemporary()
{
$this->applyFilters();
if (!$this->hasTranslations() || !$this->isValid()) {
throw new Exception('unable to save empty or invalid translations');
}
$path = $this->getTemporaryTranslationPath();
Filesystem::mkdir(dirname($path));
return file_put_contents($path, $this->__toString());
}
/**
* Adds an validator to check before saving
*
* @param ValidateAbstract $validator
*/
public function addValidator(ValidateAbstract $validator)
{
$this->validators[] = $validator;
}
/**
* Returns if translations are valid to save or not
*
* @return bool
*/
public function isValid()
{
$this->applyFilters();
$this->validationMessage = null;
foreach ($this->validators as $validator) {
if (!$validator->isValid($this->translations)) {
$this->validationMessage = $validator->getMessage();
return false;
}
}
return true;
}
/**
* Returns last validation message
*
* @return null|string
*/
public function getValidationMessage()
{
return $this->validationMessage;
}
/**
* Returns if the were translations removed while cleaning
*
* @return bool
*/
public function wasFiltered()
{
return !empty($this->filterMessages);
}
/**
* Returns the cleaning errors
*
* @return array
*/
public function getFilterMessages()
{
return $this->filterMessages;
}
/**
* @param FilterAbstract $filter
*/
public function addFilter(FilterAbstract $filter)
{
$this->filters[] = $filter;
}
/**
* @throws \Exception
*
* @return bool error state
*/
protected function applyFilters()
{
// skip if already cleaned
if ($this->currentState == self::FILTERED) {
return $this->wasFiltered();
}
$this->filterMessages = array();
// skip if not translations available
if (!$this->hasTranslations()) {
$this->currentState = self::FILTERED;
return false;
}
$cleanedTranslations = $this->translations;
foreach ($this->filters as $filter) {
$cleanedTranslations = $filter->filter($cleanedTranslations);
$filteredData = $filter->getFilteredData();
if (!empty($filteredData)) {
$this->filterMessages[] = get_class($filter) . " changed: " . var_export($filteredData, 1);
}
}
$this->currentState = self::FILTERED;
if ($cleanedTranslations != $this->translations) {
$this->filterMessages[] = 'translations have been cleaned';
}
$this->translations = $cleanedTranslations;
return $this->wasFiltered();
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\LanguagesManager;
use Piwik\Updater;
use Piwik\Updates;
use Piwik\Updater\Migration\Factory as MigrationFactory;
class Updates_2_15_1_b1 extends Updates
{
/**
* @var MigrationFactory
*/
private $migration;
public function __construct(MigrationFactory $factory)
{
$this->migration = $factory;
}
public function getMigrations(Updater $updater)
{
return array(
$this->migration->db->addColumn('user_language', 'use_12_hour_clock', 'TINYINT(1) NOT NULL DEFAULT 0', 'language')
);
}
public function doUpdate(Updater $updater)
{
$updater->executeMigrations(__FILE__, $this->getMigrations($updater));
}
}

View File

@ -0,0 +1,36 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div class="languageSelection">
* </div>
*/
(function () {
angular.module('piwikApp').directive('languageSelection', languageSelection);
function languageSelection() {
return {
restrict: 'C',
link: function(scope, element, attr, ctrl) {
function postLanguageChange () {
var value = $(this).attr('value');
if (value) {
element.find('#language').val(value).parents('form').submit();
}
}
element.on('click', 'a[value]', postLanguageChange);
scope.$on('$destroy', function() {
element.off('click', 'a[value]', postLanguageChange);
});
}
};
}
})();

View File

@ -0,0 +1,68 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('TranslationSearchController', TranslationSearchController);
TranslationSearchController.$inject = ['piwikApi'];
function TranslationSearchController(piwikApi) {
function fetchTranslations(languageCode) {
piwikApi.fetch({
method: 'LanguagesManager.getTranslationsForLanguage',
filter_limit: -1,
languageCode: languageCode
}).then(function (response) {
if (response) {
if (languageCode === 'en') {
vm.existingTranslations = response;
} else {
vm.compareTranslations = {};
angular.forEach(response, function (translation) {
vm.compareTranslations[translation.label] = translation.value;
});
}
}
});
}
function fetchLanguages() {
piwikApi.fetch({
method: 'LanguagesManager.getAvailableLanguagesInfo',
filter_limit: -1
}).then(function (languages) {
vm.languages = [{key: '', value: 'None'}];
if (languages) {
angular.forEach(languages, function (language) {
if (language.code === 'en') {
return;
}
vm.languages.push({key: language.code, value: language.name});
});
}
});
}
var vm = this;
vm.compareTranslations = null;
vm.existingTranslations = [];
vm.languages = [];
vm.compareLanguage = '';
this.doCompareLanguage = function () {
if (vm.compareLanguage) {
vm.compareTranslations = null;
fetchTranslations(vm.compareLanguage);
}
};
fetchTranslations('en');
fetchLanguages();
}
})();

View File

@ -0,0 +1,44 @@
<div>
<p>
This page helps you to find existing translations that you can reuse in your Plugin.
If you want to know more about translations have a look at our <a href="https://developer.matomo.org/guides/internationalization" rel="noreferrer noopener" target="_blank">Internationalization guide</a>.
Enter a search term to find translations and their corresponding keys:
</p>
<div piwik-field uicontrol="text" name="alias"
inline-help="Search for English translation. Max 1000 results will be shown."
ng-model="translationSearch.searchTerm"
placeholder="Search for English translation">
</div>
<div piwik-field uicontrol="select" name="translationSearch.compareLanguage"
inline-help="Optionally select a language to compare the English language with."
ng-model="translationSearch.compareLanguage"
ng-change="translationSearch.doCompareLanguage()"
options='translationSearch.languages'>
</div>
<br />
<br />
<table piwik-content-table
ng-show="translationSearch.searchTerm"
style="word-break: break-all;">
<thead>
<tr>
<th style="width:250px;">Key</th>
<th>English translation</th>
<th ng-show="translationSearch.compareLanguage && translationSearch.compareTranslations">Compare translation</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="translation in translationSearch.existingTranslations | filter:translationSearch.searchTerm | limitTo: 1000">
<td>{{ translation.label }}</td>
<td>{{ translation.value }}</td>
<td ng-show="translationSearch.compareLanguage && translationSearch.compareTranslations">{{ translationSearch.compareTranslations[translation.label] }}</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,31 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
*
* <div piwik-translation-search></div>
*
* Will show a text box which allows the user to search for translation keys and actual translations. Currently,
* only english is supported.
*/
(function () {
angular.module('piwikApp').directive('piwikTranslationSearch', piwikTranslationSearch);
piwikTranslationSearch.$inject = ['piwik'];
function piwikTranslationSearch(piwik){
return {
restrict: 'A',
scope: {},
templateUrl: 'plugins/LanguagesManager/angularjs/translationsearch/translationsearch.directive.html?cb=' + piwik.cacheBuster,
controller: 'TranslationSearchController',
controllerAs: 'translationSearch'
};
}
})();

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "حول ترجمة Matomo",
"TranslationSearch": "بحث في الترجمة"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Аб перакладах Matomo"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "За Matomo преводите"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Matomo অনুবাদ সম্পর্কে",
"TranslationSearch": "অনুবাদ অনুসন্ধান"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Quant a les traduccions del Matomo",
"TranslationSearch": "Cerca traduccions"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "O překladech Matomou",
"TranslationSearch": "Vyhledávání v překladech"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Om Matomo oversættelser",
"TranslationSearch": "Oversættelsessøgning"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Über Matomo Übersetzungen",
"TranslationSearch": "Übersetzungssuche"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Σχετικά με τις μεταφράσεις του Matomo",
"TranslationSearch": "Αναζήτηση Μετάφρασης"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "About Matomo translations",
"TranslationSearch": "Translation Search"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"TranslationSearch": "Búsqueda de localización"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Acerca de las traducciones de Matomo",
"TranslationSearch": "Buscar traducción"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Rohkem infot Matomou tõlkimisest",
"TranslationSearch": "Tõlke otsing"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "درباره ترجمه های پیویک",
"TranslationSearch": "جستجوی ترجمه"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Tietoja Matomon käännöksistä",
"TranslationSearch": "Käännösten haku"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "À propos des traductions de Matomo",
"TranslationSearch": "Recherche de traduction"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Matomo अनुवाद के बारे में",
"TranslationSearch": "अनुवाद खोजें"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "A Matomo fordításokról"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Tentang penerjemahan Matomo",
"TranslationSearch": "Pencarian Translasi"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Um Þýðendur Matomo"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Riguardo alle traduzioni di Matomo",
"TranslationSearch": "Ricerca Traduzione"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Matomo の翻訳について",
"TranslationSearch": "翻訳の検索"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Matomo თარგმანების შესახებ"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Matomo 번역에 대해",
"TranslationSearch": "번역본 검색"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Apie Matomo vertimus"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Par Matomo tulkojumiem"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Om Matomo-oversettelser",
"TranslationSearch": "Oversettelsessøk"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Over Matomo-vertalingen",
"TranslationSearch": "Vertaling zoeken"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Om Matomo-omsetjingane"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "O tłumaczeniach Matomo",
"TranslationSearch": "Przeszukaj tłumaczenia"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Sobre traduções do Matomo.",
"TranslationSearch": "Pesquisa de tradução"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Sobre as traduções do Matomo",
"TranslationSearch": "Pesquisa de traduções"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Despre traduceri Matomo"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "О переводах Matomo",
"TranslationSearch": "Поиск перевода"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "O prekladoch Matomo",
"TranslationSearch": "Hľadanie prekladu"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "O prevodih Matomo-a"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Mbi përkthimet e Matomo-s",
"TranslationSearch": "Kërkim Përkthimi"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "O Matomo prevodima",
"TranslationSearch": "Pretraživanje prevoda"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Om Matomo's översättningar",
"TranslationSearch": "Sök översättning"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "పివిక్ అనువాదాల గురించి"
}
}

View File

@ -0,0 +1,5 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "เกี่ยวกับการแปล Matomo"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Tungkol sa pagsasaling ng Matomo",
"TranslationSearch": "Paghahanap ng Pagsasalin"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Matomo çevirileri hakkında",
"TranslationSearch": "Çeviri Arama"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Про переклади Matomo",
"TranslationSearch": "Пошук перекладу"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "Về Matomo dịch thuật",
"TranslationSearch": "Tìm kiếm bản dịch"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "关于 Matomo 翻译",
"TranslationSearch": "交易搜索"
}
}

View File

@ -0,0 +1,6 @@
{
"LanguagesManager": {
"AboutPiwikTranslations": "關於 Matomo 翻譯",
"TranslationSearch": "翻譯搜尋"
}
}

View File

@ -0,0 +1,18 @@
<div class="languageSelection"
ng-cloak
menu-title="{{ currentLanguageName|e('html_attr') }}"
piwik-menudropdown>
<a class="item" target="_blank" rel="noreferrer noopener"
href="https://matomo.org/translations/">{{ 'LanguagesManager_AboutPiwikTranslations'|translate }}</a>
{% for language in languages %}
<a class="item {% if language.code == currentLanguageCode %}active{% endif %}"
value="{{ language.code }}"
title="{{ language.name }} ({{ language.english_name }})">{{ language.name }}</a>
{% endfor %}
<form action="index.php?module=LanguagesManager&amp;action=saveLanguage" method="post">
<input type="hidden" name="language" id="language">
{# During installation token_auth is not set #}
{% if token_auth is defined %}<input type="hidden" name="token_auth" value="{{ token_auth }}"/>{% endif %}
</form>
</div>

View File

@ -0,0 +1,11 @@
{% extends 'admin.twig' %}
{% set title %}{{ 'LanguagesManager_TranslationSearch'|translate }}{% endset %}
{% block content %}
<div piwik-content-block content-title="{{ title|e('html_attr') }}" feature="true">
<div piwik-translation-search></div>
</div>
{% endblock %}