key(); $definition = $message->definition(); $translation = $message->translation(); $subcheck = 'extra'; $matches = $links = []; preg_match_all( "/\[\[([{$tc}]+)(\\|(.+?))?]]/sDu", $translation, $matches ); $count = count( $matches[0] ); for ( $i = 0; $i < $count; $i++ ) { $backMatch = preg_quote( $matches[1][$i], '/' ); if ( preg_match( "/\[\[$backMatch/", $definition ) ) { continue; } $links[] = "[[{$matches[1][$i]}{$matches[2][$i]}]]"; } if ( count( $links ) ) { $warnings[$key][] = [ [ 'links', $subcheck, $key, $code ], 'translate-checks-links', [ 'PARAMS', $links ], [ 'COUNT', count( $links ) ], ]; } $subcheck = 'missing'; $matches = $links = []; preg_match_all( "/\[\[([{$tc}]+)(\\|(.+?))?]]/sDu", $definition, $matches ); $count = count( $matches[0] ); for ( $i = 0; $i < $count; $i++ ) { $backMatch = preg_quote( $matches[1][$i], '/' ); if ( preg_match( "/\[\[$backMatch/", $translation ) ) { continue; } $links[] = "[[{$matches[1][$i]}{$matches[2][$i]}]]"; } if ( count( $links ) ) { $warnings[$key][] = [ [ 'links', $subcheck, $key, $code ], 'translate-checks-links-missing', [ 'PARAMS', $links ], [ 'COUNT', count( $links ) ], ]; } } } /** * Checks if the \
and \
tags are using the correct syntax. * * @param TMessage[] $messages Iterable list of TMessage objects. * @param string $code Language code of the translations. * @param array &$warnings Array where warnings are appended to. */ protected function XhtmlCheck( $messages, $code, &$warnings ) { foreach ( $messages as $message ) { $key = $message->key(); $translation = $message->translation(); if ( strpos( $translation, '<' ) === false ) { continue; } $subcheck = 'invalid'; $tags = [ '~
~suDi' => '
', // Wrong syntax '~
~suDi' => '
', '~
~suDi' => '
', // Wrong syntax '~
~suDi' => '
', '~<(HR|Hr|hR) />~su' => '
', // Case '~<(BR|Br|bR) />~su' => '
', ]; $definition = $message->definition(); $wrongTags = []; foreach ( $tags as $wrong => $correct ) { $matches = []; preg_match_all( $wrong, $translation, $matches, PREG_PATTERN_ORDER ); foreach ( $matches[0] as $wrongMatch ) { if ( strpos( $definition, $wrongMatch ) !== false ) { // If the message definition contains a // non-strict string, do not enforce it continue; } $wrongTags[$wrongMatch] = "$wrongMatch → $correct"; } } if ( count( $wrongTags ) ) { $warnings[$key][] = [ [ 'xhtml', $subcheck, $key, $code ], 'translate-checks-xhtml', [ 'PARAMS', $wrongTags ], [ 'COUNT', count( $wrongTags ) ], ]; } } } /** * Checks if the translation doesn't use plural while the definition has one. * * @param TMessage[] $messages Iterable list of TMessage objects. * @param string $code Language code of the translations. * @param array &$warnings Array where warnings are appended to. */ protected function pluralCheck( $messages, $code, &$warnings ) { foreach ( $messages as $message ) { $key = $message->key(); $definition = $message->definition(); $translation = $message->translation(); $subcheck = 'missing'; if ( stripos( $definition, '{{plural:' ) !== false && stripos( $translation, '{{plural:' ) === false ) { $warnings[$key][] = [ [ 'plural', $subcheck, $key, $code ], 'translate-checks-plural', ]; } } } /** * Checks if the translation uses too many plural forms * @param TMessage[] $messages * @param string $code * @param array &$warnings * @since 2012-09-19 */ protected function pluralFormsCheck( $messages, $code, &$warnings ) { foreach ( $messages as $message ) { $key = $message->key(); $translation = $message->translation(); // Are there any plural forms for this language in this message? if ( stripos( $translation, '{{plural:' ) === false ) { return; } $plurals = self::getPluralForms( $translation ); $allowed = self::getPluralFormCount( $code ); foreach ( $plurals as $forms ) { $forms = self::removeExplicitPluralForms( $forms ); $provided = count( $forms ); if ( $provided > $allowed ) { $warnings[$key][] = [ [ 'plural', 'forms', $key, $code ], 'translate-checks-plural-forms', $provided, $allowed ]; } // Are the last two forms identical? if ( $provided > 1 && $forms[$provided - 1] === $forms[$provided - 2] ) { $warnings[$key][] = [ [ 'plural', 'dupe', $key, $code ], 'translate-checks-plural-dupe' ]; } } } } /** * Returns the number of plural forms %MediaWiki supports * for a language. * @since 2012-09-19 * @param string $code Language code * @return int */ public static function getPluralFormCount( $code ) { $forms = Language::factory( $code )->getPluralRules(); // +1 for the 'other' form return count( $forms ) + 1; } /** * Ugly home made probably awfully slow looping parser * that parses {{PLURAL}} instances from message and * returns array of invokations having array of forms. * @since 2012-09-19 * @param string $translation * @return array[] */ public static function getPluralForms( $translation ) { // Stores the forms from plural invocations $plurals = []; $cb = function ( $parser, $frame, $args ) use ( &$plurals ) { $forms = []; foreach ( $args as $index => $form ) { // The first arg is the number, we skip it if ( $index !== 0 ) { // Collect the raw text $forms[] = $frame->expand( $form, PPFrame::RECOVER_ORIG ); // Expand the text to process embedded plurals $frame->expand( $form ); } } $plurals[] = $forms; return ''; }; // Setup parser $parser = new Parser(); // Load the default magic words etc now. $parser->firstCallInit(); // So that they don't overrider our own callback $parser->setFunctionHook( 'plural', $cb, Parser::SFH_NO_HASH | Parser::SFH_OBJECT_ARGS ); // Setup things needed for preprocess $title = null; $options = new ParserOptions( new User(), Language::factory( 'en' ) ); $parser->preprocess( $translation, $title, $options ); return $plurals; } /** * Imitiates the core plural form handling by removing * plural forms that start with explicit number. * @since 2012-09-19 * @param array $forms * @return array */ public static function removeExplicitPluralForms( array $forms ) { // Handle explicit 0= and 1= forms foreach ( $forms as $index => $form ) { if ( preg_match( '/^[0-9]+=/', $form ) ) { unset( $forms[$index] ); } } return array_values( $forms ); } /** * Checks for page names that they have an untranslated namespace. * * @param TMessage[] $messages Iterable list of TMessage objects. * @param string $code Language code of the translations. * @param array &$warnings Array where warnings are appended to. */ protected function pagenameMessagesCheck( $messages, $code, &$warnings ) { foreach ( $messages as $message ) { $key = $message->key(); $definition = $message->definition(); $translation = $message->translation(); $subcheck = 'namespace'; $namespaces = 'help|project|\{\{ns:project}}|mediawiki'; $matches = []; if ( preg_match( "/^($namespaces):[\w\s]+$/ui", $definition, $matches ) && !preg_match( "/^{$matches[1]}:.+$/u", $translation ) ) { $warnings[$key][] = [ [ 'pagename', $subcheck, $key, $code ], 'translate-checks-pagename', ]; } } } /** * Checks for some miscellaneous messages with special syntax. * * @param TMessage[] $messages Iterable list of TMessage objects. * @param string $code Language code of the translations. * @param array &$warnings Array where warnings are appended to. */ protected function miscMWChecks( $messages, $code, &$warnings ) { $timeList = [ 'protect-expiry-options', 'ipboptions' ]; foreach ( $messages as $message ) { $key = $message->key(); $definition = $message->definition(); $translation = $message->translation(); if ( in_array( strtolower( $key ), $timeList, true ) ) { $defArray = explode( ',', $definition ); $traArray = explode( ',', $translation ); $subcheck = 'timelist-count'; $defCount = count( $defArray ); $traCount = count( $traArray ); if ( $defCount !== $traCount ) { $warnings[$key][] = [ [ 'miscmw', $subcheck, $key, $code ], 'translate-checks-format', wfMessage( 'translate-checks-parametersnotequal' ) ->numParams( $traCount, $defCount )->text() ]; continue; } for ( $i = 0; $i < $defCount; $i++ ) { $defItems = array_map( 'trim', explode( ':', $defArray[$i] ) ); $traItems = array_map( 'trim', explode( ':', $traArray[$i] ) ); $subcheck = 'timelist-format'; if ( count( $traItems ) !== 2 ) { $warnings[$key][] = [ [ 'miscmw', $subcheck, $key, $code ], 'translate-checks-format', wfMessage( 'translate-checks-malformed', $traArray[$i] )->text() ]; continue; } $subcheck = 'timelist-format-value'; if ( $traItems[1] !== $defItems[1] ) { $warnings[$key][] = [ [ 'miscmw', $subcheck, $key, $code ], 'translate-checks-format', "$traItems[1] !== $defItems[1]", // @todo FIXME: i18n missing. ]; continue; } } } } } }