1638 lines
No EOL
50 KiB
PHP
1638 lines
No EOL
50 KiB
PHP
<?php
|
|
/**
|
|
* @brief translater, a plugin for Dotclear 2
|
|
*
|
|
* @package Dotclear
|
|
* @subpackage Plugin
|
|
*
|
|
* @author Jean-Christian Denis & contributors
|
|
*
|
|
* @copyright Jean-Christian Denis
|
|
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
|
|
*/
|
|
|
|
if (!defined('DC_CONTEXT_ADMIN')) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Translater tools.
|
|
*/
|
|
class dcTranslater
|
|
{
|
|
public $core;
|
|
public $proposal;
|
|
|
|
/** @var array $iso List of l10n code/name allowed from clearbricks l10n class */
|
|
protected static $iso = [];
|
|
|
|
/** @var array $allowed_backup_folders List of allowed backup folder */
|
|
public static $allowed_backup_folders = [
|
|
'public',
|
|
'module',
|
|
'plugin',
|
|
'cache',
|
|
'translater'
|
|
];
|
|
|
|
/** @var array $allowed_l10n_groups List of place of tranlsations */
|
|
public static $allowed_l10n_groups = [
|
|
'main',
|
|
'public',
|
|
'theme',
|
|
'admin',
|
|
'date',
|
|
'error'
|
|
];
|
|
|
|
/** @var array $allowed_user_informations List of user info can be parsed */
|
|
public static $allowed_user_informations = [
|
|
'firstname',
|
|
'displayname',
|
|
'name',
|
|
'email',
|
|
'url'
|
|
];
|
|
|
|
/** @var array $default_settings Plugins default settings */
|
|
private $default_settings = [
|
|
'plugin_menu' => [
|
|
'id' => 'translater_plugin_menu',
|
|
'value' => 0,
|
|
'type' => 'boolean',
|
|
'label' => 'Put an link in plugins page'
|
|
],
|
|
'theme_menu' => [
|
|
'id' => 'translater_theme_menu',
|
|
'value' => 0,
|
|
'type' => 'boolean',
|
|
'label' => 'Put a link in themes page'
|
|
],
|
|
'backup_auto' => [
|
|
'id' => 'translater_backup_auto',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Make a backup of languages old files when there are modified'
|
|
],
|
|
'backup_limit' => [
|
|
'id' => 'translater_backup_limit',
|
|
'value' => 20,
|
|
'type' => 'string',
|
|
'label' => 'Maximum backups per module'
|
|
],
|
|
'backup_folder' => [
|
|
'id' => 'translater_backup_folder',
|
|
'value' => 'module',
|
|
'type' => 'string',
|
|
'label' => 'In which folder to store backups'
|
|
],
|
|
'start_page' => [
|
|
'id' => 'translater_start_page',
|
|
'value' => 'setting',
|
|
'type' => 'string',
|
|
'label' => 'Page to start on'
|
|
],
|
|
'write_po' => [
|
|
'id' => 'translater_write_po',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Write .po languages files'
|
|
],
|
|
'write_langphp' => [
|
|
'id' => 'translater_write_langphp',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Write .lang.php languages files'
|
|
],
|
|
'scan_tpl' => [
|
|
'id' => 'translater_scan_tpl',
|
|
'value' => 0,
|
|
'type' => 'boolean',
|
|
'label' => 'Translate strings of templates files'
|
|
],
|
|
'parse_nodc' => [
|
|
'id' => 'translater_parse_nodc',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Translate only untranslated strings of Dotclear',
|
|
],
|
|
'hide_default' => [
|
|
'id' => 'translater_hide_default',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Hide default modules of Dotclear',
|
|
],
|
|
'parse_comment' => [
|
|
'id' => 'translater_parse_comment',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Write comments and strings informations in lang files'
|
|
],
|
|
'parse_user' => [
|
|
'id' => 'translater_parse_user',
|
|
'value' => 1,
|
|
'type' => 'boolean',
|
|
'label' => 'Write inforamtions about author in lang files'
|
|
],
|
|
'parse_userinfo' => [
|
|
'id' => 'translater_parse_userinfo',
|
|
'value' => 'displayname, email',
|
|
'type' => 'string',
|
|
'label' => 'Type of informations about user to write'
|
|
],
|
|
'import_overwrite' => [
|
|
'id' => 'translater_import_overwrite',
|
|
'value' => 0,
|
|
'type' => 'boolean',
|
|
'label' => 'Overwrite existing languages when import packages'
|
|
],
|
|
'export_filename' => [
|
|
'id' => 'translater_export_filename',
|
|
'value' => 'type-module-l10n-timestamp',
|
|
'type' => 'string',
|
|
'label' => 'Name of files of exported package'
|
|
],
|
|
'proposal_tool' => [
|
|
'id' => 'translater_proposal_tool',
|
|
'value' => 'google',
|
|
'type' => 'string',
|
|
'label' => 'Id of default tool for proposed translation'
|
|
],
|
|
'proposal_lang' => [
|
|
'id' => 'translater_proposal_lang',
|
|
'value' => 'en',
|
|
'type' => 'string',
|
|
'label' => 'Default source language for proposed translation'
|
|
]
|
|
];
|
|
|
|
/** @var array $default_dotclear_modules List of distributed plugins and themes */
|
|
public static $default_dotclear_modules = ['plugin' => [], 'theme' => []];
|
|
|
|
/** @var array $modules List of modules we could work on */
|
|
private $modules = [];
|
|
|
|
/** @var array $module Module to work on */
|
|
private $module = [];
|
|
|
|
/**
|
|
* Main translater object
|
|
*
|
|
* @param dcCore $core dcCore instance
|
|
*/
|
|
public function __construct(dcCore $core)
|
|
{
|
|
self::$default_dotclear_modules = [
|
|
'plugin' => explode(',', DC_DISTRIB_PLUGINS),
|
|
'theme' => explode(',', DC_DISTRIB_THEMES)
|
|
];
|
|
$this->core = $core;
|
|
$core->blog->settings->addNamespace('translater');
|
|
$this->loadModules();
|
|
$this->proposal = new translaterProposals($core);
|
|
}
|
|
|
|
/// @name settings methods
|
|
//@{
|
|
/**
|
|
* Get array of default settings
|
|
*
|
|
* @return array All default settings
|
|
*/
|
|
public function getDefaultSettings(): array
|
|
{
|
|
return $this->default_settings;
|
|
}
|
|
|
|
/**
|
|
* Get a setting according to default settings list
|
|
*
|
|
* @param string $id The settings short id
|
|
* @return mixed The setting value if exists or null
|
|
*/
|
|
public function getSetting(string $id)
|
|
{
|
|
if (!isset($this->default_settings[$id])) {
|
|
return null;
|
|
}
|
|
return $this->core->blog->settings->translater->get(
|
|
$this->default_settings[$id]['id']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Magic getSetting
|
|
*/
|
|
public function __get($id)
|
|
{
|
|
return $this->getSetting($id);
|
|
}
|
|
|
|
/**
|
|
* Set a setting according to default settings list
|
|
*
|
|
* @param string $k The setting short id
|
|
* @param mixed $v The setting value
|
|
*/
|
|
public function setSetting(string $k, $v)
|
|
{
|
|
if (!isset($this->default_settings[$k])) {
|
|
return false;
|
|
}
|
|
|
|
$this->core->blog->settings->translater->drop($id);
|
|
$this->core->blog->settings->translater->put(
|
|
$this->default_settings[$k]['id'],
|
|
$v,
|
|
$this->default_settings[$k]['type'],
|
|
$this->default_settings[$k]['label'],
|
|
true,
|
|
true
|
|
);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Magis setSetting
|
|
*/
|
|
public function __set($k, $v)
|
|
{
|
|
return $this->setSetting($k, $v);
|
|
}
|
|
//@}
|
|
|
|
/// @name modules methods
|
|
//@{
|
|
/**
|
|
* Retrieve a particular info for a given module
|
|
*
|
|
* @param string $id The module id
|
|
* @param string $info The module info
|
|
* @return mixed The module info value or null
|
|
*/
|
|
public function moduleInfo(string $id, string $info)
|
|
{
|
|
if (isset($this->modules['plugin'][$id])) {
|
|
$type = 'plugin';
|
|
} elseif (isset($this->modules['theme'][$id])) {
|
|
$type = 'theme';
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
if ($info == 'type') {
|
|
return $type;
|
|
}
|
|
|
|
return isset($this->modules[$type][$id][$info]) ? $this->modules[$type][$id][$info] : null;
|
|
}
|
|
|
|
/**
|
|
* Load array of modules infos by type of modules
|
|
*/
|
|
private function loadModules()
|
|
{
|
|
$themes = new dcThemes($this->core);
|
|
$themes->loadModules($this->core->blog->themes_path, null);
|
|
$this->modules['theme'] = $this->modules['plugin'] = [];
|
|
|
|
$m = $themes->getModules();
|
|
foreach($m AS $k => $v) {
|
|
if (!$v['root_writable']) {
|
|
continue;
|
|
}
|
|
$this->modules['theme'][$k] = $v;
|
|
$this->modules['theme'][$k]['id'] = $k;
|
|
$this->modules['theme'][$k]['type'] = 'theme';
|
|
}
|
|
|
|
$m = $this->core->plugins->getModules();
|
|
foreach($m AS $k => $v) {
|
|
if (!$v['root_writable']) {
|
|
continue;
|
|
}
|
|
$this->modules['plugin'][$k] = $v;
|
|
$this->modules['plugin'][$k]['id'] = $k;
|
|
$this->modules['plugin'][$k]['type'] = 'plugin';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return array of modules infos by type of modules
|
|
*
|
|
* @param string $type The module type
|
|
* @return array The list of modules infos
|
|
*/
|
|
public function listModules(string $type = '')
|
|
{
|
|
return in_array($type, ['plugin', 'theme']) ?
|
|
$this->modules[$type] :
|
|
array_merge($this->modules['theme'], $this->modules['plugin']);
|
|
}
|
|
|
|
/**
|
|
* Return array object of a particular module for a given type of module
|
|
*
|
|
* @param string $id The module id
|
|
* @param string $type The module type
|
|
* @return ArrayObject The module info
|
|
*/
|
|
public function getModule(string $id = '', string $type = '')
|
|
{
|
|
$o = new ArrayObject();
|
|
|
|
# Load nothing?
|
|
if (empty($id)) {
|
|
return false;
|
|
}
|
|
|
|
# Unknow type?
|
|
$modules = !in_array($type, ['plugin', 'theme']) ?
|
|
array_merge($this->modules['theme'], $this->modules['plugin']) :
|
|
$this->modules[$type];
|
|
|
|
# Unknow module?
|
|
if (!isset($modules[$id])) {
|
|
throw new Exception(
|
|
sprintf(__('Cannot find module %s'), $id)
|
|
);
|
|
return false;
|
|
}
|
|
|
|
# Module info
|
|
foreach($modules[$id] as $a => $b) {
|
|
$o->{$a} = $b;
|
|
}
|
|
$o->root = path::real($o->root);
|
|
# Locales path
|
|
$o->locales = $o->root . '/locales';
|
|
# Module exists
|
|
$o->exists = true;
|
|
# Module Basename
|
|
$i = path::info($o->root);
|
|
$o->basename = $i['basename'];
|
|
|
|
return $o;
|
|
}
|
|
//@}
|
|
|
|
/**
|
|
* Find backup folder of a module
|
|
*
|
|
* @param string $id The module id
|
|
* @param boolean $throw Silently failed
|
|
* @return mixed The backup folder directory or false
|
|
*/
|
|
public function getBackupFolder(string $id, bool $throw = false)
|
|
{
|
|
$dir = false;
|
|
switch($this->backup_folder) {
|
|
case 'module':
|
|
# plugin
|
|
if (isset($this->modules['plugin'][$id])
|
|
&& $this->modules['plugin'][$id]['root_writable']
|
|
) {
|
|
$dir = path::real($this->modules['plugin'][$id]['root']) . '/locales';
|
|
#theme
|
|
} elseif (isset($this->modules['theme'][$id])
|
|
&& $this->modules['theme'][$id]['root_writable']
|
|
) {
|
|
$dir = path::real($this->modules['theme'][$id]['root']) . '/locales';
|
|
}
|
|
break;
|
|
|
|
case 'plugin':
|
|
$tmp = path::real(array_pop(explode(PATH_SEPARATOR, DC_PLUGINS_ROOT)));
|
|
if ($tmp && is_writable($tmp)) {
|
|
$dir = $tmp;
|
|
}
|
|
break;
|
|
|
|
case 'public':
|
|
$tmp = path::real($this->core->blog->public_path);
|
|
if ($tmp && is_writable($tmp)) {
|
|
$dir = $tmp;
|
|
}
|
|
break;
|
|
|
|
case 'cache':
|
|
$tmp = path::real(DC_TPL_CACHE);
|
|
if ($tmp && is_writable($tmp)) {
|
|
@mkDir($tmp . '/l10n');
|
|
$dir = $tmp . '/l10n';
|
|
}
|
|
break;
|
|
|
|
case 'translater':
|
|
$tmp = path::real($this->modules['plugin']['translater']['root']);
|
|
if ($tmp && is_writable($tmp)) {
|
|
@mkDir($tmp . '/locales');
|
|
$dir = $tmp . '/locales';
|
|
}
|
|
break;
|
|
}
|
|
if (!$dir && $throw) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot find backups folder for module %s'), $id
|
|
));
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
/**
|
|
* Find language path of a module
|
|
*
|
|
* @param string $id The module id
|
|
* @param boolean $throw Silently failed
|
|
* @return mixed The module language path or false
|
|
*/
|
|
public function getLangsFolder(string $id = '', bool $throw = false)
|
|
{
|
|
$dir = $id == 'dotclear' ?
|
|
DC_ROOT :
|
|
self::getModuleFolder($id, false);
|
|
|
|
if (!$dir && $throw) {
|
|
throw new Exception(
|
|
sprintf(__('Cannot find languages folder for module %s'), $id)
|
|
);
|
|
}
|
|
|
|
return !$dir ? false : $dir . '/locales';
|
|
}
|
|
|
|
/**
|
|
* Find root path of a module
|
|
*
|
|
* @param string $module The module id
|
|
* @param boolean $throw Silently failed
|
|
* @return mixed The module root path or false
|
|
*/
|
|
public function getModuleFolder($id = '', $throw = false)
|
|
{
|
|
$dir = false;
|
|
if ((isset($this->modules['plugin'][$id]['root'])
|
|
&& ($tmp = path::real($this->modules['plugin'][$id]['root'])))
|
|
|| (isset($this->modules['theme'][$id]['root'])
|
|
&& ($tmp = path::real($this->modules['theme'][$id]['root']))
|
|
)) {
|
|
$dir = $tmp;
|
|
}
|
|
if (!$dir && $throw) {
|
|
throw new Exception(
|
|
sprintf(__('Cannot find root folder for module %s'), $id)
|
|
);
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
/**
|
|
* Check limit number of backup for a module
|
|
*
|
|
* @param string $id The module id
|
|
* @param boolean $throw Silently failed
|
|
* @return boolean True if limit is riched
|
|
*/
|
|
public function isBackupLimit(string $id, bool $throw = false): bool
|
|
{
|
|
# Find folder of backups
|
|
$backup = self::getBackupFolder($id, true);
|
|
|
|
# Count backup files
|
|
$count = 0;
|
|
foreach(self::scandir($backup) AS $file) {
|
|
if (!is_dir($backup . '/' . $file)
|
|
&& preg_match('/^(l10n-'. $id . '(.*?).bck.zip)$/', $backup)
|
|
) {
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
# Limite exceed
|
|
if ($count >= $this->backup_limit) {
|
|
if ($throw) {
|
|
throw new Exception(
|
|
sprintf(__('Limit of %s backups for module %s exceed'), $this->backup_limit, $id)
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get a list of available backups
|
|
*
|
|
* @param string $id The module id
|
|
* @param bool|boolean $return_filename Return only filenames
|
|
* @return array The module backups info
|
|
*/
|
|
public function listBackups(string $id, bool $return_filename = false): array
|
|
{
|
|
# Not a module installed
|
|
self::getLangsFolder($id, true);
|
|
|
|
# No backup folder for this module
|
|
$backup = self::getBackupFolder($id, false);
|
|
if (!$backup) {
|
|
return [];
|
|
}
|
|
|
|
# Scan files for backups
|
|
$res = $sort = [];
|
|
$files = self::scandir($backup);
|
|
foreach($files AS $file) {
|
|
# Not a bakcup file
|
|
$is_backup = preg_match(
|
|
'/^(l10n-(' . $id . ')-(.*?)-([0-9]*?).bck.zip)$/', $file, $m
|
|
);
|
|
|
|
if (is_dir($backup . '/' . $file)
|
|
|| !$is_backup
|
|
|| !self::isIsoCode($m[3])
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
# Backup infos
|
|
if ($return_filename) {
|
|
$res[] = $file;
|
|
} else {
|
|
$res[$m[3]][$file] = path::info($backup . '/' . $file);
|
|
$res[$m[3]][$file]['time']= filemtime($backup . '/' . $file);
|
|
$res[$m[3]][$file]['size'] = filesize($backup . '/' . $file);
|
|
$res[$m[3]][$file]['module'] = $id;
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Create a backup
|
|
*
|
|
* @param string $id The module id
|
|
* @param string $lang The backup lang
|
|
* @return boolean True on success
|
|
*/
|
|
public function createBackup(string $id, string $lang): bool
|
|
{
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($id, true);
|
|
|
|
# No backup folder for this module
|
|
$backup = self::getBackupFolder($id, true);
|
|
|
|
# Not an existing lang
|
|
if (!is_dir($locales . '/' . $lang)) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot find language folder %s for module %s') ,$lang, $id
|
|
));
|
|
}
|
|
|
|
# Scan files for a lang
|
|
$res = [];
|
|
$files = self::scandir($locales . '/' . $lang);
|
|
foreach($files as $file) {
|
|
# Only lang file
|
|
if (!is_dir($locales . '/' . $lang . '/' . $file)
|
|
&& (self::isLangphpFile($file)
|
|
|| self::isPoFile($file))) {
|
|
$res[$locales . '/' . $lang . '/' . $file] =
|
|
$id . '/locales/' . $lang . '/' . $file;
|
|
}
|
|
}
|
|
|
|
# Do Zip
|
|
if (!empty($res)) {
|
|
self::isBackupLimit($id, true);
|
|
|
|
@set_time_limit(300);
|
|
$fp = fopen($backup . '/l10n-' . $id . '-' . $lang . '-' . time() . '.bck.zip', 'wb');
|
|
$zip = new fileZip($fp);
|
|
foreach($res AS $src => $dest) {
|
|
$zip->addFile($src, $dest);
|
|
}
|
|
$zip->write();
|
|
$zip->close();
|
|
unset($zip);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a module backup
|
|
*
|
|
* @param string $id The module id
|
|
* @param string $file The backup filename
|
|
* @return boolean True on success
|
|
*/
|
|
public function deleteBackup(string $id, string $file): bool
|
|
{
|
|
# Not a module installed
|
|
self::getLangsFolder($id, true);
|
|
|
|
# No backup folder for this module
|
|
$backup = self::getBackupFolder($id, true);
|
|
|
|
# Not a bakcup file
|
|
$is_backup = preg_match('/^(l10n-(' . $id . ')-(.*?)-([0-9]*?).bck.zip)$/', $file, $m);
|
|
|
|
if (is_dir($backup . '/' . $file)
|
|
|| !$is_backup
|
|
|| !self::isIsoCode($m[3])
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (!files::isDeletable($backup . '/' . $file)) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot delete backup file %s'), $file
|
|
));
|
|
}
|
|
|
|
unlink($backup . '/' . $file);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Retore a backup
|
|
*
|
|
* @param string $module The module id
|
|
* @param string $file The backup filename
|
|
* @return boolean True on success
|
|
*/
|
|
public function restoreBackup(string $id, string $file): bool
|
|
{
|
|
# Not a module installed
|
|
$locales = self::getModuleFolder($id, true);
|
|
|
|
# No backup folder for this module
|
|
$backup = self::getBackupFolder($id, true);
|
|
|
|
if (!file_exists($backup . '/' . $file)) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot find backup file %s'), $file
|
|
));
|
|
}
|
|
|
|
$zip = new fileUnzip($backup . '/' . $file);
|
|
$zip_files = $zip->getFilesList();
|
|
|
|
foreach($zip_files AS $zip_file) {
|
|
$f = self::explodeZipFilename($zip_file, true);
|
|
if ($id != $f['module']) {
|
|
continue;
|
|
}
|
|
|
|
$zip->unzip($zip_file, $locales . '/locales/' . $f['lang'] . '/' . $f['group'] . $f['ext']);
|
|
$done = true;
|
|
}
|
|
$zip->close();
|
|
unset($zip);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Export (to output) language pack
|
|
*
|
|
* @param array $modules The modules to work on
|
|
* @param array $langs Langs to export
|
|
*/
|
|
public function exportPack(array $modules, array $langs)
|
|
{
|
|
# Not a query well formed
|
|
if (!is_array($modules) || 1 > count($modules)
|
|
|| !is_array($langs) || 1 > count($langs)
|
|
) {
|
|
throw new Exception(
|
|
__('Wrong export query')
|
|
);
|
|
}
|
|
|
|
# Filter default filename
|
|
$filename = files::tidyFileName($this->export_filename);
|
|
|
|
# Not a filename good formed
|
|
if (empty($filename)) {
|
|
throw new Exception(
|
|
sprintf(__('Cannot use export mask %s'), $this->export_filename)
|
|
);
|
|
}
|
|
|
|
# Modules folders
|
|
$res = $count = [];
|
|
foreach($modules AS $module) {
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($module, false);
|
|
if (!$locales) {
|
|
continue;
|
|
}
|
|
|
|
# Langs folders
|
|
foreach($langs AS $lang) {
|
|
# Not a lang folder
|
|
if (!is_dir($locales . '/' . $lang)) {
|
|
continue;
|
|
}
|
|
|
|
# Scan files for a lang
|
|
$files = self::scandir($locales . '/' . $lang);
|
|
foreach($files as $file) {
|
|
# Not a lang file
|
|
if (is_dir($locales . '/' . $lang . '/' . $file)
|
|
|| !self::isLangphpFile($file)
|
|
&& !self::isPoFile($file)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
# Add file to zip in format "module/locales/lang/filename"
|
|
$res[$locales . '/' . $lang . '/' . $file] =
|
|
$module . '/locales/' . $lang . '/' . $file;
|
|
|
|
$count[$module] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Nothing to do
|
|
if (empty($res)) {
|
|
throw new Exception('Nothing to export');
|
|
}
|
|
|
|
# Prepare files to zip
|
|
@set_time_limit(300);
|
|
$fp = fopen('php://output', 'wb');
|
|
$zip = new fileZip($fp);
|
|
foreach($res as $from => $to) {
|
|
$zip->addFile($from,$to);
|
|
}
|
|
|
|
# Set filename
|
|
$file_infos = 1 < count($count) ?
|
|
[time(), 'modules', 'multi', self::$dcTranslaterVersion] :
|
|
[
|
|
time(),
|
|
$modules[0],
|
|
self::moduleInfo($modules[0], 'type'),
|
|
self::moduleInfo($modules[0], 'version')
|
|
];
|
|
$filename =
|
|
files::tidyFileName(
|
|
dt::str(
|
|
str_replace(
|
|
['timestamp', 'module', 'type', 'version'],
|
|
$file_infos,
|
|
$this->export_filename
|
|
)
|
|
)
|
|
);
|
|
|
|
# Send Zip
|
|
header('Content-Disposition: attachment;filename=' . $filename . '.zip');
|
|
header('Content-Type: application/x-zip');
|
|
$zip->write();
|
|
unset($zip);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Import a language pack
|
|
*
|
|
* @param array $modules The modules to work on
|
|
* @param array $zip_file The uploaded file info
|
|
* @return boolean True on success
|
|
*/
|
|
public function importPack($modules, $zip_file)
|
|
{
|
|
# Not a file uploaded
|
|
files::uploadStatus($zip_file);
|
|
|
|
# No modules to update
|
|
if (!is_array($modules) || 1 > count($modules)) {
|
|
throw new Exception(__('Wrong import query'));
|
|
}
|
|
|
|
$done = false;
|
|
$res = [];
|
|
|
|
# Load Unzip object
|
|
$zip = new fileUnzip($zip_file['tmp_name']);
|
|
$files = $zip->getFilesList();
|
|
|
|
# Scan zip
|
|
foreach($files AS $file) {
|
|
$f = self::explodeZipFilename($file, false);
|
|
|
|
# Not a requested module
|
|
if (!is_array($f)
|
|
|| !in_array($f['module'],$modules)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
# Get locales folder (even if "locales" is not set)
|
|
if (!$dir = self::getModuleFolder($f['module'], false)) {
|
|
continue;
|
|
}
|
|
$locales = $dir . '/locales';
|
|
|
|
# Not allow overwrite
|
|
if (!$this->import_overwrite
|
|
&& file_exists($locales . '/' . $f['lang'] . '/' . $f['group'] . $f['ext'])
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$res[] = [
|
|
'from' => $file,
|
|
'root' => $locales . '/' . $f['lang'],
|
|
'to' => $locales . '/' . $f['lang'] . '/' . $f['group'] . $f['ext']
|
|
];
|
|
}
|
|
# Unzip files
|
|
foreach ($res AS $rs) {
|
|
if (!is_dir($rs['root'])) {
|
|
files::makeDir($rs['root'], true);
|
|
}
|
|
|
|
$zip->unzip($rs['from'], $rs['to']);
|
|
$done = true;
|
|
}
|
|
$zip->close();
|
|
unlink($zip_file['tmp_name']);
|
|
|
|
# No file unzip
|
|
if (!$done) {
|
|
throw new Exception(
|
|
sprintf(__('Nothing to import for these modules in pack %s'), $zip_file['name'])
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse zip filename to module, lang info
|
|
*
|
|
* @param string $file The zip filename
|
|
* @param boolean $throw Silently failed
|
|
* @return mixed Array of file info or false
|
|
*/
|
|
public function explodeZipFilename(string $file = '', bool $throw = false)
|
|
{
|
|
# module/locales/lang/group.ext
|
|
$is_file = preg_match(
|
|
'/^(.*?)\/locales\/(.*?)\/(.*?)(.po|.lang.php)$/', $file, $f
|
|
);
|
|
|
|
# Explode file to infos
|
|
if ($is_file) {
|
|
$module = null !== self::moduleInfo($f[1], 'name') ?
|
|
$f[1] : false;
|
|
$lang = self::isIsoCode($f[2]) ?
|
|
$f[2] : false;
|
|
$group = in_array($f[3], self::$allowed_l10n_groups) ?
|
|
$f[3] : false;
|
|
$ext = self::isLangphpFile($f[4]) || self::isPoFile($f[4]) ?
|
|
$f[4] : false;
|
|
}
|
|
# Not good formed
|
|
if (!$is_file || !$module || !$lang || !$group || !$ext) {
|
|
if ($throw) {
|
|
throw new Exception(
|
|
sprintf(__('Zip file %s is not in translater format'), $file)
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
return [
|
|
'module' => $module,
|
|
'lang' => $lang,
|
|
'group' => $group,
|
|
'ext' => $ext
|
|
];
|
|
}
|
|
|
|
/**
|
|
* List available langs of a module
|
|
*
|
|
* @param string $id The module id
|
|
* @param boolean $return_path Return path or name
|
|
* @return array The lang list
|
|
*/
|
|
public function listLangs(string $id, bool $return_path = false)
|
|
{
|
|
$res = [];
|
|
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($id, true);
|
|
|
|
# Add prefix "locales" as scandir remove it
|
|
$prefix = preg_match('/(locales(.*))$/', $locales) ? 'locales' : '';
|
|
|
|
# Retrieve langs folders
|
|
$files = self::scandir($locales);
|
|
foreach($files as $file) {
|
|
if (!preg_match(
|
|
'/(.*?(locales\/)([^\/]*?)\/([^\/]*?)(.lang.php|.po))$/',
|
|
$prefix . $file, $m
|
|
)) {
|
|
continue;
|
|
}
|
|
|
|
if (!self::isIsoCode($m[3])) {
|
|
continue;
|
|
}
|
|
|
|
if ($return_path) {
|
|
$res[$m[3]][] = $file; # Path
|
|
} else {
|
|
$res[$m[3]] = self::$iso[$m[3]]; # Lang name
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Add a lang to a module
|
|
*
|
|
* @param string $module The module id
|
|
* @param string $lang The lang id
|
|
* @param string $from_lang The lang to copy from
|
|
*/
|
|
public function addLang(string $id, string $lang, string $from_lang = '')
|
|
{
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($id, true);
|
|
|
|
# Path is right formed
|
|
self::isIsoCode($lang, true);
|
|
|
|
# Retrieve langs folders
|
|
$langs = self::listLangs($id);
|
|
|
|
# Lang folder is not present
|
|
if (isset($langs[$lang])) {
|
|
throw new Exception(
|
|
sprintf(__('Language %s already exists for module %s'), $lang, $id)
|
|
);
|
|
}
|
|
|
|
# Create new lang directory
|
|
files::makeDir($locales . '/' . $lang, true);
|
|
|
|
# Verify folder of other lang
|
|
if (!empty($from_lang) && !isset($langs[$from_lang])) {
|
|
throw new Exception(
|
|
sprintf(__('Cannot copy file from language %s for module %s'), $from_lang, $id)
|
|
);
|
|
}
|
|
|
|
# Copy files from other lang
|
|
if (!empty($from_lang)
|
|
&& isset($langs[$from_lang])
|
|
) {
|
|
$files = self::scandir($locales . '/' . $from_lang);
|
|
foreach($files as $file) {
|
|
if (is_dir($locales . '/' . $from_lang . '/' . $file)
|
|
|| !self::isLangphpFile($file)
|
|
&& !self::isPoFile($file)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
files::putContent($locales . '/' . $lang . '/' . $file,
|
|
file_get_contents($locales . '/' . $from_lang . '/' . $file)
|
|
);
|
|
}
|
|
} else {
|
|
# Create basic empty lang file as translater need these files to be present
|
|
self::setLangphpFile($id, $lang, 'main', []);
|
|
self::setPoFile($id, $lang, 'main', []);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an existing lang
|
|
*
|
|
* @param string $module The module id
|
|
* @param string $lang The lang
|
|
* @param array $msgs The messages
|
|
*/
|
|
public function updLang(string $id, string $lang, array $msgs)
|
|
{
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($id, true);
|
|
|
|
# Path is right formed
|
|
self::isIsoCode($lang, true);
|
|
|
|
# Retrieve langs folders
|
|
$langs = self::listLangs($id);
|
|
|
|
# Lang folder is not present
|
|
if (!isset($langs[$lang])) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot find language folder %s for module %s'), $lang, $id
|
|
));
|
|
}
|
|
|
|
# Sort msgids by groups
|
|
$rs = [];
|
|
foreach($msgs as $msg) {
|
|
$msg['group'] = isset($msg['group']) ? $msg['group'] : '';
|
|
$msg['msgid'] = isset($msg['msgid']) ? $msg['msgid'] : '';
|
|
$msg['msgstr'] = isset($msg['msgstr']) ? trim($msg['msgstr']) : '';
|
|
/*
|
|
if (get_magic_quotes_gpc()) {
|
|
$msg['msgid'] = stripcslashes($msg['msgid']);
|
|
$msg['msgstr'] = stripcslashes($msg['msgstr']);
|
|
}
|
|
*/ if ($msg['msgstr'] == '') {
|
|
continue;
|
|
}
|
|
|
|
$rs[$msg['group']][$msg['msgid']] = $msg['msgstr'];
|
|
}
|
|
|
|
# Backup files if auto-backup is on
|
|
if ($this->backup_auto) {
|
|
self::createBackup($id, $lang);
|
|
}
|
|
|
|
# Delete empty files (files with no group)
|
|
foreach(self::$allowed_l10n_groups AS $group) {
|
|
if (isset($rs[$group])) {
|
|
continue;
|
|
}
|
|
|
|
$po_file = $locales . '/' . $lang . '/' . $group . '.po';
|
|
$langphp_file = $locales . '/' . $lang . '/' . $group . '.lang.php';
|
|
|
|
if (file_exists($po_file)) {
|
|
unlink($po_file);
|
|
}
|
|
if (file_exists($langphp_file)) {
|
|
unlink($langphp_file);
|
|
}
|
|
}
|
|
|
|
# No msgstr to write
|
|
if (empty($rs)) {
|
|
throw new Exception(sprintf(
|
|
__('No string to write, language %s deleted for module %s'),
|
|
$lang, $id
|
|
));
|
|
}
|
|
|
|
# Write .po and .lang.php files
|
|
foreach($rs AS $group => $msgs) {
|
|
self::setLangphpFile($id, $lang, $group, $msgs);
|
|
self::setPoFile($id, $lang, $group, $msgs);
|
|
}
|
|
}
|
|
|
|
public function delLang($module, $lang, $del_empty_dir = true)
|
|
{
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($module, true);
|
|
|
|
# Path is right formed
|
|
self::isIsoCode($lang, true);
|
|
|
|
# Retrieve langs folders
|
|
$files = self::listLangs($module, true);
|
|
|
|
# Lang folder is not present
|
|
if (!isset($files[$lang])) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot find language folder %s for module %s'), $lang, $module
|
|
));
|
|
}
|
|
|
|
# Delete .po and .lang.php files
|
|
foreach($files[$lang] as $file) {
|
|
unlink($locales . '/' . $file);
|
|
}
|
|
|
|
# Delete lang folder if empty
|
|
$dir = self::scandir($locales . '/' . $lang);
|
|
if (empty($dir)) {
|
|
rmdir($locales . '/' . $lang);
|
|
}
|
|
|
|
# Delete locales folder if empty
|
|
$loc = self::scandir($locales);
|
|
if (empty($loc)) {
|
|
rmdir($locales);
|
|
}
|
|
}
|
|
|
|
public static function encodeMsg($str)
|
|
{
|
|
return text::toUTF8(stripslashes(trim($str)));
|
|
}
|
|
|
|
/* Scan a module folder to find all l10n strings in .php files */
|
|
public function getMsgIds($module)
|
|
{
|
|
$res = array();
|
|
|
|
# Not a module installed
|
|
$dir = self::getModuleFolder($module, true);
|
|
|
|
$files = self::scandir($dir);
|
|
|
|
$scan_ext = array('php');
|
|
if ($this->scan_tpl) {
|
|
$scan_ext[] = 'html';
|
|
}
|
|
|
|
foreach($files AS $file) {
|
|
if (is_dir($dir . '/' . $file)
|
|
|| !in_array(files::getExtension($file),$scan_ext)) {
|
|
continue;
|
|
}
|
|
|
|
$contents = file($dir . '/' . $file);
|
|
foreach($contents AS $line => $content) {
|
|
# php files
|
|
//if (preg_match_all("|__\((['\"]{1})(.*)([\"']{1})\)|U", $content, $matches))
|
|
if (preg_match_all("|__\((['\"]{1})(.*?)([\"']{1})\)|", $content, $matches)) {
|
|
foreach($matches[2] as $id) {
|
|
$res[] = array(
|
|
'msgid' => self::encodeMsg($id),
|
|
'file' => $file,
|
|
'line' => $line + 1
|
|
);
|
|
}
|
|
}
|
|
# tpl files
|
|
if ($this->scan_tpl
|
|
&& preg_match_all('/\{\{tpl:lang\s([^}]+)\}\}/', $content, $matches)) {
|
|
foreach($matches[1] as $id) {
|
|
$res[] = array(
|
|
'msgid' => self::encodeMsg($id),
|
|
'file' => $file,
|
|
'line' => $line + 1
|
|
);
|
|
}
|
|
}
|
|
}
|
|
unset($contents);
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/* Scan a lang folder to find l10n translations in files */
|
|
public function getMsgStrs($module, $requested_lang = '')
|
|
{
|
|
$res = array();
|
|
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($module, true);
|
|
|
|
$langs = self::listLangs($module, true);
|
|
|
|
# Not an existing lang
|
|
if (!isset($langs[$requested_lang])) {
|
|
return $res;
|
|
}
|
|
|
|
# Lang files
|
|
$exists = array();
|
|
foreach($langs[$requested_lang] as $file) {
|
|
if (in_array($file,$exists)) {
|
|
continue;
|
|
}
|
|
$exists[] = $file;
|
|
$path = path::clean($locales . '/' . $file);
|
|
|
|
# .po files
|
|
if (self::isPoFile($file)) {
|
|
$po = self::getPoFile($path);
|
|
if (!is_array($po)) {
|
|
continue;
|
|
}
|
|
|
|
foreach($po as $id => $str) {
|
|
$is_po[$requested_lang][$id] = 1;
|
|
|
|
$res[] = array(
|
|
'msgid' => self::encodeMsg($id),
|
|
'msgstr' => self::encodeMsg($str),
|
|
'lang' => $requested_lang,
|
|
'type' => 'po',
|
|
'path' => $path,
|
|
'file' => basename($file),
|
|
'group'=> str_replace('.po', '', basename($file))
|
|
);
|
|
}
|
|
# .lang.php files
|
|
} elseif (self::isLangphpFile($file)) {
|
|
$php = self::getLangphpFile($path);
|
|
foreach($php AS $id => $str) {
|
|
# Don't overwrite .po
|
|
if (isset($is_po[$requested_lang][$id])) {
|
|
continue;
|
|
}
|
|
$res[] = array(
|
|
'msgid' => self::encodeMsg($id),
|
|
'msgstr' => self::encodeMsg($str),
|
|
'lang' => $requested_lang,
|
|
'type' => 'php',
|
|
'path' => $path,
|
|
'file' => basename($file),
|
|
'group'=> str_replace('.lang.php', '', basename($file))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
public function getMsgs($module, $requested_lang = '')
|
|
{
|
|
# Get messages ids of a module
|
|
$m_msgids = self::getMsgIds($module);
|
|
|
|
# Get messages translations for a module
|
|
$m_msgstrs = self::getMsgStrs($module, $requested_lang);
|
|
|
|
# Get messages translations for others modules
|
|
foreach(self::listModules() AS $o_module => $o_infos) {
|
|
if ($o_module == $module) continue;
|
|
$m_o_msgstrs[$o_module] = self::getMsgStrs($o_module, $requested_lang);
|
|
}
|
|
$m_o_msgstrs['dotclear'] = self::getMsgStrs('dotclear', $requested_lang);
|
|
|
|
# Only one lang or all
|
|
$langs = '' == $requested_lang ?
|
|
self::listLangs($module) :
|
|
array($requested_lang => self::isIsoCode($requested_lang));
|
|
|
|
# Let's go reorder the mixture
|
|
$res = array();
|
|
foreach($langs AS $lang => $iso) {
|
|
$res[$lang] = array();
|
|
|
|
# From id list
|
|
foreach($m_msgids AS $rs) {
|
|
$res[$lang][$rs['msgid']]['files'][] = array(trim($rs['file'],'/'), $rs['line']);
|
|
$res[$lang][$rs['msgid']]['group'] = 'main';
|
|
$res[$lang][$rs['msgid']]['msgstr'] = '';
|
|
$res[$lang][$rs['msgid']]['in_dc'] = false;
|
|
$res[$lang][$rs['msgid']]['o_msgstrs'] = array();
|
|
}
|
|
|
|
# From str list
|
|
foreach($m_msgstrs AS $rs) {
|
|
if ($rs['lang'] != $lang) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($res[$lang][$rs['msgid']])) {
|
|
$res[$lang][$rs['msgid']]['files'][] = array();
|
|
$res[$lang][$rs['msgid']]['in_dc'] = false;
|
|
$res[$lang][$rs['msgid']]['o_msgstrs'] = array();
|
|
}
|
|
$res[$lang][$rs['msgid']]['group'] = $rs['group'];
|
|
$res[$lang][$rs['msgid']]['msgstr'] = $rs['msgstr'];
|
|
$res[$lang][$rs['msgid']]['in_dc'] = false;
|
|
}
|
|
|
|
# From others str list
|
|
foreach($m_o_msgstrs AS $o_module => $o_msgstrs) {
|
|
foreach($o_msgstrs AS $rs) {
|
|
if ($rs['lang'] != $lang) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($res[$lang][$rs['msgid']])) {
|
|
continue;
|
|
}
|
|
|
|
$res[$lang][$rs['msgid']]['o_msgstrs'][] = array(
|
|
'msgstr' => $rs['msgstr'],
|
|
'module' => $o_module,
|
|
'file' => $rs['file']
|
|
);
|
|
if ($o_module == 'dotclear') {
|
|
$res[$lang][$rs['msgid']]['in_dc'] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return '' == $requested_lang ? $res : $res[$requested_lang];
|
|
}
|
|
|
|
/* Write a lang file */
|
|
private function writeLangFile($dir, $content, $throw)
|
|
{
|
|
$path = path::info($dir);
|
|
if (is_dir($path['dirname']) && !is_writable($path['dirname'])
|
|
|| file_exists($dir) && !is_writable($dir)) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot grant write acces on lang file %s'), $dir
|
|
));
|
|
}
|
|
|
|
# -- BEHAVIOR -- dcTranslaterBeforeWriteLangFile
|
|
$this->core->callBehavior('dcTranslaterBeforeWriteLangFile', $dir, $content, $throw);
|
|
|
|
$f = @files::putContent($dir,$content);
|
|
if (!$f && $throw) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot write lang file %s'), $dir
|
|
));
|
|
}
|
|
|
|
# -- BEHAVIOR -- dcTranslaterAfterWriteLangFile
|
|
$this->core->callBehavior('dcTranslaterAfterWriteLangFile', $f, $dir, $content, $throw);
|
|
|
|
return $f;
|
|
}
|
|
|
|
/* Try if a file is a .lang.php file */
|
|
public static function isLangphpFile($file)
|
|
{
|
|
return files::getExtension($file) == 'php' && stristr($file, '.lang.php');
|
|
}
|
|
|
|
/* Get and parse a .lang.php file */
|
|
public static function getLangphpFile($file)
|
|
{
|
|
if (!file_exists($file)) {
|
|
return array();
|
|
}
|
|
|
|
$res = array();
|
|
$content = implode('',file($file));
|
|
$count = preg_match_all('/(\$GLOBALS\[\'__l10n\'\]\[\'(.*?)\'\]\s*\x3D\s*\'(.*?)\';)/', $content, $m);
|
|
|
|
if (!$count) {
|
|
return array();
|
|
}
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$id = $m[2][$i];
|
|
$str = self::langphpString($m[3][$i]);
|
|
|
|
if ($str) {
|
|
$res[self::langphpString($id)] = $str;
|
|
}
|
|
}
|
|
if (!empty($res[''])) {
|
|
$res = array_diff_key($res,array(''=>1));
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/* Construct and write a .lang.php file */
|
|
private function setLangphpFile($module, $lang, $group, $fields)
|
|
{
|
|
if (!$this->write_langphp) {
|
|
return;
|
|
}
|
|
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($module, true);
|
|
|
|
# Path is right formed
|
|
$lang_name = self::isIsoCode($lang, true);
|
|
|
|
$l = "<?php\n";
|
|
if ($this->parse_comment) {
|
|
$l .=
|
|
'// Language: ' . $lang_name . " \n" .
|
|
'// Module: ' . $module . " - " . self::moduleInfo($module, 'version') . "\n" .
|
|
'// Date: ' . dt::str('%Y-%m-%d %H:%M:%S') . " \n";
|
|
|
|
if ($this->parse_user && !empty($this->parse_userinfo)) {
|
|
$search = self::$allowed_user_informations;
|
|
foreach($search AS $n) {
|
|
$replace[] = $this->core->auth->getInfo('user_' . $n);
|
|
}
|
|
$info = trim(str_replace($search,$replace,$this->parse_userinfo));
|
|
if (!empty($info)) {
|
|
$l .= '// Author: ' . html::escapeHTML($info) . "\n";
|
|
}
|
|
}
|
|
$l .=
|
|
'// Translated with dcTranslater - ' . $this->core->plugins->moduleInfo('translater', 'version') . " \n\n";
|
|
}
|
|
if ($this->parse_comment) {
|
|
$infos = self::getMsgids($module);
|
|
foreach($infos AS $info) {
|
|
if (isset($fields[$info['msgid']])) {
|
|
$comments[$info['msgid']] = (isset($comments[$info['msgid']]) ?
|
|
$comments[$info['msgid']] : '') .
|
|
'#'.trim($info['file'],'/') . ':' . $info['line'] . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach($fields as $id => $str) {
|
|
if ($this->parse_comment && isset($comments[$id])) {
|
|
$l .= $comments[$id];
|
|
}
|
|
|
|
$l .=
|
|
'$GLOBALS[\'__l10n\'][\'' . addcslashes($id, "'") . '\'] = ' .
|
|
'\'' . self::langphpString($str, true) . "';\n";
|
|
|
|
if ($this->parse_comment) {
|
|
$l .= "\n";
|
|
}
|
|
}
|
|
$l .= "";
|
|
|
|
self::writeLangFile($locales . '/' . $lang . '/' . $group . '.lang.php', $l, true);
|
|
}
|
|
|
|
/* Parse a .lang.php string */
|
|
private static function langphpString($string, $reverse = false)
|
|
{
|
|
if ($reverse) {
|
|
$smap = array('\'', "\n", "\t", "\r");
|
|
$rmap = array('\\\'', '\\n"' . "\n" . '"', '\\t', '\\r');
|
|
return trim((string) str_replace($smap, $rmap, $string));
|
|
} else {
|
|
$smap = array('/\\\\n/', '/\\\\r/', '/\\\\t/', "/\\\'/");
|
|
$rmap = array("\n", "\r", "\t", "'");
|
|
return trim((string) preg_replace($smap, $rmap, $string));
|
|
}
|
|
}
|
|
|
|
/* Try if a file is a .po file */
|
|
public static function isPoFile($file)
|
|
{
|
|
return files::getExtension($file) == 'po';
|
|
}
|
|
|
|
/* Get and parse a .po file */
|
|
public static function getPoFile($file)
|
|
{
|
|
if (!file_exists($file)) {
|
|
return false;
|
|
}
|
|
|
|
$res = array();
|
|
$content = implode('',file($file));
|
|
|
|
$count = preg_match_all('/msgid\s(.*(?:\n".*")*)\nmsgstr\s(.*(?:\n".*")*)/', $content, $m);
|
|
|
|
if (!$count) return false;
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$id = preg_replace('/"(.*)"/s','\\1', $m[1][$i]);
|
|
$str= preg_replace('/"(.*)"/s','\\1', $m[2][$i]);
|
|
|
|
$str = self::poString($str);
|
|
|
|
if ($str) {
|
|
$res[self::poString($id)] = $str;
|
|
}
|
|
}
|
|
|
|
if (!empty($res[''])) {
|
|
$res = array_diff_key($res, array(''=>1));
|
|
}
|
|
return $res;
|
|
}
|
|
/* Construct and parse a .po file */
|
|
private function setPoFile($module, $lang, $group, $fields)
|
|
{
|
|
if (!$this->write_po) {
|
|
return;
|
|
}
|
|
|
|
# Not a module installed
|
|
$locales = self::getLangsFolder($module, true);
|
|
|
|
# Path is right formed
|
|
self::isIsoCode($lang, true);
|
|
|
|
$l = '';
|
|
if ($this->parse_comment) {
|
|
$l .=
|
|
'# Language: ' . self::$iso[$lang] . "\n" .
|
|
'# Module: ' . $module . " - " . self::moduleInfo($module, 'version') . "\n" .
|
|
'# Date: ' . dt::str('%Y-%m-%d %H:%M:%S') . "\n";
|
|
|
|
if ($this->parse_user && !empty($this->parse_userinfo)) {
|
|
$search = self::$allowed_user_informations;
|
|
foreach($search AS $n) {
|
|
$replace[] = $this->core->auth->getInfo('user_' . $n);
|
|
}
|
|
$info = trim(str_replace($search, $replace, $this->parse_userinfo));
|
|
if (!empty($info)) {
|
|
$l .= '# Author: ' . html::escapeHTML($info) . "\n";
|
|
}
|
|
}
|
|
$l .=
|
|
'# Translated with translater ' . $this->core->plugins->moduleInfo('translater', 'version') . "\n";
|
|
}
|
|
$l .=
|
|
"\n".
|
|
"msgid \"\"\n" .
|
|
"msgstr \"\"\n" .
|
|
'"Content-Type: text/plain; charset=UTF-8\n"' . "\n" .
|
|
'"Project-Id-Version: ' . $module . ' ' . self::moduleInfo($module, 'version') . '\n"' . "\n" .
|
|
'"POT-Creation-Date: \n"' . "\n" .
|
|
'"PO-Revision-Date: ' . date('c') . '\n"' . "\n" .
|
|
'"Last-Translator: ' . $this->core->auth->getInfo('user_cn') . '\n"' . "\n" .
|
|
'"Language-Team: \n"' . "\n" .
|
|
'"MIME-Version: 1.0\n"' . "\n" .
|
|
'"Content-Transfer-Encoding: 8bit\n"' . "\n" .
|
|
'"Plural-Forms: nplurals=2; plural=(n > 1);\n"' . "\n\n";
|
|
|
|
if ($this->parse_comment) {
|
|
$infos = self::getMsgids($module);
|
|
foreach($infos AS $info) {
|
|
if (isset($fields[$info['msgid']])) {
|
|
$comments[$info['msgid']] = (isset($comments[$info['msgid']]) ?
|
|
$comments[$info['msgid']] : '') .
|
|
'#: '.trim($info['file'],'/') . ':' . $info['line'] . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach($fields as $id => $str) {
|
|
if ($this->parse_comment && isset($comments[$id])) {
|
|
$l .= $comments[$id];
|
|
}
|
|
$l .=
|
|
'msgid "' . self::poString($id, true) . '"' . "\n" .
|
|
'msgstr "' . self::poString($str, true) . '"' . "\n\n";
|
|
}
|
|
|
|
self::writeLangFile($locales . '/' . $lang . '/' . $group . '.po', $l, true);
|
|
}
|
|
|
|
/* Parse .po string */
|
|
private static function poString($string, $reverse = false)
|
|
{
|
|
if ($reverse) {
|
|
$smap = array('"', "\n", "\t", "\r");
|
|
$rmap = array('\\"', '\\n"' . "\n" . '"', '\\t', '\\r');
|
|
return trim((string) str_replace($smap, $rmap, $string));
|
|
} else {
|
|
$smap = array('/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\"/');
|
|
$rmap = array('', "\n", "\r", "\t", '"');
|
|
return trim((string) preg_replace($smap, $rmap, $string));
|
|
}
|
|
}
|
|
|
|
/* Scan recursively a folder and return files and folders names */
|
|
public static function scandir($path, $dir = '', $res = array())
|
|
{
|
|
$path = path::real($path);
|
|
if (!is_dir($path) || !is_readable($path)) {
|
|
return array();
|
|
}
|
|
|
|
$files = files::scandir($path);
|
|
|
|
foreach($files AS $file) {
|
|
if ($file == '.' || $file == '..') {
|
|
continue;
|
|
}
|
|
|
|
if (is_dir($path . '/' . $file)) {
|
|
$res[] = $file;
|
|
$res = self::scanDir($path . '/' . $file, $dir . '/' . $file, $res);
|
|
} else {
|
|
$res[] = empty($dir) ? $file : $dir . '/' . $file;
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/* Return array of langs like in clearbreaks l10n */
|
|
public static function getIsoCodes($flip = false, $name_with_code = false)
|
|
{
|
|
if (empty(self::$iso)) {
|
|
self::$iso = l10n::getISOcodes($flip, $name_with_code);
|
|
}
|
|
return self::$iso;
|
|
}
|
|
|
|
/* Find if lang code exists or lang name */
|
|
public static function isIsoCode($iso, $throw = false)
|
|
{
|
|
$codes = self::getIsoCodes();
|
|
$code = isset($codes[$iso]) ? $codes[$iso] : false;
|
|
if (!$code && $throw) {
|
|
throw new Exception(sprintf(
|
|
__('Cannot find language for code %s'), $iso
|
|
));
|
|
}
|
|
return $code;
|
|
}
|
|
} |