Browse Source

Cleanup and optimization of the handling/loading of the dictionary files.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3978 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 9 năm trước cách đây
mục cha
commit
4d54906d6d

+ 1 - 57
core/config.class.inc.php

@@ -77,7 +77,6 @@ class Config
 	protected $m_aDataModels;
 	protected $m_aWebServiceCategories;
 	protected $m_aAddons;
-	protected $m_aDictionaries;
 
 	protected $m_aModuleSettings;
 
@@ -1053,7 +1052,6 @@ class Config
 			// Default AddOn, always present can be moved to an official iTop Module later if needed
 			'user rights' => 'addons/userrights/userrightsprofile.class.inc.php',
 		);
-		$this->m_aDictionaries = self::ScanDictionariesDir();
 		
 		foreach($this->m_aSettings as $sPropCode => $aSettingInfo)
 		{
@@ -1173,10 +1171,7 @@ class Config
 			// Add one, by default
 			$MyModules['addons']['user rights'] = '/addons/userrights/userrightsnull.class.inc.php';
 		}
-		if (!array_key_exists('dictionaries', $MyModules))
-		{
-			throw new ConfigException('Missing item in configuration file', array('file' => $sConfigFile, 'expected' => '$MyModules[\'dictionaries\']'));
-		}
+
 		$this->m_aAppModules = $MyModules['application'];
 		$this->m_aDataModels = $MyModules['business'];
 		if (isset($MyModules['webservices']))
@@ -1184,7 +1179,6 @@ class Config
 			$this->m_aWebServiceCategories = $MyModules['webservices'];
 		}
 		$this->m_aAddons = $MyModules['addons'];
-		$this->m_aDictionaries = $MyModules['dictionaries'];
 
 		foreach($MySettings as $sPropCode => $rawvalue)
 		{
@@ -1304,15 +1298,6 @@ class Config
 		$this->m_aAddons = $aAddons;
 	}
 
-	public function GetDictionaries()
-	{
-		return $this->m_aDictionaries;
-	}
-	public function SetDictionaries($aDictionaries)
-	{
-		$this->m_aDictionaries = $aDictionaries;
-	}
-
 	public function GetDBHost()
 	{
 		return $this->m_sDBHost;
@@ -1608,10 +1593,6 @@ class Config
 		{
 			$aSettings['addon_list'][] = $sFile;
 		}
-		foreach($this->m_aDictionaries as $sFile)
-		{
-			$aSettings['dictionary_list'][] = $sFile;
-		}
 		return $aSettings;
 	}
 
@@ -1780,12 +1761,6 @@ class Config
 				fwrite($hFile, "\t\t'$sKey' => '$sFile',\n");
 			}
 			fwrite($hFile, "\t),\n");
-			fwrite($hFile, "\t'dictionaries' => array (\n");
-			foreach($this->m_aDictionaries as $sFile)
-			{
-				fwrite($hFile, "\t\t'$sFile',\n");
-			}
-			fwrite($hFile, "\t),\n");
 			fwrite($hFile, ");\n");
 			fwrite($hFile, '?'.'>'); // Avoid perturbing the syntax highlighting !
 			return fclose($hFile);
@@ -1795,26 +1770,6 @@ class Config
 			throw new ConfigException("Could not write to configuration file", array('file' => $sFileName));
 		}
 	}
