1*8dbb068eSAndreas Gohr<?php 2*8dbb068eSAndreas Gohr 3*8dbb068eSAndreas Gohr/** 4*8dbb068eSAndreas Gohr * Language file tests inspired by the script by schplurtz 5*8dbb068eSAndreas Gohr * @link https://www.dokuwiki.org/teams:i18n:translation-check 6*8dbb068eSAndreas Gohr */ 7*8dbb068eSAndreas Gohrclass lang_test extends DokuWikiTest 8*8dbb068eSAndreas Gohr{ 9*8dbb068eSAndreas Gohr /** 10*8dbb068eSAndreas Gohr * returen all languages except english 11*8dbb068eSAndreas Gohr * 12*8dbb068eSAndreas Gohr * @return string[] 13*8dbb068eSAndreas Gohr */ 14*8dbb068eSAndreas Gohr protected function findLanguages() 15*8dbb068eSAndreas Gohr { 16*8dbb068eSAndreas Gohr $languages = glob(DOKU_INC . 'inc/lang/*', GLOB_ONLYDIR); 17*8dbb068eSAndreas Gohr $languages = array_map('basename', $languages); 18*8dbb068eSAndreas Gohr $languages = array_filter($languages, function ($in) { 19*8dbb068eSAndreas Gohr return $in !== 'en'; 20*8dbb068eSAndreas Gohr }); 21*8dbb068eSAndreas Gohr return $languages; 22*8dbb068eSAndreas Gohr } 23*8dbb068eSAndreas Gohr 24*8dbb068eSAndreas Gohr /** 25*8dbb068eSAndreas Gohr * Get all installed plugins 26*8dbb068eSAndreas Gohr * 27*8dbb068eSAndreas Gohr * This finds all things that might be a plugin and does not care for enabled or not. 28*8dbb068eSAndreas Gohr * 29*8dbb068eSAndreas Gohr * @return string[] 30*8dbb068eSAndreas Gohr */ 31*8dbb068eSAndreas Gohr protected function findPlugins() 32*8dbb068eSAndreas Gohr { 33*8dbb068eSAndreas Gohr $plugins = glob(DOKU_INC . 'lib/plugins/*', GLOB_ONLYDIR); 34*8dbb068eSAndreas Gohr return $plugins; 35*8dbb068eSAndreas Gohr } 36*8dbb068eSAndreas Gohr 37*8dbb068eSAndreas Gohr /** 38*8dbb068eSAndreas Gohr * Get all installed templates 39*8dbb068eSAndreas Gohr * 40*8dbb068eSAndreas Gohr * This finds all things that might be a template and does not care for enabled or not. 41*8dbb068eSAndreas Gohr * 42*8dbb068eSAndreas Gohr * @return string[] 43*8dbb068eSAndreas Gohr */ 44*8dbb068eSAndreas Gohr protected function findTemplates() 45*8dbb068eSAndreas Gohr { 46*8dbb068eSAndreas Gohr $templates = glob(DOKU_INC . 'lib/tpl/*', GLOB_ONLYDIR); 47*8dbb068eSAndreas Gohr return $templates; 48*8dbb068eSAndreas Gohr } 49*8dbb068eSAndreas Gohr 50*8dbb068eSAndreas Gohr /** 51*8dbb068eSAndreas Gohr * Load the strings for the given language 52*8dbb068eSAndreas Gohr * 53*8dbb068eSAndreas Gohr * @param string $lang 54*8dbb068eSAndreas Gohr * @return array 55*8dbb068eSAndreas Gohr */ 56*8dbb068eSAndreas Gohr protected function loadLanguage($file) 57*8dbb068eSAndreas Gohr { 58*8dbb068eSAndreas Gohr $lang = []; 59*8dbb068eSAndreas Gohr if (file_exists($file)) { 60*8dbb068eSAndreas Gohr include $file; 61*8dbb068eSAndreas Gohr } 62*8dbb068eSAndreas Gohr return $lang; 63*8dbb068eSAndreas Gohr } 64*8dbb068eSAndreas Gohr 65*8dbb068eSAndreas Gohr /** 66*8dbb068eSAndreas Gohr * Provide all the language files to compare 67*8dbb068eSAndreas Gohr * 68*8dbb068eSAndreas Gohr * @return Generator 69*8dbb068eSAndreas Gohr */ 70*8dbb068eSAndreas Gohr public function provideLanguageFiles() 71*8dbb068eSAndreas Gohr { 72*8dbb068eSAndreas Gohr $bases = array_merge( 73*8dbb068eSAndreas Gohr [DOKU_INC . 'inc'], 74*8dbb068eSAndreas Gohr $this->findPlugins(), 75*8dbb068eSAndreas Gohr $this->findTemplates() 76*8dbb068eSAndreas Gohr ); 77*8dbb068eSAndreas Gohr 78*8dbb068eSAndreas Gohr foreach ($this->findLanguages() as $code) { 79*8dbb068eSAndreas Gohr foreach ($bases as $base) { 80*8dbb068eSAndreas Gohr foreach (['lang.php', 'settings.php'] as $file) { 81*8dbb068eSAndreas Gohr $englishFile = "$base/lang/en/$file"; 82*8dbb068eSAndreas Gohr $foreignFile = "$base/lang/$code/$file"; 83*8dbb068eSAndreas Gohr $name = substr($foreignFile, strlen(DOKU_INC)); 84*8dbb068eSAndreas Gohr $name = '…'.substr($name, -35); 85*8dbb068eSAndreas Gohr 86*8dbb068eSAndreas Gohr if (file_exists($foreignFile)) { 87*8dbb068eSAndreas Gohr yield ([ 88*8dbb068eSAndreas Gohr $this->loadLanguage($englishFile), 89*8dbb068eSAndreas Gohr $this->loadLanguage($foreignFile), 90*8dbb068eSAndreas Gohr $code, 91*8dbb068eSAndreas Gohr $name, 92*8dbb068eSAndreas Gohr ]); 93*8dbb068eSAndreas Gohr } 94*8dbb068eSAndreas Gohr } 95*8dbb068eSAndreas Gohr } 96*8dbb068eSAndreas Gohr } 97*8dbb068eSAndreas Gohr } 98*8dbb068eSAndreas Gohr 99*8dbb068eSAndreas Gohr /** 100*8dbb068eSAndreas Gohr * Check for obsolete language strings 101*8dbb068eSAndreas Gohr * 102*8dbb068eSAndreas Gohr * @param array $english key/value language pairs for English 103*8dbb068eSAndreas Gohr * @param array $foreign key/value language pairs for the foreign language 104*8dbb068eSAndreas Gohr * @param string $code language code of the foreign file 105*8dbb068eSAndreas Gohr * @param string $file the base file name the foreign keys came from 106*8dbb068eSAndreas Gohr * @param string $prefix sub key that is currently checked (used in recursion) 107*8dbb068eSAndreas Gohr * @dataProvider provideLanguageFiles 108*8dbb068eSAndreas Gohr */ 109*8dbb068eSAndreas Gohr public function testObsolete($english, $foreign, $code, $file, $prefix = '') 110*8dbb068eSAndreas Gohr { 111*8dbb068eSAndreas Gohr $this->assertGreaterThan(0, count($foreign), "$file exists but has no translations"); 112*8dbb068eSAndreas Gohr 113*8dbb068eSAndreas Gohr foreach ($foreign as $key => $value) { 114*8dbb068eSAndreas Gohr $name = $prefix ? $prefix . $key : $key; 115*8dbb068eSAndreas Gohr $this->assertArrayHasKey($key, $english, "$file: obsolete/unknown key '$name'"); 116*8dbb068eSAndreas Gohr 117*8dbb068eSAndreas Gohr // sub arrays as for the js translations: 118*8dbb068eSAndreas Gohr if (is_array($value) && is_array($english[$key])) { 119*8dbb068eSAndreas Gohr $this->testObsolete($english[$key], $value, $code, $file, $key); 120*8dbb068eSAndreas Gohr } 121*8dbb068eSAndreas Gohr } 122*8dbb068eSAndreas Gohr } 123*8dbb068eSAndreas Gohr 124*8dbb068eSAndreas Gohr /** 125*8dbb068eSAndreas Gohr * Check for sprintf format placeholder equality 126*8dbb068eSAndreas Gohr * 127*8dbb068eSAndreas Gohr * @param array $english key/value language pairs for English 128*8dbb068eSAndreas Gohr * @param array $foreign key/value language pairs for the foreign language 129*8dbb068eSAndreas Gohr * @param string $code language code of the foreign file 130*8dbb068eSAndreas Gohr * @param string $file the base file name the foreign keys came from 131*8dbb068eSAndreas Gohr * @param string $prefix sub key that is currently checked (used in recursion) 132*8dbb068eSAndreas Gohr * @dataProvider provideLanguageFiles 133*8dbb068eSAndreas Gohr */ 134*8dbb068eSAndreas Gohr public function testPlaceholders($english, $foreign, $code, $file, $prefix = '') 135*8dbb068eSAndreas Gohr { 136*8dbb068eSAndreas Gohr $this->assertGreaterThan(0, count($foreign), "$file exists but has no translations"); 137*8dbb068eSAndreas Gohr 138*8dbb068eSAndreas Gohr foreach ($foreign as $key => $value) { 139*8dbb068eSAndreas Gohr // non existing in english is skipped here, that what testObsolete checks 140*8dbb068eSAndreas Gohr if (!isset($english[$key])) continue; 141*8dbb068eSAndreas Gohr 142*8dbb068eSAndreas Gohr // sub arrays as for the js translations: 143*8dbb068eSAndreas Gohr if (is_array($value) && is_array($english[$key])) { 144*8dbb068eSAndreas Gohr $this->testPlaceholders($english[$key], $value, $code, $file, $key); 145*8dbb068eSAndreas Gohr return; 146*8dbb068eSAndreas Gohr } 147*8dbb068eSAndreas Gohr 148*8dbb068eSAndreas Gohr $name = $prefix ? $prefix . $key : $key; 149*8dbb068eSAndreas Gohr 150*8dbb068eSAndreas Gohr $englishPlaceholders = $this->parsePlaceholders($english[$key]); 151*8dbb068eSAndreas Gohr $foreignPlaceholders = $this->parsePlaceholders($value); 152*8dbb068eSAndreas Gohr $countEnglish = count($englishPlaceholders); 153*8dbb068eSAndreas Gohr $countForeign = count($foreignPlaceholders); 154*8dbb068eSAndreas Gohr 155*8dbb068eSAndreas Gohr $this->assertEquals($countEnglish, $countForeign, 156*8dbb068eSAndreas Gohr join("\n", 157*8dbb068eSAndreas Gohr [ 158*8dbb068eSAndreas Gohr "$file: unequal amount of sprintf format placeholders in '$name'", 159*8dbb068eSAndreas Gohr "en: '" . $english[$key] . "'", 160*8dbb068eSAndreas Gohr "$code: '$value'", 161*8dbb068eSAndreas Gohr ] 162*8dbb068eSAndreas Gohr ) 163*8dbb068eSAndreas Gohr ); 164*8dbb068eSAndreas Gohr 165*8dbb068eSAndreas Gohr $this->assertEquals($englishPlaceholders, $foreignPlaceholders, 166*8dbb068eSAndreas Gohr join("\n", 167*8dbb068eSAndreas Gohr [ 168*8dbb068eSAndreas Gohr "$file: sprintf format mismatch in '$name'", 169*8dbb068eSAndreas Gohr "en: '" . $english[$key] . "'", 170*8dbb068eSAndreas Gohr "$code: '$value'", 171*8dbb068eSAndreas Gohr ] 172*8dbb068eSAndreas Gohr ) 173*8dbb068eSAndreas Gohr ); 174*8dbb068eSAndreas Gohr } 175*8dbb068eSAndreas Gohr } 176*8dbb068eSAndreas Gohr 177*8dbb068eSAndreas Gohr /** 178*8dbb068eSAndreas Gohr * Parses the placeholders from a string and brings them in the correct order 179*8dbb068eSAndreas Gohr * 180*8dbb068eSAndreas Gohr * This has its own test below. 181*8dbb068eSAndreas Gohr * 182*8dbb068eSAndreas Gohr * @param string $string 183*8dbb068eSAndreas Gohr */ 184*8dbb068eSAndreas Gohr protected function parsePlaceholders($string) 185*8dbb068eSAndreas Gohr { 186*8dbb068eSAndreas Gohr if (!preg_match_all('/%(?:([0-9]+)\$)?([-.0-9hl]*?[%dufsc])/', $string, $matches, PREG_SET_ORDER)) { 187*8dbb068eSAndreas Gohr return []; 188*8dbb068eSAndreas Gohr } 189*8dbb068eSAndreas Gohr 190*8dbb068eSAndreas Gohr // Given this string : 'schproutch %2$s with %1$04d in %-20s plouf' 191*8dbb068eSAndreas Gohr // we have this in $matches: 192*8dbb068eSAndreas Gohr // [ 193*8dbb068eSAndreas Gohr // 0 => ['%2$s', 2, 's'], 194*8dbb068eSAndreas Gohr // 1 => ['%1$04d', 1, '04d'], 195*8dbb068eSAndreas Gohr // 2 => ['%-20s', '', '-20s'], 196*8dbb068eSAndreas Gohr // ] 197*8dbb068eSAndreas Gohr 198*8dbb068eSAndreas Gohr // sort by the given sorting in key 1 199*8dbb068eSAndreas Gohr usort($matches, function ($a, $b) { 200*8dbb068eSAndreas Gohr if ($a[1] === $b[1]) return 0; // keep as is 201*8dbb068eSAndreas Gohr 202*8dbb068eSAndreas Gohr // sort empties towards the back 203*8dbb068eSAndreas Gohr if ($a[1] === '') $a[1] = 9999; 204*8dbb068eSAndreas Gohr if ($b[1] === '') $b[1] = 9999; 205*8dbb068eSAndreas Gohr 206*8dbb068eSAndreas Gohr // compare sort numbers 207*8dbb068eSAndreas Gohr if ((int)$a[1] < (int)$b[1]) return -1; 208*8dbb068eSAndreas Gohr if ((int)$a[1] > (int)$b[1]) return 1; 209*8dbb068eSAndreas Gohr return 0; 210*8dbb068eSAndreas Gohr }); 211*8dbb068eSAndreas Gohr 212*8dbb068eSAndreas Gohr // return values in key 2 213*8dbb068eSAndreas Gohr return array_column($matches, 2); 214*8dbb068eSAndreas Gohr } 215*8dbb068eSAndreas Gohr 216*8dbb068eSAndreas Gohr /** 217*8dbb068eSAndreas Gohr * Dataprovider for the parsePlaceholder test 218*8dbb068eSAndreas Gohr * @return array[] 219*8dbb068eSAndreas Gohr */ 220*8dbb068eSAndreas Gohr public function providePlaceholders() 221*8dbb068eSAndreas Gohr { 222*8dbb068eSAndreas Gohr return [ 223*8dbb068eSAndreas Gohr ['schproutch %2$s with %1$04d in %-20s plouf', ['04d', 's', '-20s']], 224*8dbb068eSAndreas Gohr ]; 225*8dbb068eSAndreas Gohr } 226*8dbb068eSAndreas Gohr 227*8dbb068eSAndreas Gohr /** 228*8dbb068eSAndreas Gohr * Test the parsePlaceholder utility function above 229*8dbb068eSAndreas Gohr * 230*8dbb068eSAndreas Gohr * @param string $input 231*8dbb068eSAndreas Gohr * @param array $expected 232*8dbb068eSAndreas Gohr * @dataProvider providePlaceholders 233*8dbb068eSAndreas Gohr */ 234*8dbb068eSAndreas Gohr public function testParsePlaceholders($input, $expected) 235*8dbb068eSAndreas Gohr { 236*8dbb068eSAndreas Gohr $this->assertEquals($expected, $this->parsePlaceholders($input)); 237*8dbb068eSAndreas Gohr } 238*8dbb068eSAndreas Gohr} 239