translate.class.php 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. <?php
  2. /* Copyright (C) 2001 Eric Seigne <erics@rycks.com>
  3. * Copyright (C) 2004-2015 Destailleur Laurent <eldy@users.sourceforge.net>
  4. * Copyright (C) 2005-2010 Regis Houssin <regis.houssin@inodbox.com>
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 3 of the License, or
  9. * any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * \file htdocs/core/class/translate.class.php
  21. * \ingroup core
  22. * \brief File for Tanslate class
  23. */
  24. /**
  25. * Class to manage translations
  26. */
  27. class Translate
  28. {
  29. public $dir; // Directories that contains /langs subdirectory
  30. public $defaultlang; // Current language for current user
  31. public $charset_output='UTF-8'; // Codage used by "trans" method outputs
  32. public $tab_translate=array(); // Array of all translations key=>value
  33. private $_tab_loaded=array(); // Array to store result after loading each language file
  34. public $cache_labels=array(); // Cache for labels return by getLabelFromKey method
  35. public $cache_currencies=array(); // Cache to store currency symbols
  36. private $cache_currencies_all_loaded=false;
  37. /**
  38. * Constructor
  39. *
  40. * @param string $dir Force directory that contains /langs subdirectory (value is sometimes '..' like into install/* pages or support/* pages). Use '' by default.
  41. * @param Conf $conf Object with Dolibarr configuration
  42. */
  43. function __construct($dir, $conf)
  44. {
  45. if (! empty($conf->file->character_set_client)) $this->charset_output=$conf->file->character_set_client; // If charset output is forced
  46. if ($dir) $this->dir=array($dir);
  47. else $this->dir=$conf->file->dol_document_root;
  48. }
  49. /**
  50. * Set accessor for this->defaultlang
  51. *
  52. * @param string $srclang Language to use. If '' or 'auto', we use browser lang.
  53. * @return void
  54. */
  55. function setDefaultLang($srclang='en_US')
  56. {
  57. global $conf;
  58. //dol_syslog(get_class($this)."::setDefaultLang srclang=".$srclang,LOG_DEBUG);
  59. // If a module ask to force a priority on langs directories (to use its own lang files)
  60. if (! empty($conf->global->MAIN_FORCELANGDIR))
  61. {
  62. $more=array();
  63. $i=0;
  64. foreach($conf->file->dol_document_root as $dir)
  65. {
  66. $newdir=$dir.$conf->global->MAIN_FORCELANGDIR; // For example $conf->global->MAIN_FORCELANGDIR is '/mymodule' meaning we search files into '/mymodule/langs/xx_XX'
  67. if (! in_array($newdir,$this->dir))
  68. {
  69. $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.
  70. }
  71. }
  72. $this->dir=array_merge($more,$this->dir); // Forced dir ($more) are before standard dirs ($this->dir)
  73. }
  74. $this->origlang=$srclang;
  75. if (empty($srclang) || $srclang == 'auto')
  76. {
  77. $langpref=empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])?'':$_SERVER['HTTP_ACCEPT_LANGUAGE'];
  78. $langpref=preg_replace("/;([^,]*)/i","",$langpref);
  79. $langpref=str_replace("-","_",$langpref);
  80. $langlist=preg_split("/[;,]/",$langpref);
  81. $codetouse=$langlist[0];
  82. }
  83. else $codetouse=$srclang;
  84. // We redefine $srclang
  85. $langpart=explode("_",$codetouse);
  86. //print "Short code before _ : ".$langpart[0].' / Short code after _ : '.$langpart[1].'<br>';
  87. if (! empty($langpart[1])) // If it's for a codetouse that is a long code xx_YY
  88. {
  89. // Array force long code from first part, even if long code is defined
  90. $longforshort=array('ar'=>'ar_SA');
  91. $longforshortexcep=array('ar_EG');
  92. if (isset($longforshort[strtolower($langpart[0])]) && ! in_array($codetouse, $longforshortexcep)) $srclang=$longforshort[strtolower($langpart[0])];
  93. else if (! is_numeric($langpart[1])) { // Second part YY may be a numeric with some Chrome browser
  94. $srclang=strtolower($langpart[0])."_".strtoupper($langpart[1]);
  95. $longforlong=array('no_nb'=>'nb_NO');
  96. if (isset($longforlong[strtolower($srclang)])) $srclang=$longforlong[strtolower($srclang)];
  97. }
  98. else $srclang=strtolower($langpart[0])."_".strtoupper($langpart[0]);
  99. }
  100. else { // If it's for a codetouse that is a short code xx
  101. // Array to convert short lang code into long code.
  102. $longforshort=array('ar'=>'ar_SA', 'el'=>'el_GR', 'ca'=>'ca_ES', 'en'=>'en_US', 'nb'=>'nb_NO', 'no'=>'nb_NO');
  103. if (isset($longforshort[strtolower($langpart[0])])) $srclang=$longforshort[strtolower($langpart[0])];
  104. else if (! empty($langpart[0])) $srclang=strtolower($langpart[0])."_".strtoupper($langpart[0]);
  105. else $srclang='en_US';
  106. }
  107. $this->defaultlang=$srclang;
  108. //print 'this->defaultlang='.$this->defaultlang;
  109. }
  110. /**
  111. * Return active language code for current user
  112. * It's an accessor for this->defaultlang
  113. *
  114. * @param int $mode 0=Long language code, 1=Short language code (en, fr, es, ...)
  115. * @return string Language code used (en_US, en_AU, fr_FR, ...)
  116. */
  117. function getDefaultLang($mode=0)
  118. {
  119. if (empty($mode)) return $this->defaultlang;
  120. else return substr($this->defaultlang,0,2);
  121. }
  122. /**
  123. * Load translation files.
  124. *
  125. * @param array $domains Array of lang files to load
  126. * @return int <0 if KO, 0 if already loaded or loading not required, >0 if OK
  127. */
  128. function loadLangs($domains)
  129. {
  130. foreach($domains as $domain)
  131. {
  132. $this->load($domain);
  133. }
  134. }
  135. /**
  136. * Load translation key-value for a particular file, into a memory array.
  137. * If data for file already loaded, do nothing.
  138. * All data in translation array are stored in UTF-8 format.
  139. * tab_loaded is completed with $domain key.
  140. * rule "we keep first entry found with we keep last entry found" so it is probably not what you want to do.
  141. *
  142. * Value for hash are: 1:Loaded from disk, 2:Not found, 3:Loaded from cache
  143. *
  144. * @param string $domain File name to load (.lang file). Must be "file" or "file@module" for module language files:
  145. * If $domain is "file@module" instead of "file" then we look for module lang file
  146. * in htdocs/custom/modules/mymodule/langs/code_CODE/file.lang
  147. * then in htdocs/module/langs/code_CODE/file.lang instead of htdocs/langs/code_CODE/file.lang
  148. * @param integer $alt 0 (try xx_ZZ then 1), 1 (try xx_XX then 2), 2 (try en_US)
  149. * @param int $stopafterdirection Stop when the DIRECTION tag is found (optimize speed)
  150. * @param int $forcelangdir To force a different lang directory
  151. * @param int $loadfromfileonly 1=Do not load overwritten translation from file or old conf.
  152. * @return int <0 if KO, 0 if already loaded or loading not required, >0 if OK
  153. * @see loadLangs
  154. */
  155. function load($domain,$alt=0,$stopafterdirection=0,$forcelangdir='',$loadfromfileonly=0)
  156. {
  157. global $conf,$db;
  158. //dol_syslog("Translate::Load Start domain=".$domain." alt=".$alt." forcelangdir=".$forcelangdir." this->defaultlang=".$this->defaultlang);
  159. // Check parameters
  160. if (empty($domain))
  161. {
  162. dol_print_error('',get_class($this)."::Load ErrorWrongParameters");
  163. return -1;
  164. }
  165. if ($this->defaultlang == 'none_NONE') return 0; // Special language code to not translate keys
  166. // Load $this->tab_translate[] from database
  167. if (empty($loadfromfileonly) && count($this->tab_translate) == 0) $this->loadFromDatabase($db); // No translation was never loaded yet, so we load database.
  168. $newdomain = $domain;
  169. $modulename = '';
  170. // Search if a module directory name is provided into lang file name
  171. if (preg_match('/^([^@]+)@([^@]+)$/i',$domain,$regs))
  172. {
  173. $newdomain = $regs[1];
  174. $modulename = $regs[2];
  175. }
  176. // Check cache
  177. if (! empty($this->_tab_loaded[$newdomain])) // File already loaded for this domain
  178. {
  179. //dol_syslog("Translate::Load already loaded for newdomain=".$newdomain);
  180. return 0;
  181. }
  182. $fileread=0;
  183. $langofdir=(empty($forcelangdir)?$this->defaultlang:$forcelangdir);
  184. // Redefine alt
  185. $langarray=explode('_',$langofdir);
  186. if ($alt < 1 && isset($langarray[1]) && (strtolower($langarray[0]) == strtolower($langarray[1]) || in_array(strtolower($langofdir), array('el_gr')))) $alt=1;
  187. if ($alt < 2 && strtolower($langofdir) == 'en_us') $alt=2;
  188. if (empty($langofdir)) // This may occurs when load is called without setting the language and without providing a value for forcelangdir
  189. {
  190. dol_syslog("Error: ".get_class($this)."::Load was called but language was not set yet with langs->setDefaultLang(). Nothing will be loaded.", LOG_WARNING);
  191. return -1;
  192. }
  193. foreach($this->dir as $keydir => $searchdir)
  194. {
  195. // Directory of translation files
  196. $file_lang = $searchdir.($modulename?'/'.$modulename:'')."/langs/".$langofdir."/".$newdomain.".lang";
  197. $file_lang_osencoded=dol_osencode($file_lang);
  198. $filelangexists=is_file($file_lang_osencoded);
  199. //dol_syslog(get_class($this).'::Load Try to read for alt='.$alt.' langofdir='.$langofdir.' domain='.$domain.' newdomain='.$newdomain.' modulename='.$modulename.' file_lang='.$file_lang." => filelangexists=".$filelangexists);
  200. //print 'Try to read for alt='.$alt.' langofdir='.$langofdir.' domain='.$domain.' newdomain='.$newdomain.' modulename='.$modulename.' this->_tab_loaded[newdomain]='.$this->_tab_loaded[$newdomain].' file_lang='.$file_lang." => filelangexists=".$filelangexists."\n";
  201. if ($filelangexists)
  202. {
  203. // TODO Move cache read out of loop on dirs or at least filelangexists
  204. $found=false;
  205. // Enable caching of lang file in memory (not by default)
  206. $usecachekey='';
  207. // Using a memcached server
  208. if (! empty($conf->memcached->enabled) && ! empty($conf->global->MEMCACHED_SERVER))
  209. {
  210. $usecachekey=$newdomain.'_'.$langofdir.'_'.md5($file_lang); // Should not contains special chars
  211. }
  212. // Using cache with shmop. Speed gain: 40ms - Memory overusage: 200ko (Size of session cache file)
  213. else if (isset($conf->global->MAIN_OPTIMIZE_SPEED) && ($conf->global->MAIN_OPTIMIZE_SPEED & 0x02))
  214. {
  215. $usecachekey=$newdomain;
  216. }
  217. if ($usecachekey)
  218. {
  219. //dol_syslog('Translate::Load we will cache result into usecachekey '.$usecachekey);
  220. //global $aaa; $aaa+=1;
  221. //print $aaa." ".$usecachekey."\n";
  222. require_once DOL_DOCUMENT_ROOT .'/core/lib/memory.lib.php';
  223. $tmparray=dol_getcache($usecachekey);
  224. if (is_array($tmparray) && count($tmparray))
  225. {
  226. $this->tab_translate+=$tmparray; // Faster than array_merge($tmparray,$this->tab_translate). Note: If a value already exists into tab_translate, value into tmparaay is not added.
  227. //print $newdomain."\n";
  228. //var_dump($this->tab_translate);
  229. if ($alt == 2) $fileread=1;
  230. $found=true; // Found in dolibarr PHP cache
  231. }
  232. }
  233. if (! $found)
  234. {
  235. if ($fp = @fopen($file_lang,"rt"))
  236. {
  237. if ($usecachekey) $tabtranslatedomain=array(); // To save lang content in cache
  238. /**
  239. * Read each lines until a '=' (with any combination of spaces around it)
  240. * and split the rest until a line feed.
  241. * This is more efficient than fgets + explode + trim by a factor of ~2.
  242. */
  243. while ($line = fscanf($fp, "%[^= ]%*[ =]%[^\n]"))
  244. {
  245. if (isset($line[1]))
  246. {
  247. list($key, $value) = $line;
  248. //if ($domain == 'orders') print "Domain=$domain, found a string for $tab[0] with value $tab[1]. Currently in cache ".$this->tab_translate[$key]."<br>";
  249. //if ($key == 'Order') print "Domain=$domain, found a string for key=$key=$tab[0] with value $tab[1]. Currently in cache ".$this->tab_translate[$key]."<br>";
  250. if (empty($this->tab_translate[$key]))
  251. { // If translation was already found, we must not continue, even if MAIN_FORCELANGDIR is set (MAIN_FORCELANGDIR is to replace lang dir, not to overwrite entries)
  252. $value = preg_replace('/\\n/', "\n", $value); // Parse and render carriage returns
  253. if ($key == 'DIRECTION') { // This is to declare direction of language
  254. if ($alt < 2 || empty($this->tab_translate[$key])) { // We load direction only for primary files or if not yet loaded
  255. $this->tab_translate[$key] = $value;
  256. if ($stopafterdirection) {
  257. break; // We do not save tab if we stop after DIRECTION
  258. } elseif ($usecachekey) {
  259. $tabtranslatedomain[$key] = $value;
  260. }
  261. }
  262. }
  263. elseif ($key[0] == '#')
  264. {
  265. continue;
  266. }
  267. else {
  268. $this->tab_translate[$key] = $value;
  269. //if ($domain == 'orders') print "$tab[0] value $value<br>";
  270. if ($usecachekey) {
  271. $tabtranslatedomain[$key] = $value;
  272. } // To save lang content in cache
  273. }
  274. }
  275. }
  276. }
  277. fclose($fp);
  278. $fileread=1;
  279. // TODO Move cache write out of loop on dirs
  280. // To save lang content for usecachekey into cache
  281. if ($usecachekey && count($tabtranslatedomain))
  282. {
  283. $ressetcache=dol_setcache($usecachekey,$tabtranslatedomain);
  284. if ($ressetcache < 0)
  285. {
  286. $error='Failed to set cache for usecachekey='.$usecachekey.' result='.$ressetcache;
  287. dol_syslog($error, LOG_ERR);
  288. }
  289. }
  290. if (empty($conf->global->MAIN_FORCELANGDIR)) break; // Break loop on each root dir. If a module has forced dir, we do not stop loop.
  291. }
  292. }
  293. }
  294. }
  295. // Now we complete with next file (fr_CA->fr_FR, es_MX->ex_ES, ...)
  296. if ($alt == 0)
  297. {
  298. // This function MUST NOT contains call to syslog
  299. //dol_syslog("Translate::Load loading alternate translation file (to complete ".$this->defaultlang."/".$newdomain.".lang file)", LOG_DEBUG);
  300. $langofdir=strtolower($langarray[0]).'_'.strtoupper($langarray[0]);
  301. if ($langofdir == 'el_EL') $langofdir = 'el_GR'; // main parent for el_CY is not 'el_EL' but 'el_GR'
  302. if ($langofdir == 'ar_AR') $langofdir = 'ar_SA'; // main parent for ar_EG is not 'ar_AR' but 'ar_SA'
  303. $this->load($domain,$alt+1,$stopafterdirection,$langofdir);
  304. }
  305. // Now we complete with reference file (en_US)
  306. if ($alt == 1)
  307. {
  308. // This function MUST NOT contains call to syslog
  309. //dol_syslog("Translate::Load loading alternate translation file (to complete ".$this->defaultlang."/".$newdomain.".lang file)", LOG_DEBUG);
  310. $langofdir='en_US';
  311. $this->load($domain,$alt+1,$stopafterdirection,$langofdir);
  312. }
  313. // We are in the pass of the reference file. No more files to scan to complete.
  314. if ($alt == 2)
  315. {
  316. if ($fileread) $this->_tab_loaded[$newdomain]=1; // Set domain file as found so loaded
  317. if (empty($this->_tab_loaded[$newdomain])) $this->_tab_loaded[$newdomain]=2; // Set this file as not found
  318. }
  319. // This part is deprecated and replaced with table llx_overwrite_trans
  320. // Kept for backward compatibility.
  321. if (empty($loadfromfileonly))
  322. {
  323. $overwritekey='MAIN_OVERWRITE_TRANS_'.$this->defaultlang;
  324. if (! empty($conf->global->$overwritekey)) // Overwrite translation with key1:newstring1,key2:newstring2
  325. {
  326. // Overwrite translation with param MAIN_OVERWRITE_TRANS_xx_XX
  327. $tmparray=explode(',', $conf->global->$overwritekey);
  328. foreach($tmparray as $tmp)
  329. {
  330. $tmparray2=explode(':',$tmp);
  331. if (! empty($tmparray2[1])) $this->tab_translate[$tmparray2[0]]=$tmparray2[1];
  332. }
  333. }
  334. }
  335. // Check to be sure that SeparatorDecimal differs from SeparatorThousand
  336. if (! empty($this->tab_translate["SeparatorDecimal"]) && ! empty($this->tab_translate["SeparatorThousand"])
  337. && $this->tab_translate["SeparatorDecimal"] == $this->tab_translate["SeparatorThousand"]) $this->tab_translate["SeparatorThousand"]='';
  338. return 1;
  339. }
  340. /**
  341. * Load translation key-value from database into a memory array.
  342. * If data already loaded, do nothing.
  343. * All data in translation array are stored in UTF-8 format.
  344. * tab_loaded is completed with $domain key.
  345. * rule "we keep first entry found with we keep last entry found" so it is probably not what you want to do.
  346. *
  347. * Value for hash are: 1:Loaded from disk, 2:Not found, 3:Loaded from cache
  348. *
  349. * @param Database $db Database handler
  350. * @return int <0 if KO, 0 if already loaded or loading not required, >0 if OK
  351. */
  352. function loadFromDatabase($db)
  353. {
  354. global $conf;
  355. $domain='database';
  356. // Check parameters
  357. if (empty($db)) return 0; // Database handler can't be used
  358. //dol_syslog("Translate::Load Start domain=".$domain." alt=".$alt." forcelangdir=".$forcelangdir." this->defaultlang=".$this->defaultlang);
  359. $newdomain = $domain;
  360. // Check cache
  361. if (! empty($this->_tab_loaded[$newdomain])) // File already loaded for this domain 'database'
  362. {
  363. //dol_syslog("Translate::Load already loaded for newdomain=".$newdomain);
  364. return 0;
  365. }
  366. $this->_tab_loaded[$newdomain] = 1; // We want to be sure this function is called once only for domain 'database'
  367. $fileread=0;
  368. $langofdir=$this->defaultlang;
  369. if (empty($langofdir)) // This may occurs when load is called without setting the language and without providing a value for forcelangdir
  370. {
  371. dol_syslog("Error: ".get_class($this)."::Load was called but language was not set yet with langs->setDefaultLang(). Nothing will be loaded.", LOG_WARNING);
  372. return -1;
  373. }
  374. // TODO Move cache read out of loop on dirs or at least filelangexists
  375. $found=false;
  376. // Enable caching of lang file in memory (not by default)
  377. $usecachekey='';
  378. // Using a memcached server
  379. if (! empty($conf->memcached->enabled) && ! empty($conf->global->MEMCACHED_SERVER))
  380. {
  381. $usecachekey=$newdomain.'_'.$langofdir; // Should not contains special chars
  382. }
  383. // Using cache with shmop. Speed gain: 40ms - Memory overusage: 200ko (Size of session cache file)
  384. else if (isset($conf->global->MAIN_OPTIMIZE_SPEED) && ($conf->global->MAIN_OPTIMIZE_SPEED & 0x02))
  385. {
  386. $usecachekey=$newdomain;
  387. }
  388. if ($usecachekey)
  389. {
  390. //dol_syslog('Translate::Load we will cache result into usecachekey '.$usecachekey);
  391. //global $aaa; $aaa+=1;
  392. //print $aaa." ".$usecachekey."\n";
  393. require_once DOL_DOCUMENT_ROOT .'/core/lib/memory.lib.php';
  394. $tmparray=dol_getcache($usecachekey);
  395. if (is_array($tmparray) && count($tmparray))
  396. {
  397. $this->tab_translate+=$tmparray; // Faster than array_merge($tmparray,$this->tab_translate). Note: If a valuer already exists into tab_translate, value into tmparaay is not added.
  398. //print $newdomain."\n";
  399. //var_dump($this->tab_translate);
  400. $fileread=1;
  401. $found=true; // Found in dolibarr PHP cache
  402. }
  403. }
  404. if (! $found && ! empty($conf->global->MAIN_ENABLE_OVERWRITE_TRANSLATION))
  405. {
  406. // Overwrite translation with database read
  407. $sql="SELECT transkey, transvalue FROM ".MAIN_DB_PREFIX."overwrite_trans where lang='".$db->escape($this->defaultlang)."'";
  408. $resql=$db->query($sql);
  409. if ($resql)
  410. {
  411. $num = $db->num_rows($resql);
  412. if ($num)
  413. {
  414. if ($usecachekey) $tabtranslatedomain=array(); // To save lang content in cache
  415. $i = 0;
  416. while ($i < $num) // Ex: Need 225ms for all fgets on all lang file for Third party page. Same speed than file_get_contents
  417. {
  418. $obj=$db->fetch_object($resql);
  419. $key=$obj->transkey;
  420. $value=$obj->transvalue;
  421. //print "Domain=$domain, found a string for $tab[0] with value $tab[1]<br>";
  422. if (empty($this->tab_translate[$key])) // If translation was already found, we must not continue, even if MAIN_FORCELANGDIR is set (MAIN_FORCELANGDIR is to replace lang dir, not to overwrite entries)
  423. {
  424. $value=trim(preg_replace('/\\n/',"\n",$value));
  425. $this->tab_translate[$key]=$value;
  426. if ($usecachekey) $tabtranslatedomain[$key]=$value; // To save lang content in cache
  427. }
  428. $i++;
  429. }
  430. $fileread=1;
  431. // TODO Move cache write out of loop on dirs
  432. // To save lang content for usecachekey into cache
  433. if ($usecachekey && count($tabtranslatedomain))
  434. {
  435. $ressetcache=dol_setcache($usecachekey,$tabtranslatedomain);
  436. if ($ressetcache < 0)
  437. {
  438. $error='Failed to set cache for usecachekey='.$usecachekey.' result='.$ressetcache;
  439. dol_syslog($error, LOG_ERR);
  440. }
  441. }
  442. }
  443. }
  444. else
  445. {
  446. dol_print_error($db);
  447. }
  448. }
  449. if ($fileread) $this->_tab_loaded[$newdomain]=1; // Set domain file as loaded
  450. if (empty($this->_tab_loaded[$newdomain])) $this->_tab_loaded[$newdomain]=2; // Marque ce cas comme non trouve (no lines found for language)
  451. return 1;
  452. }
  453. /**
  454. * Return translated value of key for special keys ("Currency...", "Civility...", ...).
  455. * Search in lang file, then into database. Key must be any complete entry into lang file: CurrencyEUR, ...
  456. * If not found, return key.
  457. * The string return is not formated (translated with transnoentitiesnoconv)
  458. * NOTE: To avoid infinite loop (getLabelFromKey->transnoentities->getTradFromKey), if you modify this function,
  459. * check that getLabelFromKey is not called with same value than input.
  460. *
  461. * @param string $key Key to translate
  462. * @return string Translated string (translated with transnoentitiesnoconv)
  463. */
  464. private function getTradFromKey($key)
  465. {
  466. global $conf, $db;
  467. if (! is_string($key)) return 'ErrorBadValueForParamNotAString'; // Avoid multiple errors with code not using function correctly.
  468. $newstr=$key;
  469. if (preg_match('/^Civility([0-9A-Z]+)$/i',$key,$reg))
  470. {
  471. $newstr=$this->getLabelFromKey($db,$reg[1],'c_civility','code','label');
  472. }
  473. elseif (preg_match('/^Currency([A-Z][A-Z][A-Z])$/i',$key,$reg))
  474. {
  475. $newstr=$this->getLabelFromKey($db,$reg[1],'c_currencies','code_iso','label');
  476. }
  477. elseif (preg_match('/^SendingMethod([0-9A-Z]+)$/i',$key,$reg))
  478. {
  479. $newstr=$this->getLabelFromKey($db,$reg[1],'c_shipment_mode','code','libelle');
  480. }
  481. elseif (preg_match('/^PaymentTypeShort([0-9A-Z]+)$/i',$key,$reg))
  482. {
  483. $newstr=$this->getLabelFromKey($db,$reg[1],'c_paiement','code','libelle','',1);
  484. }
  485. elseif (preg_match('/^OppStatus([0-9A-Z]+)$/i',$key,$reg))
  486. {
  487. $newstr=$this->getLabelFromKey($db,$reg[1],'c_lead_status','code','label');
  488. }
  489. elseif (preg_match('/^OrderSource([0-9A-Z]+)$/i',$key,$reg))
  490. {
  491. // TODO OrderSourceX must be replaced with content of table llx_c_input_reason or llx_c_input_method
  492. //$newstr=$this->getLabelFromKey($db,$reg[1],'c_ordersource','code','label');
  493. }
  494. /* Disabled. There is too many cases where translation of $newstr is not defined is normal (like when output with setEventMessage an already translated string)
  495. if (! empty($conf->global->MAIN_FEATURES_LEVEL) && $conf->global->MAIN_FEATURES_LEVEL >= 2)
  496. {
  497. dol_syslog(__METHOD__." MAIN_FEATURES_LEVEL=DEVELOP: missing translation for key '".$newstr."' in ".$_SERVER["PHP_SELF"], LOG_DEBUG);
  498. }*/
  499. return $newstr;
  500. }
  501. /**
  502. * Return text translated of text received as parameter (and encode it into HTML)
  503. * Si il n'y a pas de correspondance pour ce texte, on cherche dans fichier alternatif
  504. * et si toujours pas trouve, il est retourne tel quel
  505. * Les parametres de cette methode peuvent contenir de balises HTML.
  506. *
  507. * @param string $key Key to translate
  508. * @param string $param1 chaine de param1
  509. * @param string $param2 chaine de param2
  510. * @param string $param3 chaine de param3
  511. * @param string $param4 chaine de param4
  512. * @param int $maxsize Max length of text
  513. * @return string Translated string (encoded into HTML entities and UTF8)
  514. */
  515. function trans($key, $param1='', $param2='', $param3='', $param4='', $maxsize=0)
  516. {
  517. global $conf;
  518. if (! empty($this->tab_translate[$key])) // Translation is available
  519. {
  520. $str=$this->tab_translate[$key];
  521. // Make some string replacement after translation
  522. $replacekey='MAIN_REPLACE_TRANS_'.$this->defaultlang;
  523. if (! empty($conf->global->$replacekey)) // Replacement translation variable with string1:newstring1;string2:newstring2
  524. {
  525. $tmparray=explode(';', $conf->global->$replacekey);
  526. foreach($tmparray as $tmp)
  527. {
  528. $tmparray2=explode(':',$tmp);
  529. $str=preg_replace('/'.preg_quote($tmparray2[0]).'/',$tmparray2[1],$str);
  530. }
  531. }
  532. if (! preg_match('/^Format/',$key))
  533. {
  534. //print $str;
  535. $str=sprintf($str,$param1,$param2,$param3,$param4); // Replace %s and %d except for FormatXXX strings.
  536. }
  537. if ($maxsize) $str=dol_trunc($str,$maxsize);
  538. // We replace some HTML tags by __xx__ to avoid having them encoded by htmlentities
  539. $str=str_replace(array('<','>','"',),array('__lt__','__gt__','__quot__'),$str);
  540. // Crypt string into HTML
  541. $str=htmlentities($str, ENT_COMPAT, $this->charset_output); // Do not convert simple quotes in translation (strings in html are enmbraced by "). Use dol_escape_htmltag around text in HTML content
  542. // Restore HTML tags
  543. $str=str_replace(array('__lt__','__gt__','__quot__'),array('<','>','"',),$str);
  544. return $str;
  545. }
  546. else // Translation is not available
  547. {
  548. //if ($key[0] == '$') { return dol_eval($key,1); }
  549. return $this->getTradFromKey($key);
  550. }
  551. }
  552. /**
  553. * Return translated value of a text string
  554. * Si il n'y a pas de correspondance pour ce texte, on cherche dans fichier alternatif
  555. * et si toujours pas trouve, il est retourne tel quel.
  556. * Parameters of this method must not contains any HTML tags.
  557. *
  558. * @param string $key Key to translate
  559. * @param string $param1 chaine de param1
  560. * @param string $param2 chaine de param2
  561. * @param string $param3 chaine de param3
  562. * @param string $param4 chaine de param4
  563. * @param string $param5 chaine de param5
  564. * @return string Translated string (encoded into UTF8)
  565. */
  566. function transnoentities($key, $param1='', $param2='', $param3='', $param4='', $param5='')
  567. {
  568. return $this->convToOutputCharset($this->transnoentitiesnoconv($key, $param1, $param2, $param3, $param4, $param5));
  569. }
  570. /**
  571. * Return translated value of a text string
  572. * Si il n'y a pas de correspondance pour ce texte, on cherche dans fichier alternatif
  573. * et si toujours pas trouve, il est retourne tel quel.
  574. * No convert to encoding charset of lang object is done.
  575. * Parameters of this method must not contains any HTML tags.
  576. *
  577. * @param string $key Key to translate
  578. * @param string $param1 chaine de param1
  579. * @param string $param2 chaine de param2
  580. * @param string $param3 chaine de param3
  581. * @param string $param4 chaine de param4
  582. * @param string $param5 chaine de param5
  583. * @return string Translated string
  584. */
  585. function transnoentitiesnoconv($key, $param1='', $param2='', $param3='', $param4='', $param5='')
  586. {
  587. global $conf;
  588. if (! empty($this->tab_translate[$key])) // Translation is available
  589. {
  590. $str=$this->tab_translate[$key];
  591. // Make some string replacement after translation
  592. $replacekey='MAIN_REPLACE_TRANS_'.$this->defaultlang;
  593. if (! empty($conf->global->$replacekey)) // Replacement translation variable with string1:newstring1;string2:newstring2
  594. {
  595. $tmparray=explode(';', $conf->global->$replacekey);
  596. foreach($tmparray as $tmp)
  597. {
  598. $tmparray2=explode(':',$tmp);
  599. $str=preg_replace('/'.preg_quote($tmparray2[0]).'/',$tmparray2[1],$str);
  600. }
  601. }
  602. if (! preg_match('/^Format/',$key))
  603. {
  604. //print $str;
  605. $str=sprintf($str, $param1, $param2, $param3, $param4, $param5); // Replace %s and %d except for FormatXXX strings.
  606. }
  607. return $str;
  608. }
  609. else
  610. {
  611. if ($key[0] == '$') { return dol_eval($key,1); }
  612. return $this->getTradFromKey($key);
  613. }
  614. }
  615. /**
  616. * Return translation of a key depending on country
  617. *
  618. * @param string $str string root to translate
  619. * @param string $countrycode country code (FR, ...)
  620. * @return string translated string
  621. */
  622. function transcountry($str, $countrycode)
  623. {
  624. if ($this->tab_translate["$str$countrycode"]) return $this->trans("$str$countrycode");
  625. else return $this->trans($str);
  626. }
  627. /**
  628. * Retourne la version traduite du texte passe en parametre complete du code pays
  629. *
  630. * @param string $str string root to translate
  631. * @param string $countrycode country code (FR, ...)
  632. * @return string translated string
  633. */
  634. function transcountrynoentities($str, $countrycode)
  635. {
  636. if ($this->tab_translate["$str$countrycode"]) return $this->transnoentities("$str$countrycode");
  637. else return $this->transnoentities($str);
  638. }
  639. /**
  640. * Convert a string into output charset (this->charset_output that should be defined to conf->file->character_set_client)
  641. *
  642. * @param string $str String to convert
  643. * @param string $pagecodefrom Page code of src string
  644. * @return string Converted string
  645. */
  646. function convToOutputCharset($str,$pagecodefrom='UTF-8')
  647. {
  648. if ($pagecodefrom == 'ISO-8859-1' && $this->charset_output == 'UTF-8') $str=utf8_encode($str);
  649. if ($pagecodefrom == 'UTF-8' && $this->charset_output == 'ISO-8859-1') $str=utf8_decode(str_replace('€',chr(128),$str));
  650. return $str;
  651. }
  652. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.NotCamelCaps
  653. /**
  654. * Return list of all available languages
  655. *
  656. * @param string $langdir Directory to scan
  657. * @param integer $maxlength Max length for each value in combo box (will be truncated)
  658. * @param int $usecode 1=Show code instead of country name for language variant, 2=Show only code
  659. * @return array List of languages
  660. */
  661. function get_available_languages($langdir=DOL_DOCUMENT_ROOT,$maxlength=0,$usecode=0)
  662. {
  663. // phpcs:enable
  664. global $conf;
  665. // We scan directory langs to detect available languages
  666. $handle=opendir($langdir."/langs");
  667. $langs_available=array();
  668. while ($dir = trim(readdir($handle)))
  669. {
  670. if (preg_match('/^[a-z]+_[A-Z]+/i',$dir))
  671. {
  672. $this->load("languages");
  673. if ($usecode == 2)
  674. {
  675. $langs_available[$dir] = $dir;
  676. }
  677. if ($usecode == 1 || ! empty($conf->global->MAIN_SHOW_LANGUAGE_CODE))
  678. {
  679. $langs_available[$dir] = $dir.': '.dol_trunc($this->trans('Language_'.$dir),$maxlength);
  680. }
  681. else
  682. {
  683. $langs_available[$dir] = $this->trans('Language_'.$dir);
  684. }
  685. }
  686. }
  687. return $langs_available;
  688. }
  689. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.NotCamelCaps
  690. /**
  691. * Return if a filename $filename exists for current language (or alternate language)
  692. *
  693. * @param string $filename Language filename to search
  694. * @param integer $searchalt Search also alernate language file
  695. * @return boolean true if exists and readable
  696. */
  697. function file_exists($filename,$searchalt=0)
  698. {
  699. // phpcs:enable
  700. // Test si fichier dans repertoire de la langue
  701. foreach($this->dir as $searchdir)
  702. {
  703. if (is_readable(dol_osencode($searchdir."/langs/".$this->defaultlang."/".$filename))) return true;
  704. if ($searchalt)
  705. {
  706. // Test si fichier dans repertoire de la langue alternative
  707. if ($this->defaultlang != "en_US") $filenamealt = $searchdir."/langs/en_US/".$filename;
  708. //else $filenamealt = $searchdir."/langs/fr_FR/".$filename;
  709. if (is_readable(dol_osencode($filenamealt))) return true;
  710. }
  711. }
  712. return false;
  713. }
  714. /**
  715. * Return full text translated to language label for a key. Store key-label in a cache.
  716. * This function need module "numberwords" to be installed. If not it will return
  717. * same number (this module is not provided by default as it use non GPL source code).
  718. *
  719. * @param int $number Number to encode in full text
  720. * @param int $isamount 1=It's an amount, 0=it's just a number
  721. * @return string Label translated in UTF8 (but without entities)
  722. * 10 if setDefaultLang was en_US => ten
  723. * 123 if setDefaultLang was fr_FR => cent vingt trois
  724. */
  725. function getLabelFromNumber($number,$isamount=0)
  726. {
  727. global $conf;
  728. $newnumber=$number;
  729. $dirsubstitutions=array_merge(array(),$conf->modules_parts['substitutions']);
  730. foreach($dirsubstitutions as $reldir)
  731. {
  732. $dir=dol_buildpath($reldir,0);
  733. $newdir=dol_osencode($dir);
  734. // Check if directory exists
  735. if (! is_dir($newdir)) continue; // We must not use dol_is_dir here, function may not be loaded
  736. $fonc='numberwords';
  737. if (file_exists($newdir.'/functions_'.$fonc.'.lib.php'))
  738. {
  739. include_once $newdir.'/functions_'.$fonc.'.lib.php';
  740. $newnumber=numberwords_getLabelFromNumber($this,$number,$isamount);
  741. break;
  742. }
  743. }
  744. return $newnumber;
  745. }
  746. /**
  747. * Return a label for a key.
  748. * Search into translation array, then into cache, then if still not found, search into database.
  749. * Store key-label found into cache variable $this->cache_labels to save SQL requests to get labels.
  750. *
  751. * @param DoliDB $db Database handler
  752. * @param string $key Translation key to get label (key in language file)
  753. * @param string $tablename Table name without prefix
  754. * @param string $fieldkey Field for key
  755. * @param string $fieldlabel Field for label
  756. * @param string $keyforselect Use another value than the translation key for the where into select
  757. * @param int $filteronentity Use a filter on entity
  758. * @return string Label in UTF8 (but without entities)
  759. * @see dol_getIdFromCode
  760. */
  761. function getLabelFromKey($db,$key,$tablename,$fieldkey,$fieldlabel,$keyforselect='',$filteronentity=0)
  762. {
  763. // If key empty
  764. if ($key == '') return '';
  765. //print 'param: '.$key.'-'.$keydatabase.'-'.$this->trans($key); exit;
  766. // Check if a translation is available (this can call getTradFromKey)
  767. $tmp=$this->transnoentitiesnoconv($key);
  768. if ($tmp != $key && $tmp != 'ErrorBadValueForParamNotAString')
  769. {
  770. return $tmp; // Found in language array
  771. }
  772. // Check in cache
  773. if (isset($this->cache_labels[$tablename][$key])) // Can be defined to 0 or ''
  774. {
  775. return $this->cache_labels[$tablename][$key]; // Found in cache
  776. }
  777. $sql = "SELECT ".$fieldlabel." as label";
  778. $sql.= " FROM ".MAIN_DB_PREFIX.$tablename;
  779. $sql.= " WHERE ".$fieldkey." = '".$db->escape($keyforselect?$keyforselect:$key)."'";
  780. if ($filteronentity) $sql.= " AND entity IN (" . getEntity($tablename). ')';
  781. dol_syslog(get_class($this).'::getLabelFromKey', LOG_DEBUG);
  782. $resql = $db->query($sql);
  783. if ($resql)
  784. {
  785. $obj = $db->fetch_object($resql);
  786. if ($obj) $this->cache_labels[$tablename][$key]=$obj->label;
  787. else $this->cache_labels[$tablename][$key]=$key;
  788. $db->free($resql);
  789. return $this->cache_labels[$tablename][$key];
  790. }
  791. else
  792. {
  793. $this->error=$db->lasterror();
  794. return -1;
  795. }
  796. }
  797. /**
  798. * Return a currency code into its symbol
  799. *
  800. * @param string $currency_code Currency Code
  801. * @param string $amount If not '', show currency + amount according to langs ($10, 10€).
  802. * @return string Amount + Currency symbol encoded into UTF8
  803. * @deprecated Use method price to output a price
  804. * @see price()
  805. */
  806. function getCurrencyAmount($currency_code, $amount)
  807. {
  808. $symbol=$this->getCurrencySymbol($currency_code);
  809. if (in_array($currency_code, array('USD'))) return $symbol.$amount;
  810. else return $amount.$symbol;
  811. }
  812. /**
  813. * Return a currency code into its symbol.
  814. * If mb_convert_encoding is not available, return currency code.
  815. *
  816. * @param string $currency_code Currency code
  817. * @param integer $forceloadall 1=Force to load all currencies into cache. We know we need to use all of them. By default read and cache only required currency.
  818. * @return string Currency symbol encoded into UTF8
  819. */
  820. function getCurrencySymbol($currency_code, $forceloadall=0)
  821. {
  822. $currency_sign = ''; // By default return iso code
  823. if (function_exists("mb_convert_encoding"))
  824. {
  825. $this->loadCacheCurrencies($forceloadall?'':$currency_code);
  826. if (isset($this->cache_currencies[$currency_code]) && ! empty($this->cache_currencies[$currency_code]['unicode']) && is_array($this->cache_currencies[$currency_code]['unicode']))
  827. {
  828. foreach($this->cache_currencies[$currency_code]['unicode'] as $unicode)
  829. {
  830. $currency_sign .= mb_convert_encoding("&#{$unicode};", "UTF-8", 'HTML-ENTITIES');
  831. }
  832. }
  833. }
  834. return ($currency_sign?$currency_sign:$currency_code);
  835. }
  836. /**
  837. * Load into the cache this->cache_currencies, all currencies
  838. *
  839. * @param string $currency_code Get only currency. Get all if ''.
  840. * @return int Nb of loaded lines, 0 if already loaded, <0 if KO
  841. */
  842. public function loadCacheCurrencies($currency_code)
  843. {
  844. global $db;
  845. if ($this->cache_currencies_all_loaded) return 0; // Cache already loaded for all
  846. if (! empty($currency_code) && isset($this->cache_currencies[$currency_code])) return 0; // Cache already loaded for the currency
  847. $sql = "SELECT code_iso, label, unicode";
  848. $sql.= " FROM ".MAIN_DB_PREFIX."c_currencies";
  849. $sql.= " WHERE active = 1";
  850. if (! empty($currency_code)) $sql.=" AND code_iso = '".$db->escape($currency_code)."'";
  851. //$sql.= " ORDER BY code_iso ASC"; // Not required, a sort is done later
  852. dol_syslog(get_class($this).'::loadCacheCurrencies', LOG_DEBUG);
  853. $resql = $db->query($sql);
  854. if ($resql)
  855. {
  856. $this->load("dict");
  857. $label=array();
  858. if (! empty($currency_code)) foreach($this->cache_currencies as $key => $val) $label[$key]=$val['label']; // Label in already loaded cache
  859. $num = $db->num_rows($resql);
  860. $i = 0;
  861. while ($i < $num)
  862. {
  863. $obj = $db->fetch_object($resql);
  864. // Si traduction existe, on l'utilise, sinon on prend le libelle par defaut
  865. $this->cache_currencies[$obj->code_iso]['label'] = ($obj->code_iso && $this->trans("Currency".$obj->code_iso)!="Currency".$obj->code_iso?$this->trans("Currency".$obj->code_iso):($obj->label!='-'?$obj->label:''));
  866. $this->cache_currencies[$obj->code_iso]['unicode'] = (array) json_decode($obj->unicode, true);
  867. $label[$obj->code_iso] = $this->cache_currencies[$obj->code_iso]['label'];
  868. $i++;
  869. }
  870. if (empty($currency_code)) $this->cache_currencies_all_loaded=true;
  871. //print count($label).' '.count($this->cache_currencies);
  872. // Resort cache
  873. array_multisort($label, SORT_ASC, $this->cache_currencies);
  874. //var_dump($this->cache_currencies); $this->cache_currencies is now sorted onto label
  875. return $num;
  876. }
  877. else
  878. {
  879. dol_print_error($db);
  880. return -1;
  881. }
  882. }
  883. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.NotCamelCaps
  884. /**
  885. * Return an array with content of all loaded translation keys (found into this->tab_translate) so
  886. * we get a substitution array we can use for substitutions (for mail or ODT generation for example)
  887. *
  888. * @return array Array of translation keys lang_key => string_translation_loaded
  889. */
  890. function get_translations_for_substitutions()
  891. {
  892. // phpcs:enable
  893. $substitutionarray = array();
  894. foreach($this->tab_translate as $code => $label) {
  895. $substitutionarray['lang_'.$code] = $label;
  896. $substitutionarray['__('.$code.')__'] = $label;
  897. }
  898. return $substitutionarray;
  899. }
  900. }