-	
-	protected static function ScanDictionariesDir()
-	{
-		$aResult = array();
-		// Populate automatically the list of dictionary files
-		$sDir = APPROOT.'/dictionaries';
-		if ($hDir = @opendir($sDir))
-		{
-			while (($sFile = readdir($hDir)) !== false)
-			{
-				$aMatches = array();
-				if (preg_match("/^([^\.]+\.)?dictionary\.itop\.(ui|core)\.php$/i", $sFile, $aMatches)) // Dictionary files named like [<Lang>.]dictionary.[core|ui].php are loaded automatically
-				{
-					$aResult[] = 'dictionaries/'.$sFile;
-				}
-			}
-			closedir($hDir);
-		}
-		return $aResult;
-	}
 
 	/**
 	 * Helper function to initialize a configuration from the page arguments
@@ -1882,7 +1837,6 @@ class Config
 			}
 			$aDataModels = $oEmptyConfig->GetDataModels();
 			$aWebServiceCategories = $oEmptyConfig->GetWebServiceCategories();
-			$aDictionaries = $oEmptyConfig->GetDictionaries();
 			// Merge the values with the ones provided by the modules
 			// Make sure when don't load the same file twice...
 			
@@ -1935,15 +1889,6 @@ class Config
 			$this->SetAppModules($aAppModules);
 			$this->SetDataModels($aDataModels);
 			$this->SetWebServiceCategories($aWebServiceCategories);
-			
-			// Scan dictionaries
-			//
-			foreach (glob(APPROOT.$sModulesDir.'/dictionaries/*.dict.php') as $sFilePath)
-			{
-				$sFile = basename($sFilePath);
-				$aDictionaries[] = $sModulesDir.'/dictionaries/'.$sFile;
-			}
-			$this->SetDictionaries($aDictionaries);
 		}
 	}
 
@@ -1970,7 +1915,6 @@ class Config
 		$sNewPrefix = 'env-'.$sTargetEnv.'/';
 		self::ChangePrefix($this->m_aDataModels, $sSearchPrefix, $sNewPrefix);
 		self::ChangePrefix($this->m_aWebServiceCategories, $sSearchPrefix, $sNewPrefix);
-		self::ChangePrefix($this->m_aDictionaries, $sSearchPrefix, $sNewPrefix);
 	}
 	
 	/**

+ 112 - 89
core/dict.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -57,26 +57,13 @@ define('DICT_ERR_EXCEPTION', 2); // when a string is missing, throw an exception
 
 class Dict
 {
-	protected static $m_bTraceFiles = false;
-	protected static $m_aEntryFiles = array();
-
 	protected static $m_iErrorMode = DICT_ERR_STRING;
 	protected static $m_sDefaultLanguage = 'EN US';
 	protected static $m_sCurrentLanguage = null; // No language selected by default
 
 	protected static $m_aLanguages = array(); // array( code => array( 'description' => '...', 'localized_description' => '...') ...)
 	protected static $m_aData = array();
-
-
-	public static function EnableTraceFiles()
-	{
-		self::$m_bTraceFiles = true;
-	}
-
-	public static function GetEntryFiles()
-	{
-		return self::$m_aEntryFiles;
-	}
+	protected static $m_sApplicationPrefix = null;
 
 	public static function SetDefaultLanguage($sLanguageCode)
 	{
@@ -119,11 +106,20 @@ class Dict
 		self::$m_iErrorMode = $iErrorMode;
 	}
 
-
+	/**
+	 * Returns a localised string from the dictonary
+	 * @param string $sStringCode The code identifying the dictionary entry
+	 * @param string $sDefault Default value if there is no match in the dictionary
+	 * @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise
+	 * @throws DictExceptionMissingString
+	 * @return unknown|Ambigous <>|string
+	 */
 	public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
 	{
 		// Attempt to find the string in the user language
 		//
+		self::InitLangIfNeeded(self::GetUserLanguage());
+
 		if (!array_key_exists(self::GetUserLanguage(), self::$m_aData))
 		{
 			// It may happen, when something happens before the dictionnaries get loaded
@@ -138,6 +134,8 @@ class Dict
 		{
 			// Attempt to find the string in the default language
 			//
+			self::InitLangIfNeeded(self::$m_sDefaultLanguage);
+			
 			$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
 			if (array_key_exists($sStringCode, $aDefaultDictionary))
 			{
@@ -175,6 +173,12 @@ class Dict
 	}
 
 
+	/**
+	 * Formats a localized string with numbered placeholders (%1$s...) for the additional arguments
+	 * See vsprintf for more information about the syntax of the placeholders
+	 * @param string $sFormatCode
+	 * @return string
+	 */
 	public static function Format($sFormatCode /*, ... arguments ....*/)
 	{
 		$sLocalizedFormat = self::S($sFormatCode);
@@ -189,43 +193,95 @@ class Dict
 
 		return vsprintf($sLocalizedFormat, $aArguments);
 	}
-
-
-	// sLanguageCode: Code identifying the language i.e. FR-FR
-	// sEnglishLanguageDesc: Description of the language code, in English. i.e. French (France)
-	// sLocalizedLanguageDesc: Description of the language code, in its own language. i.e. Français (France)
-	// aEntries: Hash array of dictionnary entries
-	// ~~ or ~* can be used to indicate entries still to be translated. 
-	public static function Add($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries)
+	
+	/**
+	 * Initialize a the entries for a given language (replaces the former Add() method)
+	 * @param string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US'
+	 * @param hash $aEntries Hash array of dictionnary entries
+	 */
+	public static function SetEntries($sLanguageCode, $aEntries)
+	{
+		self::$m_aData[$sLanguageCode] = $aEntries;
+	}
+	
+	/**
+	 * Set the list of available languages
+	 * @param hash $aLanguagesList
+	 */
+	public static function SetLanguagesList($aLanguagesList)
 	{
-		if (self::$m_bTraceFiles)
+		self::$m_aLanguages = $aLanguagesList;
+	}
+	
+	/**
+	 * Load a language from the language dictionary, if not already loaded
+	 * @param string $sLangCode Language code
+	 * @return boolean
+	 */
+	public static function InitLangIfNeeded($sLangCode)
+	{
+		if (array_key_exists($sLangCode, self::$m_aData)) return true;
+		
+		$bResult = false;
+		
+		if (function_exists('apc_fetch') && (self::$m_sApplicationPrefix !== null))
 		{
-			$aBacktrace = debug_backtrace();
-			$sFile = $aBacktrace[0]["file"];
-
-			foreach($aEntries as $sKey => $sValue)
+			// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
+			//
+			self::$m_aData[$sLangCode] = apc_fetch(self::$m_sApplicationPrefix.'-dict-'.$sLangCode);
+			if (self::$m_aData[$sLangCode] === false)
+			{
+				unset(self::$m_aData[$sLangCode]);
+			}
+			else
 			{
-				self::$m_aEntryFiles[$sLanguageCode][$sKey] = array(
-					'file' => $sFile,
-					'value' => $sValue
-				);
+				$bResult = true;
 			}
 		}
-
-		if (!array_key_exists($sLanguageCode, self::$m_aLanguages))
+		if (!$bResult)
 		{
-			self::$m_aLanguages[$sLanguageCode] = array('description' => $sEnglishLanguageDesc, 'localized_description' => $sLocalizedLanguageDesc);
-			self::$m_aData[$sLanguageCode] = array();
+			$sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php';
+			require_once($sDictFile);
+			
+			if (function_exists('apc_store') && (self::$m_sApplicationPrefix !== null))
+			{
+				apc_store(self::$m_sApplicationPrefix.'-dict-'.$sLangCode, self::$m_aData[$sLangCode]);
+			}
+			$bResult = true;
 		}
-		foreach($aEntries as $sCode => $sValue)
+		return $bResult;
+	}
+	
+	/**
+	 * Enable caching (cached using APC)
+	 * @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance
+	 */
+	public static function EnableCache($sApplicationPrefix)
+	{
+		self::$m_sApplicationPrefix = $sApplicationPrefix;
+	}
+
+	/**
+	 * Reset the cached entries (cached using APC)
+	 * @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance
+	 */
+	public static function ResetCache($sApplicationPrefix)
+	{
+		if (function_exists('apc_delete'))
 		{
-			self::$m_aData[$sLanguageCode][$sCode] = self::FilterString($sValue);
+			foreach(self::$m_aLanguages as $sLang => $void)
+			{
+				apc_delete($sApplicationPrefix.'-dict-'.$sLang);
+			}
 		}
 	}
 
+	/////////////////////////////////////////////////////////////////////////
+
+
 	/**
 	 * Clone a string in every language (if it exists in that language)
-	 */	 	
+	 */
 	public static function CloneString($sSourceCode, $sDestCode)
 	{
 		foreach(self::$m_aLanguages as $sLanguageCode => $foo)
@@ -236,14 +292,14 @@ class Dict
 			}
 		}
 	}
-
+	
 	public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US')
 	{
 		$aMissing = array(); // Strings missing for the target language
 		$aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary
 		$aNotTranslated = array(); // Strings having the same value in both dictionaries
 		$aOK = array(); // Strings having different values in both dictionaries
-
+	
 		foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue)
 		{
 			if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode]))
