locales = $args; } /** * Print command usage information. */ function usage() { echo "Script to test locales for consistency\n" . "Usage: {$this->scriptName} [localeName (optional)] ...\n"; } /** * Test locales. */ function execute() { // Flush the file cache just to be certain we're using // the most recent stuff import('cache.CacheManager'); $cacheManager =& CacheManager::getManager(); $cacheManager->flush('locale'); // Load plugins so that their locale data is included too $plugins = array(); foreach (PluginRegistry::getCategories() as $category) { echo "Loading plugin category \"$category\"...\n"; $morePlugins = PluginRegistry::loadCategory($category); if (is_array($morePlugins)) $plugins += $morePlugins; } foreach (Locale::getAllLocales() as $locale => $name) { if (!empty($this->locales) && !in_array($locale, $this->locales)) continue; if ($locale != MASTER_LOCALE) { echo "Testing locale \"$name\" ($locale) against reference locale " . MASTER_LOCALE . ".\n"; $this->testLocale($locale, MASTER_LOCALE, $plugins); $this->testEmails($locale, MASTER_LOCALE); } } } function testEmails($locale, $referenceLocale) { import('install.Installer'); // Bring in data dir $errors = array( ); $xmlParser =& new XMLParser(); $referenceEmails =& $xmlParser->parse( INSTALLER_DATA_DIR . "/data/locale/$referenceLocale/email_templates_data.xml" ); $emails =& $xmlParser->parse( INSTALLER_DATA_DIR . "/data/locale/$locale/email_templates_data.xml" ); $emailsTable =& $emails->getChildByName('table'); $referenceEmailsTable =& $referenceEmails->getChildByName('table'); $matchedReferenceEmails = array(); // Pass 1: For all translated emails, check that they match // against reference translations. for ($emailIndex = 0; ($email =& $emailsTable->getChildByName('row', $emailIndex)) !== null; $emailIndex++) { // Extract the fields from the email to be tested. $fields = $this->extractFields($email); // Locate the reference email and extract its fields. for ($referenceEmailIndex = 0; ($referenceEmail =& $referenceEmailsTable->getChildByName('row', $referenceEmailIndex)) !== null; $referenceEmailIndex++) { $referenceFields = $this->extractFields($referenceEmail); if ($referenceFields['email_key'] == $fields['email_key']) break; } // Check if a matching reference email was found. if (!isset($referenceEmail) || $referenceEmail === null) { $errors[EMAIL_ERROR_EXTRA_EMAIL][] = array( 'key' => $fields['email_key'] ); continue; } // We've successfully found a matching reference email. // Compare it against the translation. $bodyParams = $this->getParameterNames($fields['body']); $referenceBodyParams = $this->getParameterNames($referenceFields['body']); if ($bodyParams !== $referenceBodyParams) { $errors[EMAIL_ERROR_DIFFERING_PARAMS][] = array( 'key' => $fields['email_key'], 'mismatch' => array_diff($bodyParams, $referenceBodyParams) ); } $subjectParams = $this->getParameterNames($fields['subject']); $referenceSubjectParams = $this->getParameterNames($referenceFields['subject']); if ($subjectParams !== $referenceSubjectParams) { $errors[EMAIL_ERROR_DIFFERING_PARAMS][] = array( 'key' => $fields['email_key'], 'mismatch' => array_diff($subjectParams, $referenceSubjectParams) ); } $matchedReferenceEmails[] = $fields['email_key']; unset($email); unset($referenceEmail); } // Pass 2: Make sure that there are no missing translations. for ($referenceEmailIndex = 0; ($referenceEmail =& $referenceEmailsTable->getChildByName('row', $referenceEmailIndex)) !== null; $referenceEmailIndex++) { // Extract the fields from the email to be tested. $referenceFields = $this->extractFields($referenceEmail); if (!in_array($referenceFields['email_key'], $matchedReferenceEmails)) { $errors[EMAIL_ERROR_MISSING_EMAIL][] = array( 'key' => $referenceFields['email_key'] ); } } $this->displayEmailErrors($locale, $errors); } function extractFields(&$node) { $returner = array(); foreach ($node->getChildren() as $field) if ($field->getName() === 'field') { $returner[$field->getAttribute('name')] = $field->getValue(); } return $returner; } function displayEmailErrors($locale, $errors) { ksort($errors); echo "\nERROR REPORT FOR EMAILS IN \"$locale\":\n"; echo "-----------------------------------\n"; foreach ($errors as $type => $errorList) { if (!empty($errorList)) switch ($type) { case EMAIL_ERROR_MISSING_EMAIL: echo "The following messages are missing from the emails file and need translation.\n"; foreach ($errorList as $error) echo " - " . $error['key'] . "\n"; break; case EMAIL_ERROR_EXTRA_EMAIL: echo "\nThe following emails are not in the master translation and may be deleted:\n"; foreach ($errorList as $error) echo " - " . $error['key'] . "\n"; break; case EMAIL_ERROR_DIFFERING_PARAMS: echo "\nThe following emails are missing parameters or have extra parameters and need\n"; echo "correcting against the master translation:\n"; foreach ($errorList as $error) { echo " - " . $error['key'] . "\n"; echo " Mismatching parameter(s): " . implode(', ', $error['mismatch']) . "\n"; } break; default: die("Unknown error type \"$type\"!\n"); } } } function testLocale($locale, $referenceLocale, $plugins) { $errors = array( LOCALE_ERROR_MISSING_KEY => array(), LOCALE_ERROR_EXTRA_KEY => array(), LOCALE_ERROR_SUSPICIOUS_LENGTH => array(), LOCALE_ERROR_DIFFERING_PARAMS => array() ); $localeCache =& Locale::_getCache($locale); $referenceLocaleCache =& Locale::_getCache($referenceLocale); $localeContents =& $localeCache->getContents(); $referenceContents =& $referenceLocaleCache->getContents(); // Add locale data for plugins foreach ($plugins as $plugin) { $pluginCache = $plugin->_getCache($locale); $referencePluginCache = $plugin->_getCache($referenceLocale); $localeContents += $pluginCache->getContents(); $referenceContents += $referencePluginCache->getContents(); } foreach ($referenceContents as $key => $referenceValue) { if (!isset($localeContents[$key])) { $errors[LOCALE_ERROR_MISSING_KEY][] = array( 'key' => $key, 'locale' => $locale ); continue; } $value = $localeContents[$key]; // Watch for suspicious lengths. if (!$this->checkLengths($referenceValue, $value)) { $errors[LOCALE_ERROR_SUSPICIOUS_LENGTH][] = array( 'key' => $key, 'locale' => $locale, 'reference' => $referenceValue, 'value' => $value ); } $referenceParams = $this->getParameterNames($referenceValue); $params = $this->getParameterNames($value); if ($referenceParams !== $params) { $errors[LOCALE_ERROR_DIFFERING_PARAMS][] = array( 'key' => $key, 'locale' => $locale, 'mismatch' => array_diff($referenceParams, $params) ); } // After processing a key, remove it from the list; // this way, the remainder at the end of the loop // will be extra unnecessary keys. unset($localeContents[$key]); } // Leftover keys are extraneous. foreach ($localeContents as $key => $value) { $errors[LOCALE_ERROR_EXTRA_KEY][] = array( 'key' => $key, 'locale' => $locale ); } $this->displayLocaleErrors($locale, $errors); } function displayLocaleErrors($locale, $errors) { ksort($errors); echo "\nERROR REPORT FOR LOCALE STRINGS IN \"$locale\":\n"; echo "---------------------------------------\n"; foreach ($errors as $type => $errorList) { if (!empty($errorList)) switch ($type) { case LOCALE_ERROR_MISSING_KEY: echo "The following keys are missing from a locale file and need to be translated.\n"; foreach ($errorList as $error) echo " - " . $error['key'] . "\n"; break; case LOCALE_ERROR_EXTRA_KEY: echo "\nThe following keys are not in the master translation and may be deleted:\n"; foreach ($errorList as $error) echo " - " . $error['key'] . "\n"; break; case LOCALE_ERROR_SUSPICIOUS_LENGTH: echo "\nThe following keys have suspicious lengths compared with the master translation\n"; echo "and may need checking:\n"; foreach ($errorList as $error) { $reference = $this->truncate($error['reference'], 65); $value = $this->truncate($error['value'], 65); echo " - " . $error['key'] . "\n"; echo " \"" . $reference . "\" vs.\n"; echo " \"" . $value . "\" ($locale)\n"; } break; case LOCALE_ERROR_DIFFERING_PARAMS: echo "\nThe following keys are missing parameters or have extra parameters and need\n"; echo "correcting against the master translation:\n"; foreach ($errorList as $error) { echo " - " . $error['key'] . "\n"; echo " Mismatching parameter(s): " . implode(', ', $error['mismatch']) . "\n"; } break; default: die("Unknown error type \"$type\"!\n"); } } } function truncate($value, $length = 80, $ellipsis = '...') { if (String::strlen($value) > $length) { $value = String::substr($value, 0, $length - String::strlen($ellipsis)); return $value . $ellipsis; } return $value; } function checkLengths($reference, $value) { $referenceLength = String::strlen($reference); $length = String::strlen($value); $lengthDifference = abs($referenceLength - $length); if ($referenceLength == 0) return false; if ($lengthDifference / $referenceLength > 1 && $lengthDifference > 10) return false; return true; } /** * Given a locale string, get the list of parameter references of the * form {$myParameterName}. * @param $source string * @return array */ function getParameterNames($source) { $matches = null; String::regexp_match_get('/({\$[^}]+})/' /* '/{\$[^}]+})/' */, $source, $matches); array_shift($matches); // Knock the top element off the array return $matches; } } $tool = &new localeCheck(isset($argv) ? $argv : array()); $tool->execute(); ?>