diff --git a/core/dict.class.inc.php b/core/dict.class.inc.php
index 629094a52..33245f11d 100644
--- a/core/dict.class.inc.php
+++ b/core/dict.class.inc.php
@@ -3,7 +3,7 @@
//
// 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
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
@@ -18,7 +18,7 @@
/**
* Class Dict
- * Management of localizable strings
+ * Management of localizable strings
*
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
@@ -144,33 +144,50 @@ class Dict
* @return string
*/
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
//
$sLangCode = self::GetUserLanguage();
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
- return $sStringCode;
+ return [ 'label' => $sStringCode, 'lang' => $sLangCode ];
}
$aCurrentDictionary = self::$m_aData[$sLangCode];
if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary))
{
- return $aCurrentDictionary[$sStringCode];
+ return [ 'label' => $aCurrentDictionary[$sStringCode], 'lang' => $sLangCode ];
}
if (!$bUserLanguageOnly)
{
// Attempt to find the string in the default language
//
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
-
+
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
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
//
@@ -179,17 +196,17 @@ class Dict
$aDefaultDictionary = self::$m_aData['EN US'];
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{
- return $aDefaultDictionary[$sStringCode];
+ return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => 'EN US' ];
}
}
// Could not find the string...
//
if (is_null($sDefault))
{
- return $sStringCode;
+ return [ 'label' => $sStringCode, 'lang' => null ];
}
- return $sDefault;
+ return [ 'label' => $sDefault, 'lang' => null ];
}
@@ -205,19 +222,25 @@ class Dict
*/
public static function Format($sFormatCode /*, ... arguments ....*/)
{
- $sLocalizedFormat = self::S($sFormatCode);
+ ['label' => $sLocalizedFormat, 'lang' => $sLangCode] = self::GetLabelAndLangCode($sFormatCode);
+
$aArguments = func_get_args();
array_shift($aArguments);
-
+
if ($sLocalizedFormat == $sFormatCode)
{
// Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded)
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)
* @param string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US'
@@ -227,7 +250,7 @@ class Dict
{
self::$m_aData[$sLanguageCode] = $aEntries;
}
-
+
/**
* Set the list of available languages
* @param hash $aLanguagesList
@@ -288,7 +311,7 @@ class Dict
{
$sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php';
require_once($sDictFile);
-
+
if (self::GetApcService()->function_exists('apc_store')
&& (self::$m_sApplicationPrefix !== null))
{
@@ -298,7 +321,7 @@ class Dict
}
return $bResult;
}
-
+
/**
* Enable caching (cached using APC)
* @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance
@@ -342,14 +365,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]))
@@ -357,7 +380,7 @@ class Dict
$aMissing[$sStringCode] = $sValue;
}
}
-
+
foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue)
{
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef]))
@@ -380,7 +403,7 @@ class Dict
}
return array($aMissing, $aUnexpected, $aNotTranslated, $aOK);
}
-
+
public static function Dump()
{
MyHelpers::var_dump_html(self::$m_aData);
@@ -403,7 +426,7 @@ class Dict
// No need to actually load the strings since it's only used to know the list of languages
// at setup time !!
}
-
+
/**
* 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
@@ -416,7 +439,7 @@ class Dict
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aEntries = array();
$iLength = strlen($sStartingWith);
-
+
// First prefill the array with entries from the default language
foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry)
{
@@ -425,7 +448,7 @@ class Dict
$aEntries[$sCode] = $sEntry;
}
}
-
+
// Now put (overwrite) the entries for the user language
foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry)
{
diff --git a/dictionaries/hu.dictionary.itop.core.php b/dictionaries/hu.dictionary.itop.core.php
index cf2cbe090..502680cf4 100755
--- a/dictionaries/hu.dictionary.itop.core.php
+++ b/dictionaries/hu.dictionary.itop.core.php
@@ -20,7 +20,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
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:UnknownObjectLabel' => 'Object not found (class: %1$s, id: %2$d)~~',
@@ -201,7 +201,7 @@ Operators:
'Core:AttributeTag' => 'Tags~~',
'Core:AttributeTag+' => 'Tags~~',
-
+
'Core:Context=REST/JSON' => 'REST~~',
'Core:Context=Synchro' => 'Synchro~~',
'Core:Context=Setup' => 'Setup~~',
diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php
index 8434f2921..792c6e018 100755
--- a/dictionaries/hu.dictionary.itop.ui.php
+++ b/dictionaries/hu.dictionary.itop.ui.php
@@ -433,7 +433,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'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: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:ObjectAlreadyUpdated' => 'Hiba: az objketum már korábban módosításra került.',
'UI:Error:ObjectCannotBeUpdated' => 'Hiba: az objektum nem frissíthető.',
@@ -1004,7 +1004,7 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az
'Menu:UserAccountsMenu' => 'Felhasználói fiókok', // Duplicated into itop-welcome-itil (will be removed from here...)
'Menu:UserAccountsMenu+' => '', // Duplicated into itop-welcome-itil (will be removed from here...)
- 'Menu:UserAccountsMenu:Title' => 'Felhasználói fiókok', // Duplicated into itop-welcome-itil (will be removed from here...)
+ 'Menu:UserAccountsMenu:Title' => 'Felhasználói fiókok', // Duplicated into itop-welcome-itil (will be removed from here...)
'UI:iTopVersion:Short' => '%1$s verzió: %2$s',
'UI:iTopVersion:Long' => '%1$s verzió: %2$s-%3$s %4$s',
@@ -1019,7 +1019,7 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az
'UI:Deadline_LessThan1Min' => '< 1 perc',
'UI:Deadline_Minutes' => '%1$d perc',
'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:PasswordConfirm' => '(Jóváhagyás)',
'UI:BeforeAdding_Class_ObjectsSaveThisObject' => '%1$s objektumok hozzáadása előtt mentse ezt az objektumot',
diff --git a/dictionaries/ja.dictionary.itop.core.php b/dictionaries/ja.dictionary.itop.core.php
index 39bd9618b..651a6c2dd 100644
--- a/dictionaries/ja.dictionary.itop.core.php
+++ b/dictionaries/ja.dictionary.itop.core.php
@@ -20,7 +20,7 @@
* @licence http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('JA JP', 'Japanese', '日本語', array(
- 'Core:DeletedObjectLabel' => '%1s (削除されました)',
+ 'Core:DeletedObjectLabel' => '%1$s (削除されました)',
'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)',
'Core:UnknownObjectLabel' => 'オブジェクトは見つかりません (クラス: %1$s, id: %2$d)',
@@ -201,7 +201,7 @@ Operators:
'Core:AttributeTag' => 'Tags~~',
'Core:AttributeTag+' => 'Tags~~',
-
+
'Core:Context=REST/JSON' => 'REST~~',
'Core:Context=Synchro' => 'Synchro~~',
'Core:Context=Setup' => 'Setup~~',
diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php
index 8f3ad8e22..98cf85d31 100644
--- a/dictionaries/ja.dictionary.itop.ui.php
+++ b/dictionaries/ja.dictionary.itop.ui.php
@@ -794,7 +794,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => '%2$sクラスの%1$dオブジェクトの削除',
'UI:Delete:CannotDeleteBecause' => '削除できません: %1$s',
'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => '自動的に削除されるべきですが、出来ません。: %1$s',
- 'UI:Delete:MustBeDeletedManuallyButNotPossible' => '手動で削除されるべきですが、出来ません。: %1$',
+ 'UI:Delete:MustBeDeletedManuallyButNotPossible' => '手動で削除されるべきですが、出来ません。: %1$s',
'UI:Delete:WillBeDeletedAutomatically' => '自動的に削除されます。',
'UI:Delete:MustBeDeletedManually' => '手動で削除されるべきです。',
'UI:Delete:CannotUpdateBecause_Issue' => '自動的に更新されるべきですが、しかし: %1$s',
@@ -1005,7 +1005,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'Menu:UserAccountsMenu' => 'ユーザアカウント', // Duplicated into itop-welcome-itil (will be removed from here...)
'Menu:UserAccountsMenu+' => 'ユーザアカウント', // Duplicated into itop-welcome-itil (will be removed from here...)
- 'Menu:UserAccountsMenu:Title' => 'ユーザアカウント', // Duplicated into itop-welcome-itil (will be removed from here...)
+ 'Menu:UserAccountsMenu:Title' => 'ユーザアカウント', // Duplicated into itop-welcome-itil (will be removed from here...)
'UI:iTopVersion:Short' => '%1$sバージョン%2$s',
'UI:iTopVersion:Long' => '%1$sバージョン%2$s-%3$s ビルド%4$s',
@@ -1094,7 +1094,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'Enum:Undefined' => '未定義',
'UI:DurationForm_Days_Hours_Minutes_Seconds' => '%1$s 日 %2$s 時 %3$s 分 %4$s 秒',
'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:Menu:ModifyAll' => '修正...',
'UI:Button:ModifyAll' => '全て修正',
@@ -1111,7 +1111,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'UI:BulkModify_Count_DistinctValues' => '%1$d 個の個別の値:',
'UI:BulkModify:Value_Exists_N_Times' => '%1$s, %2$d 回存在',
'UI:BulkModify:N_MoreValues' => '%1$d 個以上の値...',
- 'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$にセットしょうとしています。',
+ 'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$sにセットしょうとしています。',
'UI:FailedToApplyStimuli' => 'アクションは失敗しました。',
'UI:StimulusModify_N_ObjectsOf_Class' => '%1$s: クラス%3$sの%2$dオブジェクトを修正',
'UI:CaseLogTypeYourTextHere' => 'テキストを入力ください:',
diff --git a/dictionaries/ru.dictionary.itop.core.php b/dictionaries/ru.dictionary.itop.core.php
index 3aedc2f27..a1a26429a 100644
--- a/dictionaries/ru.dictionary.itop.core.php
+++ b/dictionaries/ru.dictionary.itop.core.php
@@ -9,7 +9,7 @@
*
*/
Dict::Add('RU RU', 'Russian', 'Русский', array(
- 'Core:DeletedObjectLabel' => '%1ы (удален)',
+ 'Core:DeletedObjectLabel' => '%1$sы (удален)',
'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)',
'Core:UnknownObjectLabel' => 'Объект не найден (class: %1$s, id: %2$d)',
@@ -190,7 +190,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'Core:AttributeTag' => 'Тег',
'Core:AttributeTag+' => 'Тег',
-
+
'Core:Context=REST/JSON' => 'REST',
'Core:Context=Synchro' => 'Synchro',
'Core:Context=Setup' => 'Setup',
diff --git a/dictionaries/sk.dictionary.itop.ui.php b/dictionaries/sk.dictionary.itop.ui.php
index 3574a85d4..7e4c010a3 100644
--- a/dictionaries/sk.dictionary.itop.ui.php
+++ b/dictionaries/sk.dictionary.itop.ui.php
@@ -4,7 +4,7 @@
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
@@ -419,7 +419,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
'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: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: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).',
@@ -1007,7 +1007,7 @@ Keď sú priradené spúštačom, každej akcii je dané číslo "príkazu", šp
'Menu:UserAccountsMenu' => 'Užívateľské účty', // Duplicated into itop-welcome-itil (will be removed from here...)
'Menu:UserAccountsMenu+' => '', // Duplicated into itop-welcome-itil (will be removed from here...)
- 'Menu:UserAccountsMenu:Title' => 'Užívateľské účty', // Duplicated into itop-welcome-itil (will be removed from here...)
+ 'Menu:UserAccountsMenu:Title' => 'Užívateľské účty', // Duplicated into itop-welcome-itil (will be removed from here...)
'UI:iTopVersion:Short' => 'iTop verzia %1$s',
'UI:iTopVersion:Long' => 'iTop verzia %1$s-%2$s postavená na %3$s',
@@ -1238,7 +1238,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:Select-Hour' => '%1$s (hodina)',
'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:MissingGroupBy' => 'Prosím zvoľte pole na ktorom objekty budú zoskupené spolu',
diff --git a/dictionaries/tr.dictionary.itop.core.php b/dictionaries/tr.dictionary.itop.core.php
index 0d99e1b94..82f36b56a 100644
--- a/dictionaries/tr.dictionary.itop.core.php
+++ b/dictionaries/tr.dictionary.itop.core.php
@@ -211,7 +211,7 @@ Operators:
'Core:AttributeTag' => 'Tags~~',
'Core:AttributeTag+' => 'Tags~~',
-
+
'Core:Context=REST/JSON' => 'REST~~',
'Core:Context=Synchro' => 'Synchro~~',
'Core:Context=Setup' => 'Setup~~',
@@ -309,7 +309,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' => '%1$s\'nin değeri %2$s olarak atandı',
'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_EntryAdded' => '%1$s değiştirilmiş, yeni giriş eklendi.',
'Change:LinkSet:Added' => '%1$s \'eklendi',
diff --git a/tests/php-unit-tests/integration-tests/DictionariesConsistencyAfterSetupTest.php b/tests/php-unit-tests/integration-tests/DictionariesConsistencyAfterSetupTest.php
new file mode 100644
index 000000000..8b54282f6
--- /dev/null
+++ b/tests/php-unit-tests/integration-tests/DictionariesConsistencyAfterSetupTest.php
@@ -0,0 +1,242 @@
+ [
+ '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) );
+ }
+ }
+ */
+}
diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
index 821c0140f..cd5bcb0a5 100644
--- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
+++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php
@@ -222,6 +222,21 @@ abstract class ItopTestCase extends TestCase
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
@@ -234,11 +249,27 @@ abstract class ItopTestCase extends TestCase
*/
public function GetNonPublicProperty(object $oObject, string $sProperty)
{
- $class = new \ReflectionClass(get_class($oObject));
- $property = $class->getProperty($sProperty);
- $property->setAccessible(true);
+ $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
- 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;
}
/**
@@ -251,10 +282,22 @@ abstract class ItopTestCase extends TestCase
*/
public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
{
- $class = new \ReflectionClass(get_class($oObject));
- $property = $class->getProperty($sProperty);
- $property->setAccessible(true);
-
- $property->setValue($oObject, $value);
+ $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
+ $oProperty->setValue($oObject, $value);
}
-}
\ No newline at end of file
+
+ /**
+ * @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);
+ }
+
+}