@@ -251,7 +307,7 @@ class Dict
 				$aMissing[$sStringCode] = $sValue;
 			}
 		}
-
+	
 		foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue)
 		{
 			if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef]))
@@ -279,57 +335,24 @@ class Dict
 	{
 		MyHelpers::var_dump_html(self::$m_aData);
 	}
-	
-	public static function InCache($sApplicationPrefix)
-	{
-		if (function_exists('apc_fetch'))
-		{
-			$bResult = false;
-			// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
-			//
-			self::$m_aData = apc_fetch($sApplicationPrefix.'-dict');
-			if (is_bool(self::$m_aData) && (self::$m_aData === false))
-			{
-				self::$m_aData = array();
-			}
-			else
-			{
-				self::$m_aLanguages = apc_fetch($sApplicationPrefix.'-languages');
-				if (is_bool(self::$m_aLanguages) && (self::$m_aLanguages === false))
-				{
-					self::$m_aLanguages = array();
-				}
-				else
-				{
-					$bResult = true;
-				}
-			}
-			return $bResult;
-		}
-		return false;
-	}
-	
-	public static function InitCache($sApplicationPrefix)
+
+	// Obsolete: only used by the setup/compiler which replaces this method invocation by its own handler !!
+	// sLanguageCode: Code identifying the language i.e. FR-FR
+	// sEnglishLanguageDesc: Description of the language code, in English. i.e. French (France)
+	// sLocalizedLanguageDesc: Description of the language code, in its own language. i.e. Français (France)
+	// aEntries: Hash array of dictionnary entries
+	// ~~ or ~* can be used to indicate entries still to be translated.
+	public static function Add($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries)
 	{
-		if (function_exists('apc_store'))
+		if (!array_key_exists($sLanguageCode, self::$m_aLanguages))
 		{
-			apc_store($sApplicationPrefix.'-languages', self::$m_aLanguages);
-			apc_store($sApplicationPrefix.'-dict', self::$m_aData);
+			self::$m_aLanguages[$sLanguageCode] = array('description' => $sEnglishLanguageDesc, 'localized_description' => $sLocalizedLanguageDesc);
+			self::$m_aData[$sLanguageCode] = array();
 		}
-	}
-
-	public static function ResetCache($sApplicationPrefix)
-	{
-		if (function_exists('apc_delete'))
+		foreach($aEntries as $sCode => $sValue)
 		{
-			apc_delete($sApplicationPrefix.'-languages');
-			apc_delete($sApplicationPrefix.'-dict');
+			self::$m_aData[$sLanguageCode][$sCode] = self::FilterString($sValue);
 		}
 	}
