diff --git a/core/dict.class.inc.php b/core/dict.class.inc.php index 6e9f9d386..66cec36c9 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. @@ -116,33 +116,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 // @@ -151,17 +168,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 ]; } @@ -177,19 +194,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' @@ -199,7 +222,7 @@ class Dict { self::$m_aData[$sLanguageCode] = $aEntries; } - + /** * Set the list of available languages * @param hash $aLanguagesList @@ -260,7 +283,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)) { @@ -270,7 +293,7 @@ class Dict } return $bResult; } - + /** * Enable caching (cached using APC) * @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') { $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])) @@ -328,7 +351,7 @@ class Dict $aMissing[$sStringCode] = $sValue; } } - + foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue) { if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef])) @@ -351,7 +374,7 @@ class Dict } return array($aMissing, $aUnexpected, $aNotTranslated, $aOK); } - + public static function Dump() { 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 // 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 @@ -387,7 +410,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) { @@ -396,7 +419,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 e9f49ccef..cc7117c3f 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)~~', @@ -204,8 +204,8 @@ Operators:
'Core:FriendlyName-Description' => 'Friendly name~~', 'Core:AttributeTag' => 'Tags~~', - 'Core:AttributeTag+' => '', - + '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 819a9695e..31929884e 100755 --- a/dictionaries/hu.dictionary.itop.ui.php +++ b/dictionaries/hu.dictionary.itop.ui.php @@ -443,7 +443,7 @@ We hope you’ll 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: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ő.', @@ -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_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 c235ecf67..6526d0a2f 100644 --- a/dictionaries/ja.dictionary.itop.core.php +++ b/dictionaries/ja.dictionary.itop.core.php @@ -20,8 +20,8 @@ * @licence http://opensource.org/licenses/AGPL-3.0 */ Dict::Add('JA JP', 'Japanese', '日本語', array( - 'Core:DeletedObjectLabel' => '%1s (削除されました)', - 'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)', + 'Core:DeletedObjectLabel' => '%1$s (削除されました)', + 'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)', 'Core:UnknownObjectLabel' => 'オブジェクトは見つかりません (クラス: %1$s, id: %2$d)', 'Core:UnknownObjectTip' => 'オブジェクトは見つかりません。しばらく前に削除され、その後ログが削除されたかもしれません。', @@ -205,7 +205,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 3c78e7d90..95dfbbf41 100644 --- a/dictionaries/ja.dictionary.itop.ui.php +++ b/dictionaries/ja.dictionary.itop.ui.php @@ -820,7 +820,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating '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', @@ -1124,7 +1124,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating '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' => '全て修正', @@ -1141,7 +1141,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating '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 2e19f6ba7..e915cc99a 100644 --- a/dictionaries/ru.dictionary.itop.core.php +++ b/dictionaries/ru.dictionary.itop.core.php @@ -9,8 +9,8 @@ * */ Dict::Add('RU RU', 'Russian', 'Русский', array( - 'Core:DeletedObjectLabel' => '%1ы (удален)', - 'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)', + 'Core:DeletedObjectLabel' => '%1$sы (удален)', + 'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)', 'Core:UnknownObjectLabel' => 'Объект не найден (class: %1$s, id: %2$d)', 'Core:UnknownObjectTip' => 'Объект не удается найти. Возможно, он был удален некоторое время назад, и журнал с тех пор был очищен.', @@ -193,8 +193,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', array( 'Core:FriendlyName-Description' => 'Полное название', 'Core:AttributeTag' => 'Тег', - '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 e7db57eee..ba14fc64a 100644 --- a/dictionaries/sk.dictionary.itop.ui.php +++ b/dictionaries/sk.dictionary.itop.ui.php @@ -430,7 +430,7 @@ We hope you’ll 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: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).', @@ -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: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 afc159585..0f5275dfb 100644 --- a/dictionaries/tr.dictionary.itop.core.php +++ b/dictionaries/tr.dictionary.itop.core.php @@ -214,8 +214,8 @@ Operators:
'Core:FriendlyName-Description' => 'Yaygın Adı', 'Core:AttributeTag' => 'Tags~~', - 'Core:AttributeTag+' => '', - + 'Core:AttributeTag+' => 'Tags~~', + 'Core:Context=REST/JSON' => 'REST~~', 'Core:Context=Synchro' => 'Synchro~~', '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' => '%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:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s~~', 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 fc8cc58c3..7b77ab91a 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -277,6 +277,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 @@ -289,11 +304,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; } /** @@ -306,11 +337,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); + $oProperty = $this->GetProperty(get_class($oObject), $sProperty); + $oProperty->setValue($oObject, $value); + } - $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) { @@ -383,4 +425,4 @@ abstract class ItopTestCase extends TestCase } closedir($dir); } -} \ No newline at end of file +}