* Copyright (C) 2004-2012 Destailleur Laurent * Copyright (C) 2005-2010 Regis Houssin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/core/class/translate.class.php * \ingroup core * \brief File for Tanslate class */ /** * Class to manage translations */ class Translate { var $dir; // Directories that contains /langs subdirectory var $defaultlang; // Current language for current user var $direction = 'ltr'; // Left to right or Right to left var $charset_inputfile=array(); // To store charset encoding used for language var $charset_output='UTF-8'; // Codage used by "trans" method outputs var $tab_translate=array(); // Array of all translations key=>value private $_tab_loaded=array(); // Array to store result after loading each language file var $cache_labels=array(); // Cache for labels return by getLabelFromKey method /** * Constructor * * @param string $dir Force directory that contains /langs subdirectory (value is sometine '..' like into install/* pages or support/* pages). * @param Conf $conf Object with Dolibarr configuration */ function __construct($dir,$conf) { if (! empty($conf->file->character_set_client)) $this->charset_output=$conf->file->character_set_client; // If charset output is forced if ($dir) $this->dir=array($dir); else $this->dir=$conf->file->dol_document_root; } /** * Set accessor for this->defaultlang * * @param string $srclang Language to use * @return void */ function setDefaultLang($srclang='en_US') { global $conf; //dol_syslog(get_class($this)."::setDefaultLang srclang=".$srclang,LOG_DEBUG); // If a module ask to force a priority on langs directories (to use its own lang files) if (! empty($conf->global->MAIN_FORCELANGDIR)) { $more=array(); $i=0; foreach($conf->file->dol_document_root as $dir) { $newdir=$dir.$conf->global->MAIN_FORCELANGDIR; // For example $conf->global->MAIN_FORCELANGDIR is '/mymodule' meaning we search files into '/mymodule/langs/xx_XX' if (! in_array($newdir,$this->dir)) { $more['module_'.$i]=$newdir; $i++; // We add the forced dir into the array $more. Just after, we add entries into $more to list of lang dir $this->dir. } } $this->dir=array_merge($more,$this->dir); // Forced dir ($more) are before standard dirs ($this->dir) } $this->origlang=$srclang; if (empty($srclang) || $srclang == 'auto') { $langpref=empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])?'':$_SERVER['HTTP_ACCEPT_LANGUAGE']; $langpref=preg_replace("/;([^,]*)/i","",$langpref); $langpref=str_replace("-","_",$langpref); $langlist=preg_split("/[;,]/",$langpref); $codetouse=$langlist[0]; } else $codetouse=$srclang; // We redefine $srclang $langpart=explode("_",$codetouse); //print "Short before _ : ".$langpart[0].'/ Short after _ : '.$langpart[1].'
'; if (! empty($langpart[1])) // If it's for a codetouse that is a long code xx_YY { // Array force long code from first part, even if long code is defined $longforshort=array('ar'=>'ar_SA'); if (isset($longforshort[strtolower($langpart[0])])) $srclang=$longforshort[strtolower($langpart[0])]; else { $srclang=strtolower($langpart[0])."_".strtoupper($langpart[1]); $longforlong=array('no_nb'=>'nb_NO'); if (isset($longforlong[strtolower($srclang)])) $srclang=$longforlong[strtolower($srclang)]; } } else { // If it's for a codetouse that is a short code xx // Array to convert short lang code into long code. $longforshort=array('ar'=>'ar_SA', 'el'=>'el_GR', 'ca'=>'ca_ES', 'en'=>'en_US', 'nb'=>'nb_NO', 'no'=>'nb_NO'); if (isset($longforshort[strtolower($langpart[0])])) $srclang=$longforshort[strtolower($langpart[0])]; else if (! empty($langpart[0])) $srclang=strtolower($langpart[0])."_".strtoupper($langpart[0]); else $srclang='en_US'; } $this->defaultlang=$srclang; //print 'this->defaultlang='.$this->defaultlang; } /** * Return active language code for current user * It's an accessor for this->defaultlang * * @param int $mode 0=Long language code, 1=Short language code * @return string Language code used (en_US, en_AU, fr_FR, ...) */ function getDefaultLang($mode=0) { if (empty($mode)) return $this->defaultlang; else return substr($this->defaultlang,0,2); } /** * Load translation key-value for a particular file, into a memory array. * If data for file already loaded, do nothing. * All data in translation array are stored in UTF-8 format. * tab_loaded is completed with $domain key. * Value for hash are: 1:Loaded from disk, 2:Not found, 3:Loaded from cache * * @param string $domain File name to load (.lang file). Must be "file" or "file@module" for module language files: * If $domain is "file@module" instead of "file" then we look for module lang file * in htdocs/custom/modules/mymodule/langs/code_CODE/file.lang * then in htdocs/module/langs/code_CODE/file.lang instead of htdocs/langs/code_CODE/file.lang * @param string $alt 0 (try xx_ZZ then 1), 1 (try xx_XX then 2), 2 (try en_US or fr_FR or es_ES) * @param int $stopafterdirection Stop when the DIRECTION tag is found (optimize speed) * @param int $forcelangdir To force a different lang directory * @return int <0 if KO, 0 if already loaded, >0 if OK */ function Load($domain,$alt=0,$stopafterdirection=0,$forcelangdir='') { global $conf; // Check parameters if (empty($domain)) { dol_print_error('',get_class($this)."::Load ErrorWrongParameters"); exit; } if ($this->defaultlang == 'none_NONE') return; // Special language code to not translate keys //dol_syslog("Translate::Load Start domain=".$domain." alt=".$alt." forcelangdir=".$forcelangdir." this->defaultlang=".$this->defaultlang); $newdomain = $domain; $modulename = ''; // Search if a module directory name is provided into lang file name if (preg_match('/^([^@]+)@([^@]+)$/i',$domain,$regs)) { $newdomain = $regs[1]; $modulename = $regs[2]; } // Check cache if (! empty($this->_tab_loaded[$newdomain])) // File already loaded for this domain { //dol_syslog("Translate::Load already loaded for newdomain=".$newdomain); return 0; } $fileread=0; $langofdir=(empty($forcelangdir)?$this->defaultlang:$forcelangdir); // Redefine alt $langarray=explode('_',$langofdir); if ($alt < 1 && strtolower($langarray[0]) == strtolower($langarray[1])) $alt=1; if ($alt < 2 && (strtolower($langofdir) == 'en_us' || strtolower($langofdir) == 'fr_fr' || strtolower($langofdir) == 'es_es')) $alt=2; foreach($this->dir as $keydir => $searchdir) { // Directory of translation files $file_lang = $searchdir.($modulename?'/'.$modulename:'')."/langs/".$langofdir."/".$newdomain.".lang"; $file_lang_osencoded=dol_osencode($file_lang); $filelangexists=is_file($file_lang_osencoded); //dol_syslog('Translate::Load Try to read for alt='.$alt.' langofdir='.$langofdir.' file_lang='.$file_lang." => filelangexists=".$filelangexists); if ($filelangexists) { // TODO Move cache read out of loop on dirs $found=false; // Enable caching of lang file in memory (not by default) $usecachekey=''; // Using a memcached server if (! empty($conf->memcached->enabled) && ! empty($conf->global->MEMCACHED_SERVER)) { $usecachekey=$newdomain.'_'.$langofdir.'_'.md5($file_lang); // Should not contains special chars } // Using cache with shmop. Speed gain: 40ms - Memory overusage: 200ko (Size of session cache file) else if (isset($conf->global->MAIN_OPTIMIZE_SPEED) && ($conf->global->MAIN_OPTIMIZE_SPEED & 0x02)) { $usecachekey=$newdomain; } if ($usecachekey) { //dol_syslog('Translate::Load we will cache result into usecachekey '.$usecachekey); require_once(DOL_DOCUMENT_ROOT ."/core/lib/memory.lib.php"); $tmparray=dol_getcache($usecachekey); if (is_array($tmparray) && count($tmparray)) { $this->tab_translate=array_merge($tmparray,$this->tab_translate); // Already found values tab_translate overwrites duplicates //print $newdomain."\n"; //var_dump($this->tab_translate); if ($alt == 2) $fileread=1; $found=true; // Found in dolibarr PHP cache } } if (! $found) { if ($fp = @fopen($file_lang,"rt")) { if ($usecachekey) $tabtranslatedomain=array(); // To save lang content in cache while ($line = fgets($fp,4096)) // Ex: Need 225ms for all fgets on all lang file for Third party page. Same speed than file_get_contents { if ($line[0] != "\n" && $line[0] != " " && $line[0] != "#") { $tab=explode('=',$line,2); $key=trim($tab[0]); //print "Domain=$domain, found a string for $tab[0] with value $tab[1]
"; if ((! empty($conf->global->MAIN_USE_CUSTOM_TRANSLATION) || empty($this->tab_translate[$key])) && isset($tab[1])) // If data was already found, we must not enter here, even if MAIN_FORCELANGDIR is set (MAIN_FORCELANGDIR is to replace lang dir, not to overwrite) { $value=trim(preg_replace('/\\n/',"\n",$tab[1])); if ($key == 'DIRECTION') // This is to declare direction of language { if ($alt < 2 || empty($this->tab_translate[$key])) // We load direction only for primary files or if not yet loaded { $this->tab_translate[$key]=$value; if ($stopafterdirection) break; // We do not save tab if we stop after DIRECTION else if ($usecachekey) $tabtranslatedomain[$key]=$value; } } else { $this->tab_translate[$key]=$value; if ($usecachekey) $tabtranslatedomain[$key]=$value; // To save lang content in cache } } } } fclose($fp); $fileread=1; // TODO Move cache write out of loop on dirs // To save lang content for usecachekey into cache if ($usecachekey && count($tabtranslatedomain)) { $ressetcache=dol_setcache($usecachekey,$tabtranslatedomain); if ($ressetcache < 0) { $error='Failed to set cache for usecachekey='.$usecachekey.' result='.$ressetcache; dol_syslog($error, LOG_ERR); } } if (empty($conf->global->MAIN_FORCELANGDIR) && empty($conf->global->MAIN_USE_CUSTOM_TRANSLATION)) break; // Break loop on each root dir. If a module has forced dir, we do not stop loop. } } } } // Now we complete with next file if ($alt == 0) { // This function MUST NOT contains call to syslog //dol_syslog("Translate::Load loading alternate translation file (to complete ".$this->defaultlang."/".$newdomain.".lang file)", LOG_DEBUG); $langofdir=strtolower($langarray[0]).'_'.strtoupper($langarray[0]); $this->load($domain,$alt+1,$stopafterdirection,$langofdir); } // Now we complete with reference en_US/fr_FR/es_ES file if ($alt == 1) { // This function MUST NOT contains call to syslog //dol_syslog("Translate::Load loading alternate translation file (to complete ".$this->defaultlang."/".$newdomain.".lang file)", LOG_DEBUG); $langofdir='en_US'; if (preg_match('/^fr/i',$langarray[0])) $langofdir='fr_FR'; if (preg_match('/^es/i',$langarray[0])) $langofdir='es_ES'; $this->load($domain,$alt+1,$stopafterdirection,$langofdir); } if ($alt == 2) { if ($fileread) $this->_tab_loaded[$newdomain]=1; // Set domain file as loaded if (empty($this->_tab_loaded[$newdomain])) $this->_tab_loaded[$newdomain]=2; // Marque ce fichier comme non trouve } // Check to be sure that SeparatorDecimal differs from SeparatorThousand if (! empty($this->tab_translate["SeparatorDecimal"]) && ! empty($this->tab_translate["SeparatorThousand"]) && $this->tab_translate["SeparatorDecimal"] == $this->tab_translate["SeparatorThousand"]) $this->tab_translate["SeparatorThousand"]=''; return 1; } /** * Return translated value of key. Search in lang file, then into database. * If not found, return key. * WARNING: To avoid infinite loop (getLabelFromKey->transnoentities->getTradFromKey), getLabelFromKey must * not be called with same value than input. * * @param string $key Key to translate * @return string Translated string */ function getTradFromKey($key) { global $db; $newstr=$key; if (preg_match('/^Currency([A-Z][A-Z][A-Z])$/i',$key,$reg)) { $newstr=$this->getLabelFromKey($db,$reg[1],'c_currencies','code_iso','label'); } else if (preg_match('/^SendingMethod([0-9A-Z]+)$/i',$key,$reg)) { $newstr=$this->getLabelFromKey($db,$reg[1],'c_shipment_mode','code','libelle'); } else if (preg_match('/^PaymentTypeShort([0-9A-Z]+)$/i',$key,$reg)) { $newstr=$this->getLabelFromKey($db,$reg[1],'c_paiement','code','libelle'); } else if (preg_match('/^Civility([0-9A-Z]+)$/i',$key,$reg)) { $newstr=$this->getLabelFromKey($db,$reg[1],'c_civilite','code','civilite'); } else if (preg_match('/^OrderSource([0-9A-Z]+)$/i',$key,$reg)) { // TODO Add a table for OrderSourceX //$newstr=$this->getLabelFromKey($db,$reg[1],'c_ordersource','code','label'); } return $newstr; } /** * Return text translated of text received as parameter (and encode it into HTML) * Si il n'y a pas de correspondance pour ce texte, on cherche dans fichier alternatif * et si toujours pas trouve, il est retourne tel quel * Les parametres de cette methode peuvent contenir de balises HTML. * * @param string $key Key to translate * @param string $param1 chaine de param1 * @param string $param2 chaine de param2 * @param string $param3 chaine de param3 * @param string $param4 chaine de param4 * @param int $maxsize Max length of text * @return string Translated string (encoded into HTML entities and UTF8) */ function trans($key, $param1='', $param2='', $param3='', $param4='', $maxsize=0) { global $conf; if (! empty($this->tab_translate[$key])) // Translation is available { $str=$this->tab_translate[$key]; if (! preg_match('/^Format/',$key)) $str=sprintf($str,$param1,$param2,$param3,$param4); // Replace %s and %d except for FormatXXX strings. if ($maxsize) $str=dol_trunc($str,$maxsize); // We replace some HTML tags by __xx__ to avoid having them encoded by htmlentities $str=str_replace(array('<','>','"',),array('__lt__','__gt__','__quot__'),$str); // Crypt string into HTML $str=htmlentities($str,ENT_QUOTES,$this->charset_output); // Restore HTML tags $str=str_replace(array('__lt__','__gt__','__quot__'),array('<','>','"',),$str); // Overwrite translation if (! empty($conf->global->MAIN_OVERWRITE_TRANS)) // Overwrite translation with string1:newstring1,string2:newstring2 { $tmparray=explode(',', $conf->global->MAIN_OVERWRITE_TRANS); foreach($tmparray as $tmp) { $tmparray2=explode(':',$tmp); if ($tmparray2[0]==$str) { $str=$tmparray2[1]; break; } } } return $str; } else // Translation is not available { //$str=$this->getTradFromKey($key); //return $this->convToOutputCharset($str); return $this->getTradFromKey($key); } } /** * Return translated value of a text string * Si il n'y a pas de correspondance pour ce texte, on cherche dans fichier alternatif * et si toujours pas trouve, il est retourne tel quel. * Parameters of this method must not contains any HTML tags. * * @param string $key Key to translate * @param string $param1 chaine de param1 * @param string $param2 chaine de param2 * @param string $param3 chaine de param3 * @param string $param4 chaine de param4 * @return string Translated string (encoded into UTF8) */ function transnoentities($key, $param1='', $param2='', $param3='', $param4='') { return $this->convToOutputCharset($this->transnoentitiesnoconv($key, $param1, $param2, $param3, $param4)); } /** * Return translated value of a text string * Si il n'y a pas de correspondance pour ce texte, on cherche dans fichier alternatif * et si toujours pas trouve, il est retourne tel quel. * No convert to encoding charset of lang object is done. * Parameters of this method must not contains any HTML tags. * * @param string $key Key to translate * @param string $param1 chaine de param1 * @param string $param2 chaine de param2 * @param string $param3 chaine de param3 * @param string $param4 chaine de param4 * @return string Translated string */ function transnoentitiesnoconv($key, $param1='', $param2='', $param3='', $param4='') { if (! empty($this->tab_translate[$key])) // Translation is available { $str=$this->tab_translate[$key]; if (! preg_match('/^Format/',$key)) $str=sprintf($str,$param1,$param2,$param3,$param4); // Replace %s and %d except for FormatXXX strings. } else { $str=$this->getTradFromKey($key); } return $str; } /** * Return translation of a key depending on country * * @param string $str string root to translate * @param string $countrycode country code (FR, ...) * @return string translated string */ function transcountry($str, $countrycode) { if ($this->tab_translate["$str$countrycode"]) return $this->trans("$str$countrycode"); else return $this->trans($str); } /** * Retourne la version traduite du texte passe en parametre complete du code pays * * @param string $str string root to translate * @param string $countrycode country code (FR, ...) * @return string translated string */ function transcountrynoentities($str, $countrycode) { if ($this->tab_translate["$str$countrycode"]) return $this->transnoentities("$str$countrycode"); else return $this->transnoentities($str); } /** * Convert a string into output charset (this->charset_output that should be defined to conf->file->character_set_client) * * @param string $str String to convert * @param string $pagecodefrom Page code of src string * @return string Converted string */ function convToOutputCharset($str,$pagecodefrom='UTF-8') { if ($pagecodefrom == 'ISO-8859-1' && $this->charset_output == 'UTF-8') $str=utf8_encode($str); if ($pagecodefrom == 'UTF-8' && $this->charset_output == 'ISO-8859-1') $str=utf8_decode(str_replace('€',chr(128),$str)); return $str; } /** * Return list of all available languages * * @param string $langdir Directory to scan * @param string $maxlength Max length for each value in combo box (will be truncated) * @param int $usecode Show code instead of country name for language variant * @return array List of languages */ function get_available_languages($langdir=DOL_DOCUMENT_ROOT,$maxlength=0,$usecode=0) { global $conf; // We scan directory langs to detect available languages $handle=opendir($langdir."/langs"); $langs_available=array(); while ($dir = trim(readdir($handle))) { if (preg_match('/^[a-z]+_[A-Z]+/i',$dir)) { $this->load("languages"); if ($usecode || ! empty($conf->global->MAIN_SHOW_LANGUAGE_CODE)) { $langs_available[$dir] = $dir.': '.dol_trunc($this->trans('Language_'.$dir),$maxlength); } else { $langs_available[$dir] = $this->trans('Language_'.$dir); } } } return $langs_available; } /** * Return if a filename $filename exists for current language (or alternate language) * * @param string $filename Language filename to search * @param string $searchalt Search also alernate language file * @return boolean true if exists and readable */ function file_exists($filename,$searchalt=0) { // Test si fichier dans repertoire de la langue foreach($this->dir as $searchdir) { if (is_readable(dol_osencode($searchdir."/langs/".$this->defaultlang."/".$filename))) return true; if ($searchalt) { // Test si fichier dans repertoire de la langue alternative if ($this->defaultlang != "en_US") $filenamealt = $searchdir."/langs/en_US/".$filename; else $filenamealt = $searchdir."/langs/fr_FR/".$filename; if (is_readable(dol_osencode($filenamealt))) return true; } } return false; } /** * Return full text translated to language label for a key. Store key-label in a cache. * This function need module "numberwords" to be installed. If not it will return * same number (this module is not provided by default as it use non GPL source code). * * @param int $number Number to encode in full text * @param int $isamount 1=It's an amount, 0=it's just a number * @return string Label translated in UTF8 (but without entities) * 10 if setDefaultLang was en_US => ten * 123 if setDefaultLang was fr_FR => cent vingt trois */ function getLabelFromNumber($number,$isamount=0) { global $conf; $newnumber=$number; $dirsubstitutions=array_merge(array(),$conf->modules_parts['substitutions']); foreach($dirsubstitutions as $reldir) { $dir=dol_buildpath($reldir,0); $newdir=dol_osencode($dir); // Check if directory exists if (! dol_is_dir($dir)) continue; $fonc='numberwords'; if (file_exists($newdir.'/functions_'.$fonc.'.lib.php')) { include_once($newdir.'/functions_'.$fonc.'.lib.php'); $newnumber=numberwords_getLabelFromNumber($this,$number,$isamount); break; } } return $newnumber; } /** * Return a label for a key. Store key-label into cache variable $this->cache_labels to save SQL requests to get labels. * This function can be used to get label in database but more often to get code from key id. * * @param DoliBD $db Database handler * @param string $key Key to get label (key in language file) * @param string $tablename Table name without prefix * @param string $fieldkey Field for key * @param string $fieldlabel Field for label * @return string Label in UTF8 (but without entities) */ function getLabelFromKey($db,$key,$tablename,$fieldkey,$fieldlabel) { // If key empty if ($key == '') return ''; // Check in language array if ($this->transnoentities($key) != $key) { return $this->transnoentities($key); // Found in language array } // Check in cache if (isset($this->cache_labels[$tablename][$key])) // Can be defined to 0 or '' { return $this->cache_labels[$tablename][$key]; // Found in cache } $sql = "SELECT ".$fieldlabel." as label"; $sql.= " FROM ".MAIN_DB_PREFIX.$tablename; $sql.= " WHERE ".$fieldkey." = '".$key."'"; dol_syslog(get_class($this).'::getLabelFromKey sql='.$sql,LOG_DEBUG); $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) $this->cache_labels[$tablename][$key]=$obj->label; else $this->cache_labels[$tablename][$key]=$key; $db->free($resql); return $this->cache_labels[$tablename][$key]; } else { $this->error=$db->lasterror(); dol_syslog(get_class($this).'::getLabelFromKey error='.$this->error,LOG_ERR); return -1; } } } ?>