-	
-	protected static function FilterString($s)
-	{
-		return str_replace(array('~~', '~*'), '', $s);
-	}
 }
 ?>

+ 6 - 13
core/metamodel.class.php

@@ -4219,15 +4219,13 @@ abstract class MetaModel
 		//       needed when some error occur
 		$sAppIdentity = 'itop-'.MetaModel::GetEnvironmentId();
 		$bDictInitializedFromData = false;
-		if (!self::$m_bUseAPCCache || !Dict::InCache($sAppIdentity))
+		if (self::$m_bUseAPCCache)
 		{
-			$bDictInitializedFromData = true;
-			foreach (self::$m_oConfig->GetDictionaries() as $sModule => $sToInclude)
-			{
-				self::IncludeModule('dictionaries', $sToInclude);
-			}
-		}		
-		// Set the language... after the dictionaries have been loaded!
+			Dict::EnableCache($sAppIdentity);
+		}
+		require_once(APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/languages.php');
+		
+		// Set the default language...
 		Dict::SetDefaultLanguage(self::$m_oConfig->GetDefaultLanguage());
 
 		// Romain: this is the only way I've found to cope with the fact that
@@ -4332,11 +4330,6 @@ abstract class MetaModel
 				$oKPI->ComputeAndReport('Metamodel APC (store)');
 			}
 		}
