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