Merge branch 'support/2.7' into support/3.0

This commit is contained in:
odain
2023-09-13 10:01:15 +02:00
10 changed files with 360 additions and 53 deletions

View File

@@ -3,7 +3,7 @@
// //
// This file is part of iTop. // This file is part of iTop.
// //
// iTop is free software; you can redistribute it and/or modify // iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
@@ -116,33 +116,50 @@ class Dict
* @return string * @return string
*/ */
public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false) public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
{
$aInfo = self::GetLabelAndLangCode($sStringCode, $sDefault, $bUserLanguageOnly);
return $aInfo['label'];
}
/**
* Returns a localised string from the dictonary with its associated lang code
*
* @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
*
* @return array{
* lang: string, label: string
* } with localized label string and used lang code
*/
private static function GetLabelAndLangCode($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
{ {
// Attempt to find the string in the user language // Attempt to find the string in the user language
// //
$sLangCode = self::GetUserLanguage(); $sLangCode = self::GetUserLanguage();
self::InitLangIfNeeded($sLangCode); self::InitLangIfNeeded($sLangCode);
if (!array_key_exists($sLangCode, self::$m_aData)) if (! array_key_exists($sLangCode, self::$m_aData))
{ {
IssueLog::Warning("Cannot find $sLangCode in dictionnaries. default labels displayed"); IssueLog::Warning("Cannot find $sLangCode in all registered dictionaries.");
// It may happen, when something happens before the dictionaries get loaded // It may happen, when something happens before the dictionaries get loaded
return $sStringCode; return [ 'label' => $sStringCode, 'lang' => $sLangCode ];
} }
$aCurrentDictionary = self::$m_aData[$sLangCode]; $aCurrentDictionary = self::$m_aData[$sLangCode];
if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary)) if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary))
{ {
return $aCurrentDictionary[$sStringCode]; return [ 'label' => $aCurrentDictionary[$sStringCode], 'lang' => $sLangCode ];
} }
if (!$bUserLanguageOnly) if (!$bUserLanguageOnly)
{ {
// Attempt to find the string in the default language // Attempt to find the string in the default language
// //
self::InitLangIfNeeded(self::$m_sDefaultLanguage); self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage]; $aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary)) if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{ {
return $aDefaultDictionary[$sStringCode]; return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => self::$m_sDefaultLanguage ];
} }
// Attempt to find the string in english // Attempt to find the string in english
// //
@@ -151,17 +168,17 @@ class Dict
$aDefaultDictionary = self::$m_aData['EN US']; $aDefaultDictionary = self::$m_aData['EN US'];
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary)) if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{ {
return $aDefaultDictionary[$sStringCode]; return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => 'EN US' ];
} }
} }
// Could not find the string... // Could not find the string...
// //
if (is_null($sDefault)) if (is_null($sDefault))
{ {
return $sStringCode; return [ 'label' => $sStringCode, 'lang' => null ];
} }
return $sDefault; return [ 'label' => $sDefault, 'lang' => null ];
} }
@@ -177,19 +194,25 @@ class Dict
*/ */
public static function Format($sFormatCode /*, ... arguments ....*/) public static function Format($sFormatCode /*, ... arguments ....*/)
{ {
$sLocalizedFormat = self::S($sFormatCode); ['label' => $sLocalizedFormat, 'lang' => $sLangCode] = self::GetLabelAndLangCode($sFormatCode);
$aArguments = func_get_args(); $aArguments = func_get_args();
array_shift($aArguments); array_shift($aArguments);
if ($sLocalizedFormat == $sFormatCode) if ($sLocalizedFormat == $sFormatCode)
{ {
// Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded)
return $sFormatCode.' - '.implode(', ', $aArguments); return $sFormatCode.' - '.implode(', ', $aArguments);
} }
return vsprintf($sLocalizedFormat, $aArguments); try{
return vsprintf($sLocalizedFormat, $aArguments);
} catch(\Throwable $e){
\IssueLog::Error("Cannot format dict key", null, ["sFormatCode" => $sFormatCode, "sLangCode" => $sLangCode, 'exception_msg' => $e->getMessage() ]);
return $sFormatCode.' - '.implode(', ', $aArguments);
}
} }
/** /**
* Initialize a the entries for a given language (replaces the former Add() method) * 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 string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US'
@@ -199,7 +222,7 @@ class Dict
{ {
self::$m_aData[$sLanguageCode] = $aEntries; self::$m_aData[$sLanguageCode] = $aEntries;
} }
/** /**
* Set the list of available languages * Set the list of available languages
* @param hash $aLanguagesList * @param hash $aLanguagesList
@@ -260,7 +283,7 @@ class Dict
{ {
$sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php'; $sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php';
require_once($sDictFile); require_once($sDictFile);
if (self::GetApcService()->function_exists('apc_store') if (self::GetApcService()->function_exists('apc_store')
&& (self::$m_sApplicationPrefix !== null)) && (self::$m_sApplicationPrefix !== null))
{ {
@@ -270,7 +293,7 @@ class Dict
} }
return $bResult; return $bResult;
} }
/** /**
* Enable caching (cached using APC) * Enable caching (cached using APC)
* @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance * @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance
@@ -313,14 +336,14 @@ class Dict
} }
} }
} }
public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US') public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US')
{ {
$aMissing = array(); // Strings missing for the target language $aMissing = array(); // Strings missing for the target language
$aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary $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 $aNotTranslated = array(); // Strings having the same value in both dictionaries
$aOK = array(); // Strings having different values in both dictionaries $aOK = array(); // Strings having different values in both dictionaries
foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue) foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue)
{ {
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode])) if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode]))
@@ -328,7 +351,7 @@ class Dict
$aMissing[$sStringCode] = $sValue; $aMissing[$sStringCode] = $sValue;
} }
} }
foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue) foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue)
{ {
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef])) if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef]))
@@ -351,7 +374,7 @@ class Dict
} }
return array($aMissing, $aUnexpected, $aNotTranslated, $aOK); return array($aMissing, $aUnexpected, $aNotTranslated, $aOK);
} }
public static function Dump() public static function Dump()
{ {
MyHelpers::var_dump_html(self::$m_aData); MyHelpers::var_dump_html(self::$m_aData);
@@ -374,7 +397,7 @@ class Dict
// No need to actually load the strings since it's only used to know the list of languages // No need to actually load the strings since it's only used to know the list of languages
// at setup time !! // at setup time !!
} }
/** /**
* Export all the dictionary entries - of the given language - whose code matches the given prefix * Export all the dictionary entries - of the given language - whose code matches the given prefix
* missing entries in the current language will be replaced by entries in the default language * missing entries in the current language will be replaced by entries in the default language
@@ -387,7 +410,7 @@ class Dict
self::InitLangIfNeeded(self::$m_sDefaultLanguage); self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aEntries = array(); $aEntries = array();
$iLength = strlen($sStartingWith); $iLength = strlen($sStartingWith);
// First prefill the array with entries from the default language // First prefill the array with entries from the default language
foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry) foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry)
{ {
@@ -396,7 +419,7 @@ class Dict
$aEntries[$sCode] = $sEntry; $aEntries[$sCode] = $sEntry;
} }
} }
// Now put (overwrite) the entries for the user language // Now put (overwrite) the entries for the user language
foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry) foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry)
{ {

View File

@@ -20,7 +20,7 @@
* @license http://opensource.org/licenses/AGPL-3.0 * @license http://opensource.org/licenses/AGPL-3.0
*/ */
Dict::Add('HU HU', 'Hungarian', 'Magyar', array( Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'Core:DeletedObjectLabel' => '%1s (deleted)~~', 'Core:DeletedObjectLabel' => '%1$s (deleted)~~',
'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)~~', 'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)~~',
'Core:UnknownObjectLabel' => 'Object not found (class: %1$s, id: %2$d)~~', 'Core:UnknownObjectLabel' => 'Object not found (class: %1$s, id: %2$d)~~',
@@ -204,8 +204,8 @@ Operators:<br/>
'Core:FriendlyName-Description' => 'Friendly name~~', 'Core:FriendlyName-Description' => 'Friendly name~~',
'Core:AttributeTag' => 'Tags~~', 'Core:AttributeTag' => 'Tags~~',
'Core:AttributeTag+' => '', 'Core:AttributeTag+' => 'Tags~~',
'Core:Context=REST/JSON' => 'REST~~', 'Core:Context=REST/JSON' => 'REST~~',
'Core:Context=Synchro' => 'Synchro~~', 'Core:Context=Synchro' => 'Synchro~~',
'Core:Context=Setup' => 'Setup~~', 'Core:Context=Setup' => 'Setup~~',

View File

@@ -443,7 +443,7 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:Error:2ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s és %2$s.', 'UI:Error:2ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s és %2$s.',
'UI:Error:3ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s, %2$s és %3$s.', 'UI:Error:3ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s, %2$s és %3$s.',
'UI:Error:4ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s, %2$s, %3$s és %4$s.', 'UI:Error:4ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s, %2$s, %3$s és %4$s.',
'UI:Error:IncorrectOQLQuery_Message' => 'Hiba: nem megfelelő OQL lekérdezés: %1$', 'UI:Error:IncorrectOQLQuery_Message' => 'Hiba: nem megfelelő OQL lekérdezés: %1$s',
'UI:Error:AnErrorOccuredWhileRunningTheQuery_Message' => 'Hiba történt a lekérdezs futtatása közben: %1$s', 'UI:Error:AnErrorOccuredWhileRunningTheQuery_Message' => 'Hiba történt a lekérdezs futtatása közben: %1$s',
'UI:Error:ObjectAlreadyUpdated' => 'Hiba: az objketum már korábban módosításra került.', 'UI:Error:ObjectAlreadyUpdated' => 'Hiba: az objketum már korábban módosításra került.',
'UI:Error:ObjectCannotBeUpdated' => 'Hiba: az objektum nem frissíthető.', 'UI:Error:ObjectCannotBeUpdated' => 'Hiba: az objektum nem frissíthető.',
@@ -1050,7 +1050,7 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az
'UI:Deadline_LessThan1Min' => '< 1 perc', 'UI:Deadline_LessThan1Min' => '< 1 perc',
'UI:Deadline_Minutes' => '%1$d perc', 'UI:Deadline_Minutes' => '%1$d perc',
'UI:Deadline_Hours_Minutes' => '%1$dóra %2$dperc', 'UI:Deadline_Hours_Minutes' => '%1$dóra %2$dperc',
'UI:Deadline_Days_Hours_Minutes' => '%1$nap %2$dóra %3$dperc', 'UI:Deadline_Days_Hours_Minutes' => '%1$dnap %2$dóra %3$dperc',
'UI:Help' => 'Segítség', 'UI:Help' => 'Segítség',
'UI:PasswordConfirm' => 'Jóváhagyás', 'UI:PasswordConfirm' => 'Jóváhagyás',
'UI:BeforeAdding_Class_ObjectsSaveThisObject' => '%1$s objektumok hozzáadása előtt mentse ezt az objektumot', 'UI:BeforeAdding_Class_ObjectsSaveThisObject' => '%1$s objektumok hozzáadása előtt mentse ezt az objektumot',

View File

@@ -20,8 +20,8 @@
* @licence http://opensource.org/licenses/AGPL-3.0 * @licence http://opensource.org/licenses/AGPL-3.0
*/ */
Dict::Add('JA JP', 'Japanese', '日本語', array( Dict::Add('JA JP', 'Japanese', '日本語', array(
'Core:DeletedObjectLabel' => '%1s (削除されました)', 'Core:DeletedObjectLabel' => '%1$s (削除されました)',
'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)', 'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)',
'Core:UnknownObjectLabel' => 'オブジェクトは見つかりません (クラス: %1$s, id: %2$d)', 'Core:UnknownObjectLabel' => 'オブジェクトは見つかりません (クラス: %1$s, id: %2$d)',
'Core:UnknownObjectTip' => 'オブジェクトは見つかりません。しばらく前に削除され、その後ログが削除されたかもしれません。', 'Core:UnknownObjectTip' => 'オブジェクトは見つかりません。しばらく前に削除され、その後ログが削除されたかもしれません。',
@@ -205,7 +205,7 @@ Operators:<br/>
'Core:AttributeTag' => 'Tags~~', 'Core:AttributeTag' => 'Tags~~',
'Core:AttributeTag+' => 'Tags~~', 'Core:AttributeTag+' => 'Tags~~',
'Core:Context=REST/JSON' => 'REST~~', 'Core:Context=REST/JSON' => 'REST~~',
'Core:Context=Synchro' => 'Synchro~~', 'Core:Context=Synchro' => 'Synchro~~',
'Core:Context=Setup' => 'Setup~~', 'Core:Context=Setup' => 'Setup~~',

View File

@@ -820,7 +820,7 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => '%2$sクラスの%1$dオブジェクトの削除', 'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => '%2$sクラスの%1$dオブジェクトの削除',
'UI:Delete:CannotDeleteBecause' => '削除できません: %1$s', 'UI:Delete:CannotDeleteBecause' => '削除できません: %1$s',
'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => '自動的に削除されるべきですが、出来ません。: %1$s', 'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => '自動的に削除されるべきですが、出来ません。: %1$s',
'UI:Delete:MustBeDeletedManuallyButNotPossible' => '手動で削除されるべきですが、出来ません。: %1$', 'UI:Delete:MustBeDeletedManuallyButNotPossible' => '手動で削除されるべきですが、出来ません。: %1$s',
'UI:Delete:WillBeDeletedAutomatically' => '自動的に削除されます。', 'UI:Delete:WillBeDeletedAutomatically' => '自動的に削除されます。',
'UI:Delete:MustBeDeletedManually' => '手動で削除されるべきです。', 'UI:Delete:MustBeDeletedManually' => '手動で削除されるべきです。',
'UI:Delete:CannotUpdateBecause_Issue' => '自動的に更新されるべきですが、しかし: %1$s', 'UI:Delete:CannotUpdateBecause_Issue' => '自動的に更新されるべきですが、しかし: %1$s',
@@ -1124,7 +1124,7 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'Enum:Undefined' => '未定義', 'Enum:Undefined' => '未定義',
'UI:DurationForm_Days_Hours_Minutes_Seconds' => '%1$s 日 %2$s 時 %3$s 分 %4$s 秒', 'UI:DurationForm_Days_Hours_Minutes_Seconds' => '%1$s 日 %2$s 時 %3$s 分 %4$s 秒',
'UI:ModifyAllPageTitle' => '全てを修正', 'UI:ModifyAllPageTitle' => '全てを修正',
'UI:Modify_N_ObjectsOf_Class' => 'クラス%2$Sの%1$dオブジェクトを修正', 'UI:Modify_N_ObjectsOf_Class' => 'クラス%2$sの%1$dオブジェクトを修正',
'UI:Modify_M_ObjectsOf_Class_OutOf_N' => 'クラス%2$sの%3$d中%1$dを修正', 'UI:Modify_M_ObjectsOf_Class_OutOf_N' => 'クラス%2$sの%3$d中%1$dを修正',
'UI:Menu:ModifyAll' => '修正...', 'UI:Menu:ModifyAll' => '修正...',
'UI:Button:ModifyAll' => '全て修正', 'UI:Button:ModifyAll' => '全て修正',
@@ -1141,7 +1141,7 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:BulkModify_Count_DistinctValues' => '%1$d 個の個別の値:', 'UI:BulkModify_Count_DistinctValues' => '%1$d 個の個別の値:',
'UI:BulkModify:Value_Exists_N_Times' => '%1$s, %2$d 回存在', 'UI:BulkModify:Value_Exists_N_Times' => '%1$s, %2$d 回存在',
'UI:BulkModify:N_MoreValues' => '%1$d 個以上の値...', 'UI:BulkModify:N_MoreValues' => '%1$d 個以上の値...',
'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$にセットしょうとしています。', 'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$sにセットしょうとしています。',
'UI:FailedToApplyStimuli' => 'アクションは失敗しました。', 'UI:FailedToApplyStimuli' => 'アクションは失敗しました。',
'UI:StimulusModify_N_ObjectsOf_Class' => '%1$s: クラス%3$sの%2$dオブジェクトを修正', 'UI:StimulusModify_N_ObjectsOf_Class' => '%1$s: クラス%3$sの%2$dオブジェクトを修正',
'UI:CaseLogTypeYourTextHere' => 'テキストを入力ください:', 'UI:CaseLogTypeYourTextHere' => 'テキストを入力ください:',

View File

@@ -9,8 +9,8 @@
* *
*/ */
Dict::Add('RU RU', 'Russian', 'Русский', array( Dict::Add('RU RU', 'Russian', 'Русский', array(
'Core:DeletedObjectLabel' => '%1ы (удален)', 'Core:DeletedObjectLabel' => '%1$sы (удален)',
'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)', 'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)',
'Core:UnknownObjectLabel' => 'Объект не найден (class: %1$s, id: %2$d)', 'Core:UnknownObjectLabel' => 'Объект не найден (class: %1$s, id: %2$d)',
'Core:UnknownObjectTip' => 'Объект не удается найти. Возможно, он был удален некоторое время назад, и журнал с тех пор был очищен.', 'Core:UnknownObjectTip' => 'Объект не удается найти. Возможно, он был удален некоторое время назад, и журнал с тех пор был очищен.',
@@ -193,8 +193,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'Core:FriendlyName-Description' => 'Полное название', 'Core:FriendlyName-Description' => 'Полное название',
'Core:AttributeTag' => 'Тег', 'Core:AttributeTag' => 'Тег',
'Core:AttributeTag+' => '', 'Core:AttributeTag+' => 'Тег',
'Core:Context=REST/JSON' => 'REST', 'Core:Context=REST/JSON' => 'REST',
'Core:Context=Synchro' => 'Synchro', 'Core:Context=Synchro' => 'Synchro',
'Core:Context=Setup' => 'Setup', 'Core:Context=Setup' => 'Setup',

View File

@@ -430,7 +430,7 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:Error:MandatoryTemplateParameter_group_by' => 'Parameter group_by je povinný. Skontrolujte definíciu šablóny zobrazenia.', 'UI:Error:MandatoryTemplateParameter_group_by' => 'Parameter group_by je povinný. Skontrolujte definíciu šablóny zobrazenia.',
'UI:Error:InvalidGroupByFields' => 'Neplatný zoznam polí pre skupinu podľa: "%1$s".', 'UI:Error:InvalidGroupByFields' => 'Neplatný zoznam polí pre skupinu podľa: "%1$s".',
'UI:Error:UnsupportedStyleOfBlock' => 'Chyba: nepodporovaný štýl bloku: "%1$s".', 'UI:Error:UnsupportedStyleOfBlock' => 'Chyba: nepodporovaný štýl bloku: "%1$s".',
'UI:Error:IncorrectLinkDefinition_LinkedClass_Class' => 'Nesprávna definícia spojenia : trieda objektov na manažovanie : %l$s nebol nájdený ako externý kľúč v triede %2$s', 'UI:Error:IncorrectLinkDefinition_LinkedClass_Class' => 'Nesprávna definícia spojenia : trieda objektov na manažovanie : %1$s nebol nájdený ako externý kľúč v triede %2$s',
'UI:Error:Object_Class_Id_NotFound' => 'Objekt: %1$s:%2$d nebol nájdený.', 'UI:Error:Object_Class_Id_NotFound' => 'Objekt: %1$s:%2$d nebol nájdený.',
'UI:Error:WizardCircularReferenceInDependencies' => 'Chyba: Cyklický odkaz v závislostiach medzi poliami, skontrolujte dátový model.', 'UI:Error:WizardCircularReferenceInDependencies' => 'Chyba: Cyklický odkaz v závislostiach medzi poliami, skontrolujte dátový model.',
'UI:Error:UploadedFileTooBig' => 'Nahraný súbor je príliš veľký. (Max povolená veľkosť je %1$s). Ak chcete zmeniť tento limit, obráťte sa na správcu ITOP . (Skontrolujte, PHP konfiguráciu pre upload_max_filesize a post_max_size na serveri).', 'UI:Error:UploadedFileTooBig' => 'Nahraný súbor je príliš veľký. (Max povolená veľkosť je %1$s). Ak chcete zmeniť tento limit, obráťte sa na správcu ITOP . (Skontrolujte, PHP konfiguráciu pre upload_max_filesize a post_max_size na serveri).',
@@ -1275,7 +1275,7 @@ Keď sú priradené spúštačom, každej akcii je dané číslo "príkazu", šp
'UI:DashletGroupBy:Prop-GroupBy:DayOfMonth' => 'Deň v mesiaci pre %1$s', 'UI:DashletGroupBy:Prop-GroupBy:DayOfMonth' => 'Deň v mesiaci pre %1$s',
'UI:DashletGroupBy:Prop-GroupBy:Select-Hour' => '%1$s (hodina)', 'UI:DashletGroupBy:Prop-GroupBy:Select-Hour' => '%1$s (hodina)',
'UI:DashletGroupBy:Prop-GroupBy:Select-Month' => '%1$s (mesiac)', 'UI:DashletGroupBy:Prop-GroupBy:Select-Month' => '%1$s (mesiac)',
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek' => '%1$ (deň v týžni)', 'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek' => '%1$s (deň v týžni)',
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth' => '%1$s (deň v mesiaci)', 'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth' => '%1$s (deň v mesiaci)',
'UI:DashletGroupBy:MissingGroupBy' => 'Prosím zvoľte pole na ktorom objekty budú zoskupené spolu', 'UI:DashletGroupBy:MissingGroupBy' => 'Prosím zvoľte pole na ktorom objekty budú zoskupené spolu',

View File

@@ -214,8 +214,8 @@ Operators:<br/>
'Core:FriendlyName-Description' => 'Yaygın Adı', 'Core:FriendlyName-Description' => 'Yaygın Adı',
'Core:AttributeTag' => 'Tags~~', 'Core:AttributeTag' => 'Tags~~',
'Core:AttributeTag+' => '', 'Core:AttributeTag+' => 'Tags~~',
'Core:Context=REST/JSON' => 'REST~~', 'Core:Context=REST/JSON' => 'REST~~',
'Core:Context=Synchro' => 'Synchro~~', 'Core:Context=Synchro' => 'Synchro~~',
'Core:Context=Setup' => 'Setup~~', 'Core:Context=Setup' => 'Setup~~',
@@ -324,7 +324,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s\'nin değeri %2$s olarak atandı (önceki değer: %3$s)', 'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s\'nin değeri %2$s olarak atandı (önceki değer: %3$s)',
'Change:AttName_SetTo' => '%1$s\'nin değeri %2$s olarak atandı', 'Change:AttName_SetTo' => '%1$s\'nin değeri %2$s olarak atandı',
'Change:Text_AppendedTo_AttName' => '%2$s\'ye %1$s eklendi', 'Change:Text_AppendedTo_AttName' => '%2$s\'ye %1$s eklendi',
'Change:AttName_Changed_PreviousValue_OldValue' => '%1$\'nin değeri deiştirildi, önceki değer: %2$s', 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$snin değeri deiştirildi, önceki değer: %2$s',
'Change:AttName_Changed' => '%1$s değiştirildi', 'Change:AttName_Changed' => '%1$s değiştirildi',
'Change:AttName_EntryAdded' => '%1$s değiştirilmiş, yeni giriş eklendi.', 'Change:AttName_EntryAdded' => '%1$s değiştirilmiş, yeni giriş eklendi.',
'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s~~', 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s~~',

View File

@@ -0,0 +1,242 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class DictionariesConsistencyAfterSetupTest extends ItopTestCase
{
//used by testDictEntryValues
//to filter false positive broken traductions
private static $aLabelCodeNotToCheck = [
//use of Dict::S not Format
"UI:Audit:PercentageOk",
//unused dead labels
"Class:DatacenterDevice/Attribute:redundancy/count",
"Class:DatacenterDevice/Attribute:redundancy/disabled",
"Class:DatacenterDevice/Attribute:redundancy/percent",
"Class:TriggerOnThresholdReached/Attribute:threshold_index+"
];
public function FormatProvider(){
return [
'key does not exist in dictionnary' => [
'sTemplate' => null,
'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1',
],
'traduction that breaks expected nb of arguments' => [
'sTemplate' => 'toto %1$s titi %2$s',
'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1',
],
'traduction ok' => [
'sTemplate' => 'toto %1$s titi',
'sExpectedTraduction' => 'toto 1 titi',
],
];
}
/**
* @param $sTemplate : if null it will not create dict entry
* @since 2.7.10 N°5491 - Inconsistent dictionary entries regarding arguments to pass to Dict::Format
* @dataProvider FormatProvider
*/
public function testFormatWithOneArgumentAndCustomKey(?string $sTemplate, $sExpectedTranslation){
//tricky way to mock GetLabelAndLangCode behavior via connected user language
$sLangCode = \Dict::GetUserLanguage();
$aDictByLang = $this->GetNonPublicStaticProperty(\Dict::class, 'm_aData');
$sDictKey = 'ITOP::DICT:FORMAT:BROKEN:KEY';
if (! is_null($sTemplate)){
$aDictByLang[$sLangCode][$sDictKey] = $sTemplate;
}
$this->SetNonPublicStaticProperty(\Dict::class, 'm_aData', $aDictByLang);
$this->assertEquals($sExpectedTranslation, \Dict::Format($sDictKey, "1"));
}
//test works after setup (no annotation @beforesetup)
//even if it does not extend ItopDataTestCase
private function ReadDictKeys($sLangCode) : array {
\Dict::InitLangIfNeeded($sLangCode);
$aDictEntries = $this->GetNonPublicStaticProperty(\Dict::class, 'm_aData');
return $aDictEntries[$sLangCode];
}
/**
* foreach dictionnary label map (key/value) it counts the number argument that should be passed to use Dict::Format
* examples:
* for "gabu zomeu" label there are no args
* for "shadok %1 %2 %3" there are 3 args
*
* limitation: there is no validation check for "%3 itop %2 combodo" which seems unconsistent
* @param $aDictEntry
*
* @return array
*/
private function GetKeyArgCountMap($aDictEntry) {
$aKeyArgsCount = [];
foreach ($aDictEntry as $sKey => $sValue){
$aKeyArgsCount[$sKey] = $this->countArg($sValue);
}
ksort($aKeyArgsCount);
return $aKeyArgsCount;
}
private function countArg($sLabel) {
$iMaxIndex = 0;
if (preg_match_all("/%(\d+)/", $sLabel, $aMatches)){
$aSubMatches = $aMatches[1];
if (is_array($aSubMatches)){
foreach ($aSubMatches as $aCurrentMatch){
$iIndex = $aCurrentMatch;
$iMaxIndex = ($iMaxIndex < $iIndex) ? $iIndex : $iMaxIndex;
}
}
} else if ((false !== strpos($sLabel, "%s"))
|| (false !== strpos($sLabel, "%d"))
){
$iMaxIndex = 1;
}
return $iMaxIndex;
}
/**
* Warning: hardcoded list of languages
* It is hard to have it dynamically via Dict::GetLanguages as for each lang Dict::Init should be called first
**/
public function LangCodeProvider(){
return [
'cs' => [ 'CS CZ' ],
'da' => [ 'DA DA' ],
'de' => [ 'DE DE' ],
'en' => [ 'EN US' ],
'es' => [ 'ES CR' ],
'fr' => [ 'FR FR' ],
'hu' => [ 'HU HU' ],
'it' => [ 'IT IT' ],
'ja' => [ 'JA JP' ],
'nl' => [ 'NL NL' ],
'pt' => [ 'PT BR' ],
'ru' => [ 'RU RU' ],
'sk' => [ 'SK SK' ],
'tr' => [ 'TR TR' ],
'zh' => [ 'ZH CN' ],
];
}
/**
* compare en and other dictionaries and check that for all labels there is the same number of arguments
* if not Dict::Format could raise an exception for some languages. translation should be done again...
* @dataProvider LangCodeProvider
*/
public function testDictEntryValues($sLanguageCodeToTest)
{
$sReferenceLangCode = 'EN US';
$aReferenceLangDictEntry = $this->ReadDictKeys($sReferenceLangCode);
$aDictEntry = $this->ReadDictKeys($sLanguageCodeToTest);
$aKeyArgsCountMap = [];
$aKeyArgsCountMap[$sReferenceLangCode] = $this->GetKeyArgCountMap($aReferenceLangDictEntry);
//$aKeyArgsCountMap[$sCode] = $this->GetKeyArgCountMap($aDictEntry);
//set user language
$this->SetNonPublicStaticProperty(\Dict::class, 'm_sCurrentLanguage', $sLanguageCodeToTest);
$aMismatchedKeys = [];
foreach ($aKeyArgsCountMap[$sReferenceLangCode] as $sKey => $iExpectedNbOfArgs){
if (in_array($sKey, self::$aLabelCodeNotToCheck)){
//false positive: do not test
continue;
}
if (array_key_exists($sKey, $aDictEntry)){
$aPlaceHolders = [];
for ($i=0; $i<$iExpectedNbOfArgs; $i++){
$aPlaceHolders[]=$i;
}
$sLabelTemplate = $aDictEntry[$sKey];
try{
vsprintf($sLabelTemplate, $aPlaceHolders);
} catch(\Throwable $e){
$sError = $e->getMessage();
if (array_key_exists($sError, $aMismatchedKeys)){
$aMismatchedKeys[$sError][$sKey] = $iExpectedNbOfArgs;
} else {
$aMismatchedKeys[$sError] = [$sKey => $iExpectedNbOfArgs];
}
}
}
}
$iCount = 0;
foreach ($aMismatchedKeys as $sError => $aKeys){
var_dump($sError);
foreach ($aKeys as $sKey => $iExpectedNbOfArgs) {
$iCount++;
if ($sReferenceLangCode === $sLanguageCodeToTest) {
var_dump([
'key label' => $sKey,
'expected nb of expected args' => $iExpectedNbOfArgs,
"key value in $sLanguageCodeToTest" => $aDictEntry[$sKey],
]);
} else {
var_dump([
'key label' => $sKey,
'expected nb of expected args' => $iExpectedNbOfArgs,
"key value in $sLanguageCodeToTest" => $aDictEntry[$sKey],
"key value in $sReferenceLangCode" => $aReferenceLangDictEntry[$sKey],
]);
}
}
}
$sErrorMsg = sprintf("%s broken propertie(s) on $sLanguageCodeToTest dictionaries! either change the dict value in $sLanguageCodeToTest or add it in ignored label list (cf aLabelCodeNotToCheck)", $iCount);
$this->assertEquals([], $aMismatchedKeys, $sErrorMsg);
}
public function VsprintfProvider(){
return [
'not enough args' => [
"sLabelTemplate" => "$1%s",
"aPlaceHolders" => [],
],
'exact nb of args' => [
"sLabelTemplate" => "$1%s",
"aPlaceHolders" => ["1"],
],
'too much args' => [
"sLabelTemplate" => "$1%s",
"aPlaceHolders" => ["1", "2"],
],
'\"% ok\" without args' => [
"sLabelTemplate" => "% ok",
"aPlaceHolders" => [],
],
'\"% ok $1%s\" without args' => [
"sLabelTemplate" => "% ok",
"aPlaceHolders" => ['1'],
],
];
}
/**
* @dataProvider VsprintfProvider
public function testVsprintf($sLabelTemplate, $aPlaceHolders){
try{
$this->markTestSkipped("usefull to check a specific PHP version behavior");
vsprintf($sLabelTemplate, $aPlaceHolders);
$this->assertTrue(true);
} catch(\Throwable $e) {
$this->assertTrue(false, "label \'" . $sLabelTemplate . " failed with " . var_export($aPlaceHolders, true) );
}
}
*/
}

View File

@@ -277,6 +277,21 @@ abstract class ItopTestCase extends TestCase
return $method->invokeArgs($oObject, $aArgs); return $method->invokeArgs($oObject, $aArgs);
} }
/**
* @param string $sClass
* @param string $sProperty
*
* @return mixed property
*
* @throws \ReflectionException
* @since 2.7.10 3.1.0
*/
public function GetNonPublicStaticProperty(string $sClass, string $sProperty)
{
$oProperty = $this->GetProperty($sClass, $sProperty);
return $oProperty->getValue();
}
/** /**
* @param object $oObject * @param object $oObject
@@ -289,11 +304,27 @@ abstract class ItopTestCase extends TestCase
*/ */
public function GetNonPublicProperty(object $oObject, string $sProperty) public function GetNonPublicProperty(object $oObject, string $sProperty)
{ {
$class = new \ReflectionClass(get_class($oObject)); $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
$property = $class->getProperty($sProperty);
$property->setAccessible(true);
return $property->getValue($oObject); return $oProperty->getValue($oObject);
}
/**
* @param string $sClass
* @param string $sProperty
*
* @return \ReflectionProperty
*
* @throws \ReflectionException
* @since 2.7.10 3.1.0
*/
private function GetProperty(string $sClass, string $sProperty)
{
$oClass = new \ReflectionClass($sClass);
$oProperty = $oClass->getProperty($sProperty);
$oProperty->setAccessible(true);
return $oProperty;
} }
/** /**
@@ -306,11 +337,22 @@ abstract class ItopTestCase extends TestCase
*/ */
public function SetNonPublicProperty(object $oObject, string $sProperty, $value) public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
{ {
$class = new \ReflectionClass(get_class($oObject)); $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
$property = $class->getProperty($sProperty); $oProperty->setValue($oObject, $value);
$property->setAccessible(true); }
$property->setValue($oObject, $value); /**
* @param string $sClass
* @param string $sProperty
* @param $value
*
* @throws \ReflectionException
* @since 2.7.10 3.1.0
*/
public function SetNonPublicStaticProperty(string $sClass, string $sProperty, $value)
{
$oProperty = $this->GetProperty($sClass, $sProperty);
$oProperty->setValue($value);
} }
public function RecurseRmdir($dir) { public function RecurseRmdir($dir) {
@@ -383,4 +425,4 @@ abstract class ItopTestCase extends TestCase
} }
closedir($dir); closedir($dir);
} }
} }