-
-		if (self::$m_bUseAPCCache && $bDictInitializedFromData)
-		{
-			Dict::InitCache($sAppIdentity);
-		}
 		
 		self::$m_sDBName = $sSource;
 		self::$m_sTablePrefix = $sTablePrefix;

+ 3 - 0
setup/applicationinstaller.class.inc.php

@@ -473,6 +473,9 @@ class ApplicationInstaller
 
 		$oFactory = new ModelFactory($aDirsToScan);
 		
+		$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
+		$oFactory->LoadModule($oDictModule);
+		
 		$sDeltaFile = APPROOT.'core/datamodel.core.xml';
 		if (file_exists($sDeltaFile))
 		{

+ 47 - 24
setup/compiler.class.inc.php

@@ -424,10 +424,7 @@ EOF;
 		}
 
 		$oDictionaries = $this->oFactory->GetNodes('dictionaries/dictionary');
-		foreach($oDictionaries as $oDictionaryNode)
-		{
-			$this->CompileDictionary($oDictionaryNode, $sTempTargetDir, $sFinalTargetDir);
-		}
+		$this->CompileDictionaries($oDictionaries, $sTempTargetDir, $sFinalTargetDir);
 
 		// Compile the branding
 		//
@@ -2084,37 +2081,63 @@ EOF;
 	return $sPHP;
 	} // function CompileUserRights
 
-	protected function CompileDictionary($oDictionaryNode, $sTempTargetDir, $sFinalTargetDir)
+	protected function CompileDictionaries($oDictionaries, $sTempTargetDir, $sFinalTargetDir)
 	{
-		$sLang = $oDictionaryNode->getAttribute('id');
-		$sEnglishLanguageDesc = $oDictionaryNode->GetChildText('english_description');
-		$sLocalizedLanguageDesc = $oDictionaryNode->GetChildText('localized_description');
-
-		$aEntriesPHP = array();
-		$oEntries = $oDictionaryNode->GetUniqueElement('entries');
-		foreach($oEntries->getElementsByTagName('entry') as $oEntry)
+		$aLanguages = array();
+		foreach($oDictionaries as $oDictionaryNode)
 		{
-			$sStringCode = $oEntry->getAttribute('id');
-			$sValue = $oEntry->GetText();
-			$aEntriesPHP[] = "\t'$sStringCode' => ".self::QuoteForPHP($sValue, true).",";
-		}
-		$sEntriesPHP = implode("\n", $aEntriesPHP);
+			$sLang = $oDictionaryNode->getAttribute('id');
+			$sEnglishLanguageDesc = $oDictionaryNode->GetChildText('english_description');
+			$sLocalizedLanguageDesc = $oDictionaryNode->GetChildText('localized_description');
+			$aLanguages[$sLang] = array('description' => $sEnglishLanguageDesc, 'localized_description' => $sLocalizedLanguageDesc);
 
-		$sEscEnglishLanguageDesc = self::QuoteForPHP($sEnglishLanguageDesc);
-		$sEscLocalizedLanguageDesc = self::QuoteForPHP($sLocalizedLanguageDesc);
-		$sPHPDict =
+			$aEntriesPHP = array();
+			$oEntries = $oDictionaryNode->GetUniqueElement('entries');
+			foreach ($oEntries->getElementsByTagName('entry') as $oEntry)
+			{
+				$sStringCode = $oEntry->getAttribute('id');
+				$sValue = $oEntry->GetText();
+				$aEntriesPHP[] = "\t'$sStringCode' => ".self::QuoteForPHP(self::FilterDictString($sValue), true).",";
+			}
+			$sEntriesPHP = implode("\n", $aEntriesPHP);
+
+			$sPHPDict =
 <<<EOF
 <?php
 //
 // Dictionary built by the compiler for the language "$sLang"
 //
-Dict::Add('$sLang', $sEscEnglishLanguageDesc, $sEscLocalizedLanguageDesc, array(
+Dict::SetEntries('$sLang', array(
 $sEntriesPHP
 ));
 EOF;
-		$sSafeLang = str_replace(' ', '-', strtolower(trim($sLang)));
-		$sDictFile = $sTempTargetDir.'/dictionaries/'.$sSafeLang.'.dict.php';
-		file_put_contents($sDictFile, $sPHPDict);
+			$sSafeLang = str_replace(' ', '-', strtolower(trim($sLang)));
+			$sDictFile = $sTempTargetDir.'/dictionaries/'.$sSafeLang.'.dict.php';
+			file_put_contents($sDictFile, $sPHPDict);
+		}
+		$sLanguagesFile = $sTempTargetDir.'/dictionaries/languages.php';
+		$sLanguagesDump = var_export($aLanguages, true);
+		$sLanguagesFileContent =
+<<<EOF
+<?php
+//
+// Dictionary index built by the compiler
+//
+Dict::SetLanguagesList(
+$sLanguagesDump
+);
+EOF;
+		
+		file_put_contents($sLanguagesFile, $sLanguagesFileContent);
+	}
+
+	protected static function FilterDictString($s)
+	{
+		if (strpos($s, '~') !== false)
+		{
+			return str_replace(array('~~', '~*'), '', $s);
+		}
+		return $s;
 	}
 
 	// Transform the file references into the corresponding filename (and create the file in the relevant directory)

+ 59 - 2
setup/modelfactory.class.inc.php

@@ -207,6 +207,54 @@ class MFCoreModule extends MFModule
 }
 
 /**
+ * MFDictModule: an optional module, consisting only of dictionaries
+ * @package ModelFactory
+ */
+class MFDictModule extends MFModule
+{
+	public function __construct($sName, $sLabel, $sRootDir)
+	{
+		$this->sId = $sName;
+
+		$this->sName = $sName;
+		$this->sVersion = '1.0';
+
+		$this->sRootDir = $sRootDir;
+		$this->sLabel = $sLabel;
+		$this->aDataModels = array();
+	}
+
+	public function GetRootDir()
+	{
+		return '';
+	}
+
+	public function GetModuleDir()
+	{
+		return '';
+	}
+	
+	public function GetDictionaryFiles()
+	{
+		$aDictionaries = array();
+		if ($hDir = opendir($this->sRootDir))
+		{
+			while (($sFile = readdir($hDir)) !== false)
+			{
+				$aMatches = array();
+				if (preg_match("/^.*dictionary\\.itop.*.php$/i", $sFile, $aMatches)) // Dictionary files are named like <Lang>.dict.<ModuleName>.php
+				{
+					$aDictionaries[] = $this->sRootDir.'/'.$sFile;
+				}
+			}
+			closedir($hDir);
+		}
+		return $aDictionaries;
+	}	
+}
+
+
+/**
  * ModelFactory: the class that manages the in-memory representation of the XML MetaModel
  * @package ModelFactory
  */
