PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/Translate/MessageChecks.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 413 lines | 225 code | 51 blank | 137 comment | 31 complexity | 55cdf79987384521d2b89ad79b0db271 MD5 | raw file
  1. <?php
  2. /**
  3. * %Message checking framework.
  4. *
  5. * @file
  6. * @defgroup MessageCheckers Message Checkers
  7. *
  8. */
  9. /**
  10. * Message checkers try to find common mistakes so that translators can fix
  11. * them quickly. To implement your own checks, extend this class and add a
  12. * method of the following type:
  13. * @code
  14. * protected function myCheck( $messages, $code, &$warnings ) {
  15. * foreach ( $messages as $message ) {
  16. * $key = $message->key();
  17. * $translation = $message->translation();
  18. * if ( strpos( $translation, 'smelly' ) !== false ) {
  19. * $warnings[$key][] = array(
  20. * array( 'badword', 'smelly', $key, $code ),
  21. * 'translate-checks-badword', // Needs to be defined in i18n file
  22. * array( 'PARAMS', 'smelly' ),
  23. * );
  24. * }
  25. * }
  26. * }
  27. * @endcode
  28. *
  29. * Warnings are of format: <pre>
  30. * $warnings[$key][] = array(
  31. * array( 'printf', $subcheck, $key, $code ), # check idenfitication
  32. * 'translate-checks-parameters-unknown', # check warning message
  33. * array( 'PARAMS', $params ), # optional special param list, formatted later with Language::commaList()
  34. * array( 'COUNT', count($params) ), # optional number of params, formatted later with Language::formatNum()
  35. * 'Any other parameters to the message',
  36. * </pre>
  37. *
  38. * @ingroup MessageCheckers
  39. * @author Niklas Laxström
  40. * @copyright Copyright © 2008-2010, Niklas Laxström
  41. * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
  42. */
  43. class MessageChecker {
  44. protected $checks = array();
  45. protected $group = null;
  46. private static $globalBlacklist = null;
  47. /**
  48. * Constructs a suitable checker for given message group.
  49. * @param $group MessageGroup
  50. */
  51. public function __construct( MessageGroup $group ) {
  52. if ( self::$globalBlacklist === null ) {
  53. $file = dirname( __FILE__ ) . '/check-blacklist.php';
  54. $list = PHPVariableLoader::loadVariableFromPHPFile( $file, 'checkBlacklist' );
  55. $keys = array( 'group', 'check', 'subcheck', 'code', 'message' );
  56. foreach ( $list as $key => $pattern ) {
  57. foreach ( $keys as $checkKey ) {
  58. if ( !isset( $pattern[$checkKey] ) ) {
  59. $list[$key][$checkKey] = '#';
  60. } elseif ( is_array( $pattern[$checkKey] ) ) {
  61. $list[$key][$checkKey] =
  62. array_map( array( $this, 'foldValue' ), $pattern[$checkKey] );
  63. } else {
  64. $list[$key][$checkKey] = $this->foldValue( $pattern[$checkKey] );
  65. }
  66. }
  67. }
  68. self::$globalBlacklist = $list;
  69. }
  70. $this->group = $group;
  71. }
  72. /**
  73. * Normalises check keys.
  74. * @param $value \string check key
  75. * @return \string normalised check key
  76. */
  77. protected function foldValue( $value ) {
  78. return str_replace( ' ', '_', strtolower( $value ) );
  79. }
  80. /**
  81. * Set the tests for this checker. Array of callables with descriptive keys.
  82. * @param $checks \array List of checks (suitable methods in this class)
  83. */
  84. public function setChecks( $checks ) {
  85. foreach ( $checks as $k => $c ) {
  86. if ( !is_callable( $c ) ) {
  87. unset( $checks[$k] );
  88. wfWarn( "Check function for check $k is not callable" );
  89. }
  90. }
  91. $this->checks = $checks;
  92. }
  93. /**
  94. * Adds one tests for this checker.
  95. * @see setChecks()
  96. */
  97. public function addCheck( $check ) {
  98. if ( is_callable( $check ) ) {
  99. $this->checks[] = $check;
  100. }
  101. }
  102. /**
  103. * Checks one message, returns array of warnings that can be passed to
  104. * OutputPage::addWikiMsg or similar.
  105. *
  106. * @param $message TMessage
  107. * @param $code \string Language code
  108. * @return \array
  109. */
  110. public function checkMessage( TMessage $message, $code ) {
  111. $warningsArray = array();
  112. $messages = array( $message );
  113. foreach ( $this->checks as $check ) {
  114. call_user_func_array( $check, array( $messages, $code, &$warningsArray ) );
  115. }
  116. $warningsArray = $this->filterWarnings( $warningsArray );
  117. if ( !count( $warningsArray ) ) {
  118. return array();
  119. }
  120. $warnings = $warningsArray[$message->key()];
  121. $warnings = $this->fixMessageParams( $warnings );
  122. return $warnings;
  123. }
  124. /**
  125. * Checks one message, returns true if any check matches.
  126. * @param $message TMessage
  127. * @param $code \string Language code
  128. * @return \bool True if there is a problem, false otherwise.
  129. */
  130. public function checkMessageFast( TMessage $message, $code ) {
  131. $warningsArray = array();
  132. $messages = array( $message );
  133. foreach ( $this->checks as $check ) {
  134. call_user_func_array( $check, array( $messages, $code, &$warningsArray ) );
  135. if ( count( $warningsArray ) ) {
  136. return true;
  137. }
  138. }
  139. return false;
  140. }
  141. /**
  142. * Filters warnings defined in check-blacklist.php.
  143. * @param $warningsArray \array List of warnings produces by checkMessage().
  144. * @return \array List of filtered warnings.
  145. */
  146. protected function filterWarnings( $warningsArray ) {
  147. $groupId = $this->group->getId();
  148. // There is an array of messages...
  149. foreach ( $warningsArray as $mkey => $warnings ) {
  150. // ... each which has an array of warnings.
  151. foreach ( $warnings as $wkey => $warning ) {
  152. $check = array_shift( $warning );
  153. // Check if the key is blacklisted...
  154. foreach ( self::$globalBlacklist as $pattern ) {
  155. if ( !$this->match( $pattern['group'], $groupId ) ) continue;
  156. if ( !$this->match( $pattern['check'], $check[0] ) ) continue;
  157. if ( !$this->match( $pattern['subcheck'], $check[1] ) ) continue;
  158. if ( !$this->match( $pattern['message'], $check[2] ) ) continue;
  159. if ( !$this->match( $pattern['code'], $check[3] ) ) continue;
  160. // If all of the aboce match, filter the check
  161. unset( $warningsArray[$mkey][$wkey] );
  162. }
  163. }
  164. }
  165. return $warningsArray;
  166. }
  167. /**
  168. * Matches check information against blacklist pattern.
  169. * @param $pattern \string
  170. * @param $value \string The actual value in the warnings produces by the check
  171. * @return \bool True of the pattern matches the value.
  172. */
  173. protected function match( $pattern, $value ) {
  174. if ( $pattern === '#' ) {
  175. return true;
  176. } elseif ( is_array( $pattern ) ) {
  177. return in_array( strtolower( $value ), $pattern, true );
  178. } else {
  179. return strtolower( $value ) === $pattern;
  180. }
  181. }
  182. /**
  183. * Converts the special params to something nice. Currently useless, but
  184. * useful if in the future blacklist can work with parameter level too.
  185. * @param $warnings \array List of warnings
  186. * @return List of warning messages with parameters.
  187. */
  188. protected function fixMessageParams( $warnings ) {
  189. global $wgLang;
  190. foreach ( $warnings as $wkey => $warning ) {
  191. array_shift( $warning );
  192. $message = array( array_shift( $warning ) );
  193. foreach ( $warning as $param ) {
  194. if ( !is_array( $param ) ) {
  195. $message[] = $param;
  196. } else {
  197. list( $type, $value ) = $param;
  198. if ( $type === 'COUNT' ) {
  199. $message[] = $wgLang->formatNum( $value );
  200. } elseif ( $type === 'PARAMS' ) {
  201. $message[] = $wgLang->commaList( $value );
  202. } else {
  203. throw new MWException( "Unknown type $type" );
  204. }
  205. }
  206. }
  207. $warnings[$wkey] = $message;
  208. }
  209. return $warnings;
  210. }
  211. /**
  212. * Compares two arrays return items that don't exist in the latter.
  213. * @param $defs \array
  214. * @param $trans \array
  215. * @return Items of $defs that are not in $trans.
  216. */
  217. protected static function compareArrays( $defs, $trans ) {
  218. $missing = array();
  219. foreach ( $defs as $defVar ) {
  220. if ( !in_array( $defVar, $trans ) ) {
  221. $missing[] = $defVar;
  222. }
  223. }
  224. return $missing;
  225. }
  226. /**
  227. * Checks for missing and unknown printf formatting characters in
  228. * translations.
  229. * @param $messages \mixed Iterable list of TMessage objects.
  230. * @param $code \string Language code
  231. * @param $warnings \array Array where warnings are appended to.
  232. */
  233. protected function printfCheck( $messages, $code, &$warnings ) {
  234. return $this->parameterCheck( $messages, $code, $warnings, '/%(\d+\$)?[sduf]/U' );
  235. }
  236. /**
  237. * Checks for missing and unknown Ruby variables (%{var}) in
  238. * translations.
  239. * @param $messages \mixed Iterable list of TMessage objects.
  240. * @param $code \string Language code
  241. * @param $warnings \array Array where warnings are appended to.
  242. */
  243. protected function rubyVariableCheck( $messages, $code, &$warnings ) {
  244. return $this->parameterCheck( $messages, $code, $warnings, '/%{[a-zA-Z_]+}/' );
  245. }
  246. /**
  247. * Checks for missing and unknown python string interpolation operators in
  248. * translations.
  249. * @param $messages \mixed Iterable list of TMessage objects.
  250. * @param $code \string Language code
  251. * @param $warnings \array Array where warnings are appended to.
  252. */
  253. protected function pythonInterpolationCheck( $messages, $code, &$warnings ) {
  254. return $this->parameterCheck( $messages, $code, $warnings, '/\%\([a-zA-Z0-9]*?\)[diouxXeEfFgGcrs]/U' );
  255. }
  256. /**
  257. * Checks if the translation has even number of opening and closing
  258. * parentheses. {, [ and ( are checked.
  259. * @param $messages \mixed Iterable list of TMessage objects.
  260. * @param $code \string Language code
  261. * @param $warnings \array Array where warnings are appended to.
  262. */
  263. protected function braceBalanceCheck( $messages, $code, &$warnings ) {
  264. foreach ( $messages as $message ) {
  265. $key = $message->key();
  266. $translation = $message->translation();
  267. $translation = preg_replace( '/[^{}[\]()]/u', '', $translation );
  268. $subcheck = 'brace';
  269. $counts = array(
  270. '{' => 0, '}' => 0,
  271. '[' => 0, ']' => 0,
  272. '(' => 0, ')' => 0,
  273. );
  274. $len = strlen( $translation );
  275. for ( $i = 0; $i < $len; $i++ ) {
  276. $char = $translation[$i];
  277. $counts[$char]++;
  278. }
  279. $balance = array();
  280. if ( $counts['['] !== $counts[']'] ) {
  281. $balance[] = '[]: ' . ( $counts['['] - $counts[']'] );
  282. }
  283. if ( $counts['{'] !== $counts['}'] ) {
  284. $balance[] = '{}: ' . ( $counts['{'] - $counts['}'] );
  285. }
  286. if ( $counts['('] !== $counts[')'] ) {
  287. $balance[] = '(): ' . ( $counts['('] - $counts[')'] );
  288. }
  289. if ( count( $balance ) ) {
  290. $warnings[$key][] = array(
  291. array( 'balance', $subcheck, $key, $code ),
  292. 'translate-checks-balance',
  293. array( 'PARAMS', $balance ),
  294. array( 'COUNT', count( $balance ) ),
  295. );
  296. }
  297. }
  298. }
  299. /**
  300. * Checks for missing and unknown printf formatting characters in
  301. * translations.
  302. * @param $messages \mixed Iterable list of TMessage objects.
  303. * @param $code \string Language code
  304. * @param $warnings \array Array where warnings are appended to.
  305. * @param $pattern \string Regular expression for matching variables.
  306. */
  307. protected function parameterCheck( $messages, $code, &$warnings, $pattern ) {
  308. foreach ( $messages as $message ) {
  309. $key = $message->key();
  310. $definition = $message->definition();
  311. $translation = $message->translation();
  312. preg_match_all( $pattern, $definition, $defVars );
  313. preg_match_all( $pattern, $translation, $transVars );
  314. // Check for missing variables in the translation
  315. $subcheck = 'missing';
  316. $params = self::compareArrays( $defVars[0], $transVars[0] );
  317. if ( count( $params ) ) {
  318. $warnings[$key][] = array(
  319. array( 'variable', $subcheck, $key, $code ),
  320. 'translate-checks-parameters',
  321. array( 'PARAMS', $params ),
  322. array( 'COUNT', count( $params ) ),
  323. );
  324. }
  325. // Check for unknown variables in the translatio
  326. $subcheck = 'unknown';
  327. $params = self::compareArrays( $transVars[0], $defVars[0] );
  328. if ( count( $params ) ) {
  329. $warnings[$key][] = array(
  330. array( 'variable', $subcheck, $key, $code ),
  331. 'translate-checks-parameters-unknown',
  332. array( 'PARAMS', $params ),
  333. array( 'COUNT', count( $params ) ),
  334. );
  335. }
  336. }
  337. }
  338. protected function balancedTagsCheck( $messages, $code, &$warnings ) {
  339. foreach ( $messages as $message ) {
  340. $key = $message->key();
  341. $translation = $message->translation();
  342. libxml_use_internal_errors( true );
  343. libxml_clear_errors();
  344. $doc = simplexml_load_string( Xml::tags( 'root', null, $translation ) );
  345. if ( $doc ) continue;
  346. $errors = libxml_get_errors();
  347. $params = array();
  348. foreach ( $errors as $error ) {
  349. if ( $error->code !== 76 && $error->code !== 73 ) continue;
  350. $params[] = "<br />• [{$error->code}] $error->message";
  351. }
  352. if ( !count( $params ) ) continue;
  353. $warnings[$key][] = array(
  354. array( 'tags', 'balance', $key, $code ),
  355. 'translate-checks-format',
  356. array( 'PARAMS', $params ),
  357. array( 'COUNT', count( $params ) ),
  358. );
  359. }
  360. libxml_clear_errors();
  361. }
  362. }