[ '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 = "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; } }