@@ -549,14 +597,23 @@ class ModelFactory
 						$oXmlEntry->appendChild($oXmlValue);
 						if (array_key_exists($sLanguageCode, $this->aDictKeys) && array_key_exists($sCode, $this->aDictKeys[$sLanguageCode]))
 						{
-							$oXmlEntries->RedefineChildNode($oXmlEntry);
+							$oMe = $this->aDictKeys[$sLanguageCode][$sCode];
+							$sFlag = $oMe->getAttribute('_alteration');
+							$oMe->parentNode->replaceChild($oXmlEntry, $oMe);
+							$sNewFlag = $sFlag;
+							if ($sFlag == '')
+							{
+								$sNewFlag = 'replaced';
+							}
+							$oXmlEntry->setAttribute('_alteration', $sNewFlag);
+								
 						}
 						else 
 						{
 							$oXmlEntry->setAttribute('_alteration', 'added');
 							$oXmlEntries->appendChild($oXmlEntry);
 						}
-						$this->aDictKeys[$sLanguageCode][$sCode] = true;
+						$this->aDictKeys[$sLanguageCode][$sCode] = $oXmlEntry;
 					}
 				}	 				
 			}

+ 3 - 0
setup/runtimeenv.class.inc.php

@@ -341,6 +341,9 @@ class RunTimeEnvironment
 
 		// Do load the required modules
 		//
+		$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
+		$aRet[] = $oDictModule;
+		
 		$oFactory = new ModelFactory($aDirsToCompile);
 		$sDeltaFile = APPROOT.'core/datamodel.core.xml';
 		if (file_exists($sDeltaFile))