1<?php 2// Copyright (C) 2010-2016 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop 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 Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19/** 20 * Class Dict 21 * Management of localizable strings 22 * 23 * @copyright Copyright (C) 2010-2018 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27class DictException extends CoreException 28{ 29} 30 31class DictExceptionUnknownLanguage extends DictException 32{ 33 public function __construct($sLanguageCode) 34 { 35 $aContext = array(); 36 $aContext['language_code'] = $sLanguageCode; 37 parent::__construct('Unknown localization language', $aContext); 38 } 39} 40 41class DictExceptionMissingString extends DictException 42{ 43 public function __construct($sLanguageCode, $sStringCode) 44 { 45 $aContext = array(); 46 $aContext['language_code'] = $sLanguageCode; 47 $aContext['string_code'] = $sStringCode; 48 parent::__construct('Missing localized string', $aContext); 49 } 50} 51 52 53define('DICT_ERR_STRING', 1); // when a string is missing, return the identifier 54define('DICT_ERR_EXCEPTION', 2); // when a string is missing, throw an exception 55//define('DICT_ERR_LOG', 3); // when a string is missing, log an error 56 57 58class Dict 59{ 60 protected static $m_iErrorMode = DICT_ERR_STRING; 61 protected static $m_sDefaultLanguage = 'EN US'; 62 protected static $m_sCurrentLanguage = null; // No language selected by default 63 64 protected static $m_aLanguages = array(); // array( code => array( 'description' => '...', 'localized_description' => '...') ...) 65 protected static $m_aData = array(); 66 protected static $m_sApplicationPrefix = null; 67 68 /** 69 * @param $sLanguageCode 70 * 71 * @throws \DictExceptionUnknownLanguage 72 */ 73 public static function SetDefaultLanguage($sLanguageCode) 74 { 75 if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) 76 { 77 throw new DictExceptionUnknownLanguage($sLanguageCode); 78 } 79 self::$m_sDefaultLanguage = $sLanguageCode; 80 } 81 82 /** 83 * @param $sLanguageCode 84 * 85 * @throws \DictExceptionUnknownLanguage 86 */ 87 public static function SetUserLanguage($sLanguageCode) 88 { 89 if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) 90 { 91 throw new DictExceptionUnknownLanguage($sLanguageCode); 92 } 93 self::$m_sCurrentLanguage = $sLanguageCode; 94 } 95 96 97 public static function GetUserLanguage() 98 { 99 if (self::$m_sCurrentLanguage == null) // May happen when no user is logged in (i.e login screen, non authentifed page) 100 { 101 // In which case let's use the default language 102 return self::$m_sDefaultLanguage; 103 } 104 return self::$m_sCurrentLanguage; 105 } 106 107 //returns a hash array( code => array( 'description' => '...', 'localized_description' => '...') ...) 108 public static function GetLanguages() 109 { 110 return self::$m_aLanguages; 111 } 112 113 // iErrorMode from {DICT_ERR_STRING, DICT_ERR_EXCEPTION} 114 public static function SetErrorMode($iErrorMode) 115 { 116 self::$m_iErrorMode = $iErrorMode; 117 } 118 119 /** 120 * Check if a dictionary entry exists or not 121 * @param $sStringCode 122 * 123 * @return bool 124 */ 125 public static function Exists($sStringCode) 126 { 127 $sImpossibleString = 'aVlHYKEI3TZuDV5o0pghv7fvhYNYuzYkTk7WL0Zoqw8rggE7aq'; 128 if (static::S($sStringCode, $sImpossibleString) === $sImpossibleString) 129 { 130 return false; 131 } 132 return true; 133 } 134 135 /** 136 * Returns a localised string from the dictonary 137 * 138 * @param string $sStringCode The code identifying the dictionary entry 139 * @param string $sDefault Default value if there is no match in the dictionary 140 * @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise 141 * 142 * @return string 143 */ 144 public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false) 145 { 146 // Attempt to find the string in the user language 147 // 148 self::InitLangIfNeeded(self::GetUserLanguage()); 149 150 if (!array_key_exists(self::GetUserLanguage(), self::$m_aData)) 151 { 152 // It may happen, when something happens before the dictionaries get loaded 153 return $sStringCode; 154 } 155 $aCurrentDictionary = self::$m_aData[self::GetUserLanguage()]; 156 if (array_key_exists($sStringCode, $aCurrentDictionary)) 157 { 158 return $aCurrentDictionary[$sStringCode]; 159 } 160 if (!$bUserLanguageOnly) 161 { 162 // Attempt to find the string in the default language 163 // 164 self::InitLangIfNeeded(self::$m_sDefaultLanguage); 165 166 $aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage]; 167 if (array_key_exists($sStringCode, $aDefaultDictionary)) 168 { 169 return $aDefaultDictionary[$sStringCode]; 170 } 171 // Attempt to find the string in english 172 // 173 self::InitLangIfNeeded('EN US'); 174 175 $aDefaultDictionary = self::$m_aData['EN US']; 176 if (array_key_exists($sStringCode, $aDefaultDictionary)) 177 { 178 return $aDefaultDictionary[$sStringCode]; 179 } 180 } 181 // Could not find the string... 182 // 183 if (is_null($sDefault)) 184 { 185 return $sStringCode; 186 } 187 188 return $sDefault; 189 } 190 191 192 /** 193 * Formats a localized string with numbered placeholders (%1$s...) for the additional arguments 194 * See vsprintf for more information about the syntax of the placeholders 195 * @param string $sFormatCode 196 * @return string 197 */ 198 public static function Format($sFormatCode /*, ... arguments ....*/) 199 { 200 $sLocalizedFormat = self::S($sFormatCode); 201 $aArguments = func_get_args(); 202 array_shift($aArguments); 203 204 if ($sLocalizedFormat == $sFormatCode) 205 { 206 // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) 207 return $sFormatCode.' - '.implode(', ', $aArguments); 208 } 209 210 return vsprintf($sLocalizedFormat, $aArguments); 211 } 212 213 /** 214 * Initialize a the entries for a given language (replaces the former Add() method) 215 * @param string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US' 216 * @param array $aEntries Hash array of dictionnary entries 217 */ 218 public static function SetEntries($sLanguageCode, $aEntries) 219 { 220 self::$m_aData[$sLanguageCode] = $aEntries; 221 } 222 223 /** 224 * Set the list of available languages 225 * @param hash $aLanguagesList 226 */ 227 public static function SetLanguagesList($aLanguagesList) 228 { 229 self::$m_aLanguages = $aLanguagesList; 230 } 231 232 /** 233 * Load a language from the language dictionary, if not already loaded 234 * @param string $sLangCode Language code 235 * @return boolean 236 */ 237 public static function InitLangIfNeeded($sLangCode) 238 { 239 if (array_key_exists($sLangCode, self::$m_aData)) return true; 240 241 $bResult = false; 242 243 if (function_exists('apc_fetch') && (self::$m_sApplicationPrefix !== null)) 244 { 245 // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter 246 // 247 self::$m_aData[$sLangCode] = apc_fetch(self::$m_sApplicationPrefix.'-dict-'.$sLangCode); 248 if (self::$m_aData[$sLangCode] === false) 249 { 250 unset(self::$m_aData[$sLangCode]); 251 } 252 else 253 { 254 $bResult = true; 255 } 256 } 257 if (!$bResult) 258 { 259 $sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php'; 260 require_once($sDictFile); 261 262 if (function_exists('apc_store') && (self::$m_sApplicationPrefix !== null)) 263 { 264 apc_store(self::$m_sApplicationPrefix.'-dict-'.$sLangCode, self::$m_aData[$sLangCode]); 265 } 266 $bResult = true; 267 } 268 return $bResult; 269 } 270 271 /** 272 * Enable caching (cached using APC) 273 * @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance 274 */ 275 public static function EnableCache($sApplicationPrefix) 276 { 277 self::$m_sApplicationPrefix = $sApplicationPrefix; 278 } 279 280 /** 281 * Reset the cached entries (cached using APC) 282 * @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance 283 */ 284 public static function ResetCache($sApplicationPrefix) 285 { 286 if (function_exists('apc_delete')) 287 { 288 foreach(self::$m_aLanguages as $sLang => $void) 289 { 290 apc_delete($sApplicationPrefix.'-dict-'.$sLang); 291 } 292 } 293 } 294 295 ///////////////////////////////////////////////////////////////////////// 296 297 298 /** 299 * Clone a string in every language (if it exists in that language) 300 * 301 * @param $sSourceCode 302 * @param $sDestCode 303 */ 304 public static function CloneString($sSourceCode, $sDestCode) 305 { 306 foreach(self::$m_aLanguages as $sLanguageCode => $foo) 307 { 308 if (isset(self::$m_aData[$sLanguageCode][$sSourceCode])) 309 { 310 self::$m_aData[$sLanguageCode][$sDestCode] = self::$m_aData[$sLanguageCode][$sSourceCode]; 311 } 312 } 313 } 314 315 public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US') 316 { 317 $aMissing = array(); // Strings missing for the target language 318 $aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary 319 $aNotTranslated = array(); // Strings having the same value in both dictionaries 320 $aOK = array(); // Strings having different values in both dictionaries 321 322 foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue) 323 { 324 if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode])) 325 { 326 $aMissing[$sStringCode] = $sValue; 327 } 328 } 329 330 foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue) 331 { 332 if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef])) 333 { 334 $aUnexpected[$sStringCode] = $sValue; 335 } 336 else 337 { 338 // The value exists in the reference 339 $sRefValue = self::$m_aData[$sLanguageRef][$sStringCode]; 340 if ($sValue == $sRefValue) 341 { 342 $aNotTranslated[$sStringCode] = $sValue; 343 } 344 else 345 { 346 $aOK[$sStringCode] = $sValue; 347 } 348 } 349 } 350 return array($aMissing, $aUnexpected, $aNotTranslated, $aOK); 351 } 352 353 public static function Dump() 354 { 355 MyHelpers::var_dump_html(self::$m_aData); 356 } 357 358 // Only used by the setup to determine the list of languages to display in the initial setup screen 359 // otherwise replaced by LoadModule by its own handler 360 // sLanguageCode: Code identifying the language i.e. FR-FR 361 // sEnglishLanguageDesc: Description of the language code, in English. i.e. French (France) 362 // sLocalizedLanguageDesc: Description of the language code, in its own language. i.e. Français (France) 363 // aEntries: Hash array of dictionnary entries 364 // ~~ or ~* can be used to indicate entries still to be translated. 365 public static function Add($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries) 366 { 367 if (!array_key_exists($sLanguageCode, self::$m_aLanguages)) 368 { 369 self::$m_aLanguages[$sLanguageCode] = array('description' => $sEnglishLanguageDesc, 'localized_description' => $sLocalizedLanguageDesc); 370 self::$m_aData[$sLanguageCode] = array(); 371 } 372 // No need to actually load the strings since it's only used to know the list of languages 373 // at setup time !! 374 } 375 376 /** 377 * Export all the dictionary entries - of the given language - whose code matches the given prefix 378 * missing entries in the current language will be replaced by entries in the default language 379 * @param string $sStartingWith 380 * @return string[] 381 */ 382 public static function ExportEntries($sStartingWith) 383 { 384 self::InitLangIfNeeded(self::GetUserLanguage()); 385 self::InitLangIfNeeded(self::$m_sDefaultLanguage); 386 $aEntries = array(); 387 $iLength = strlen($sStartingWith); 388 389 // First prefill the array with entries from the default language 390 foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry) 391 { 392 if (substr($sCode, 0, $iLength) == $sStartingWith) 393 { 394 $aEntries[$sCode] = $sEntry; 395 } 396 } 397 398 // Now put (overwrite) the entries for the user language 399 foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry) 400 { 401 if (substr($sCode, 0, $iLength) == $sStartingWith) 402 { 403 $aEntries[$sCode] = $sEntry; 404 } 405 } 406 return $aEntries; 407 } 408} 409?> 410