/** * @return null|string */ public function getLazySuggestionBox() { $this->mustBeKnownMessage(); if (!$this->handle->getCode()) { return null; } $url = SpecialPage::getTitleFor('Translate', 'editpage')->getLocalUrl(array('suggestions' => 'only', 'page' => $this->handle->getTitle()->getPrefixedDbKey(), 'loadgroup' => $this->group->getId())); $url = Xml::encodeJsVar($url); $id = Sanitizer::escapeId('tm-lazysug-' . $this->dialogID()); $target = self::jQueryPathId($id); $script = Html::inlineScript("jQuery({$target}).load({$url})"); $spinner = Html::element('div', array('class' => 'mw-ajax-loader')); return Html::rawElement('div', array('id' => $id), $script . $spinner); }
public function testGetNamespaceString() { $conf = $this->groupConfiguration; $conf['BASIC']['namespace'] = 'image'; $this->group = MessageGroupBase::factory($conf); $this->assertEquals(NS_IMAGE, $this->group->getNamespace(), "should parse string namespace name."); }
/** * @dataProvider unflattenDataProvider */ public function testUnflattenPural( $key, $value, $result ) { $ffs = $this->group->getFFS(); $this->assertEquals( $result, $ffs->unflattenPlural( $key, $value ) ); }
public static function getState(MessageGroup $group, $code) { $dbw = wfGetDB(DB_MASTER); $table = 'translate_groupreviews'; $field = 'tgr_state'; $conds = array('tgr_group' => $group->getId(), 'tgr_lang' => $code); return $dbw->selectField($table, $field, $conds, __METHOD__); }
/** * Get the message definition. Cached for performance. * * @return string */ public function getDefinition() { static $cache = array(); $key = $this->handle->getTitle()->getPrefixedText(); if (array_key_exists($key, $cache)) { return $cache[$key]; } if (method_exists($this->group, 'getMessageContent')) { $cache[$key] = $this->group->getMessageContent($this->handle); } else { $cache[$key] = $this->group->getMessage($this->handle->getKey(), $this->group->getSourceLanguage()); } return $cache[$key]; }
public function contents() { $optional = $this->context->msg('translate-optional')->escaped(); $this->doLinkBatch(); $sourceLang = Language::factory($this->group->getSourceLanguage()); $targetLang = Language::factory($this->collection->getLanguage()); $titleMap = $this->collection->keys(); $output = ''; $this->collection->initMessages(); // Just to be sure /** * @var TMessage $m */ foreach ($this->collection as $key => $m) { $tools = array(); /** * @var Title $title */ $title = $titleMap[$key]; $original = $m->definition(); $translation = $m->translation(); $hasTranslation = $translation !== null; if ($hasTranslation) { $message = $translation; $extraAttribs = self::getLanguageAttributes($targetLang); } else { $message = $original; $extraAttribs = self::getLanguageAttributes($sourceLang); } Hooks::run('TranslateFormatMessageBeforeTable', array(&$message, $m, $this->group, $targetLang, &$extraAttribs)); // Using Html::element( a ) because Linker::link is memory hog. // It takes about 20 KiB per call, and that times 5000 is quite // a lot of memory. $niceTitle = htmlspecialchars($this->context->getLanguage()->truncate($title->getPrefixedText(), -35)); $linkAttribs = array('href' => $title->getLocalUrl(array('action' => 'edit'))); $linkAttribs += TranslationEditPage::jsEdit($title, $this->group->getId()); $tools['edit'] = Html::element('a', $linkAttribs, $niceTitle); $anchor = 'msg_' . $key; $anchor = Xml::element('a', array('id' => $anchor, 'href' => "#{$anchor}"), "↓"); $extra = ''; if ($m->hasTag('optional')) { $extra = '<br />' . $optional; } $tqeData = $extraAttribs + array('data-title' => $title->getPrefixedText(), 'data-group' => $this->group->getId(), 'id' => 'tqe-anchor-' . substr(sha1($title->getPrefixedText()), 0, 12), 'class' => 'tqe-inlineeditable ' . ($hasTranslation ? 'translated' : 'untranslated')); $button = $this->getReviewButton($m); $status = $this->getReviewStatus($m); $leftColumn = $button . $anchor . $tools['edit'] . $extra . $status; if ($this->reviewMode) { $output .= Xml::tags('tr', array('class' => 'orig'), Xml::tags('td', array('rowspan' => '2'), $leftColumn) . Xml::tags('td', self::getLanguageAttributes($sourceLang), TranslateUtils::convertWhiteSpaceToHTML($original))); $output .= Xml::tags('tr', null, Xml::tags('td', $tqeData, TranslateUtils::convertWhiteSpaceToHTML($message))); } else { $output .= Xml::tags('tr', array('class' => 'def'), Xml::tags('td', null, $leftColumn) . Xml::tags('td', $tqeData, TranslateUtils::convertWhiteSpaceToHTML($message))); } $output .= "\n"; } return $output; }
/** * Keeps track of recently used message groups per user. */ public static function trackGroup(MessageGroup $group, User $user) { if ($user->isAnon()) { return true; } $groups = $user->getOption('translate-recent-groups', ''); if ($groups === '') { $groups = array(); } else { $groups = explode('|', $groups); } if (isset($groups[0]) && $groups[0] === $group->getId()) { return true; } array_unshift($groups, $group->getId()); $groups = array_unique($groups); $groups = array_slice($groups, 0, 5); $user->setOption('translate-recent-groups', implode('|', $groups)); // Promise to persist the data post-send DeferredUpdates::addCallableUpdate(function () use($user) { $user->saveSettings(); }); return true; }
protected function getPageDiff() { $this->mustBeKnownMessage(); $title = $this->handle->getTitle(); $key = $this->handle->getKey(); if (!$title->exists()) { return null; } $definitionTitle = Title::makeTitleSafe($title->getNamespace(), "{$key}/en"); if (!$definitionTitle || !$definitionTitle->exists()) { return null; } $db = wfGetDB(DB_MASTER); $conds = array('rt_page' => $title->getArticleID(), 'rt_type' => RevTag::getType('tp:transver')); $options = array('ORDER BY' => 'rt_revision DESC'); $latestRevision = $definitionTitle->getLatestRevID(); $translationRevision = $db->selectField('revtag', 'rt_value', $conds, __METHOD__, $options); if ($translationRevision === false) { return null; } // Using newFromId instead of newFromTitle, because the page might have been renamed $oldrev = Revision::newFromId($translationRevision); if (!$oldrev) { // And someone might still have deleted it return null; } $oldtext = ContentHandler::getContentText($oldrev->getContent()); $newContent = Revision::newFromTitle($definitionTitle, $latestRevision)->getContent(); $newtext = ContentHandler::getContentText($newContent); if ($oldtext === $newtext) { return null; } $diff = new DifferenceEngine(); if (method_exists('DifferenceEngine', 'setTextLanguage')) { $diff->setTextLanguage($this->group->getSourceLanguage()); } $oldContent = ContentHandler::makeContent($oldtext, $diff->getTitle()); $newContent = ContentHandler::makeContent($newtext, $diff->getTitle()); $diff->setContent($oldContent, $newContent); $diff->setReducedLineNumbers(); $diff->showDiffStyle(); return $diff->getDiff(wfMessage('tpt-diff-old')->escaped(), wfMessage('tpt-diff-new')->escaped()); }
public static function sourceExport(RequestContext $context, TranslateTask $task = null, MessageGroup $group, array $options) { if ($task || $options['taction'] !== 'export' || !$group instanceof WikiPageMessageGroup) { return true; } $page = TranslatablePage::newFromTitle($group->getTitle()); $collection = $group->initCollection($options['language']); $collection->loadTranslations(DB_MASTER); $text = $page->getParse()->getTranslationPageText($collection); $display = $page->getPageDisplayTitle($options['language']); if ($display) { $text = "{{DISPLAYTITLE:{$display}}}{$text}"; } $output = Html::element('textarea', array('rows' => 25), $text); $context->getOutput()->addHtml($output); return false; }
public function __construct( MessageCollection $collection, MessageGroup $group ) { $this->collection = $collection; $this->group = $group; $this->setHeaderText( 'table', $group->getLabel() ); $this->appendEditLinkParams( 'loadgroup', $group->getId() ); }
protected function exportGroup(MessageGroup $group, $config) { $server = TTMServer::factory($config); $server->setLogger($this); $id = $group->getId(); $sourceLanguage = $group->getSourceLanguage(); $stats = MessageGroupStats::forGroup($id); $collection = $group->initCollection($sourceLanguage); $collection->filter('ignored'); $collection->initMessages(); $server->beginBatch(); $inserts = array(); foreach ($collection->keys() as $mkey => $title) { $handle = new MessageHandle($title); $inserts[] = array($handle, $sourceLanguage, $collection[$mkey]->definition()); } while ($inserts !== array()) { $batch = array_splice($inserts, 0, $this->mBatchSize); $server->batchInsertDefinitions($batch); } $inserts = array(); foreach ($stats as $targetLanguage => $numbers) { if ($targetLanguage === $sourceLanguage) { continue; } if ($numbers[MessageGroupStats::TRANSLATED] === 0) { continue; } $collection->resetForNewLanguage($targetLanguage); $collection->filter('ignored'); $collection->filter('translated', false); $collection->loadTranslations(); foreach ($collection->keys() as $mkey => $title) { $handle = new MessageHandle($title); $inserts[] = array($handle, $sourceLanguage, $collection[$mkey]->translation()); } while (count($inserts) >= $this->mBatchSize) { $batch = array_splice($inserts, 0, $this->mBatchSize); $server->batchInsertTranslations($batch); } } while ($inserts !== array()) { $batch = array_splice($inserts, 0, $this->mBatchSize); $server->batchInsertTranslations($batch); } $server->endBatch(); }
protected function getGroupDescription(MessageGroup $group) { $description = $group->getDescription($this->getContext()); if ($description !== null) { return $this->getOutput()->parse($description, false); } return ''; }
protected function getGroupDescription(MessageGroup $group) { $description = $group->getDescription(); if ($description !== null) { global $wgOut; return $wgOut->parse($description, false); } return ''; }
/** * @param MessageGroup $group * @param string $code * @param string $type * @param array $params * @param int $limit * @return string HTML */ protected function formatChange(MessageGroup $group, $code, $type, $params, &$limit) { $key = $params['key']; $title = Title::makeTitleSafe($group->getNamespace(), "{$key}/{$code}"); $id = self::changeId($group->getId(), $code, $type, $key); if ($title && $title->exists() && $type === 'addition') { // The message has for some reason dropped out from cache // or perhaps it is being reused. In any case treat it // as a change for display, so the admin can see if // action is needed and let the message be processed. // Otherwise it will end up in the postponed category // forever and will prevent rebuilding the cache, which // leads to many other annoying problems. $type = 'change'; } elseif ($title && !$title->exists() && ($type === 'deletion' || $type === 'change')) { return ''; } $text = ''; if ($type === 'deletion') { $wiki = ContentHandler::getContentText(Revision::newFromTitle($title)->getContent()); $oldContent = ContentHandler::makeContent($wiki, $title); $newContent = ContentHandler::makeContent('', $title); $this->diff->setContent($oldContent, $newContent); $text = $this->diff->getDiff(Linker::link($title), ''); } elseif ($type === 'addition') { $oldContent = ContentHandler::makeContent('', $title); $newContent = ContentHandler::makeContent($params['content'], $title); $this->diff->setContent($oldContent, $newContent); $text = $this->diff->getDiff('', Linker::link($title)); } elseif ($type === 'change') { $wiki = ContentHandler::getContentText(Revision::newFromTitle($title)->getContent()); $handle = new MessageHandle($title); if ($handle->isFuzzy()) { $wiki = '!!FUZZY!!' . str_replace(TRANSLATE_FUZZY, '', $wiki); } $label = $this->msg('translate-manage-action-ignore')->text(); $actions = Xml::checkLabel($label, "i/{$id}", "i/{$id}"); $limit--; if ($group->getSourceLanguage() === $code) { $label = $this->msg('translate-manage-action-fuzzy')->text(); $actions .= ' ' . Xml::checkLabel($label, "f/{$id}", "f/{$id}", true); $limit--; } $oldContent = ContentHandler::makeContent($wiki, $title); $newContent = ContentHandler::makeContent($params['content'], $title); $this->diff->setContent($oldContent, $newContent); $text .= $this->diff->getDiff(Linker::link($title), $actions); } $hidden = Html::hidden($id, 1); $limit--; $text .= $hidden; $classes = "mw-translate-smg-change smg-change-{$type}"; if ($limit < 0) { // Don't add if one of the fields might get dropped of at submission return ''; } return Html::rawElement('div', array('class' => $classes), $text); }
/** * Returns full path the the cache file. * @return string */ protected function getCacheFileName() { return TranslateUtils::cacheFile("translate_groupcache-{$this->group->getId()}-{$this->code}.cdb"); }
public function testNoLanguageConf() { $translatableLanguages = $this->group->getTranslatableLanguages(); $this->assertNull($translatableLanguages); }
protected function exportGroup(MessageGroup $group, $multi = false) { // Make sure all existing connections are dead, // we can't use them in forked children. LBFactory::destroyInstance(); $server = TTMServer::primary(); $id = $group->getId(); $sourceLanguage = $group->getSourceLanguage(); if ($multi) { $stats = MessageGroupStats::forGroup($id); $this->statusLine("Loaded stats for {$id}\n"); } else { $this->statusLine("Loading stats... ", 4); $stats = MessageGroupStats::forGroup($id); $this->output("done!", 4); $this->statusLine("Inserting sources: ", 5); } $collection = $group->initCollection($sourceLanguage); $collection->filter('ignored'); $collection->filter('optional'); $collection->initMessages(); $sids = array(); $counter = 0; foreach ($collection->keys() as $mkey => $title) { $def = $collection[$mkey]->definition(); $sids[$mkey] = $server->insertSource($title, $sourceLanguage, $def); if (++$counter % $this->mBatchSize === 0 && !$multi) { wfWaitForSlaves(10); $this->output('.', 5); } } $total = count($sids); if ($multi) { $this->statusLine("Inserted {$total} source entries for {$id}\n"); } else { $this->output("{$total} entries", 5); $this->statusLine("Inserting translations...", 6); } $dbw = $server->getDB(DB_MASTER); foreach ($stats as $targetLanguage => $numbers) { if ($targetLanguage === $sourceLanguage) { continue; } if ($numbers[MessageGroupStats::TRANSLATED] === 0) { continue; } if (!$multi) { $this->output(sprintf("%19s ", $targetLanguage), $targetLanguage); } $collection->resetForNewLanguage($targetLanguage); $collection->filter('ignored'); $collection->filter('optional'); $collection->filter('translated', false); $collection->loadTranslations(); $inserts = array(); foreach ($collection->keys() as $mkey => $title) { $inserts[] = array('tmt_sid' => $sids[$mkey], 'tmt_lang' => $targetLanguage, 'tmt_text' => $collection[$mkey]->translation()); } do { $batch = array_splice($inserts, 0, $this->mBatchSize); $dbw->insert('translate_tmt', $batch, __METHOD__); if (!$multi) { $this->output('.', $targetLanguage); } wfWaitForSlaves(10); } while (count($inserts)); } if ($multi) { $this->statusLine("Inserted translations for {$id}\n"); } }
/** * Do some conflict resolution for translations. * @param string $code Language code. * @param bool|int $startTs Time of the last export (changes in wiki after * this will conflict) * @param bool|int $endTs Time of the last export (changes in source before * this won't conflict) * @param bool|int $changeTs When change happened in the source. */ public function checkConflicts($code, $startTs = false, $endTs = false, $changeTs = false) { $messages = $this->group->load($code); if (!count($messages)) { return; } $collection = $this->group->initCollection($code); $collection->filter('ignored'); $collection->loadTranslations(); foreach ($messages as $key => $translation) { if (!isset($collection[$key])) { continue; } // @todo Temporary exception. Should be fixed elsewhere more generically. if ($translation == '{{PLURAL:GETTEXT|}}') { return; } $title = Title::makeTitleSafe($this->group->getNamespace(), "{$key}/{$code}"); $page = $title->getPrefixedText(); if ($collection[$key]->translation() === null) { $this->reportProgress("Importing {$page} as a new translation\n", 'importing'); $this->import($title, $translation, 'Importing a new translation'); continue; } $current = str_replace(TRANSLATE_FUZZY, '', $collection[$key]->translation()); $translation = str_replace(TRANSLATE_FUZZY, '', $translation); if ($translation === $current) { continue; } $this->reportProgress("Conflict in " . $this->color('bold', $page) . "!", $page); $iso = 'xnY-xnm-xnd"T"xnH:xni:xns'; $lang = RequestContext::getMain()->getLanguage(); // Finally all is ok, now lets start comparing timestamps // Make sure we are comparing timestamps in same format $wikiTs = $this->getLastGoodChange($title, $startTs); if ($wikiTs) { $wikiTs = wfTimestamp(TS_UNIX, $wikiTs); $wikiDate = $lang->sprintfDate($iso, wfTimestamp(TS_MW, $wikiTs)); } else { $wikiDate = 'Unknown'; } if ($startTs) { $startTs = wfTimestamp(TS_UNIX, $startTs); } if ($endTs) { $endTs = wfTimestamp(TS_UNIX, $endTs); } if ($changeTs) { $changeTs = wfTimestamp(TS_UNIX, $changeTs); $changeDate = $lang->sprintfDate($iso, wfTimestamp(TS_MW, $changeTs)); } else { $changeDate = 'Unknown'; } if ($changeTs) { if ($wikiTs > $startTs && $changeTs <= $endTs) { $this->reportProgress(" →Changed in wiki after export: IGNORE", $page); continue; } elseif (!$wikiTs || $changeTs > $endTs && $wikiTs < $startTs) { $this->reportProgress(" →Changed in source after export: IMPORT", $page); $this->import($title, $translation, 'Updating translation from external source'); continue; } } if (!$this->interactive) { continue; } $this->reportProgress(" →Needs manual resolution", $page); $this->reportProgress("Source translation at {$changeDate}:", 'source'); $this->reportProgress($this->color('blue', $translation), 'source'); $this->reportProgress("Wiki translation at {$wikiDate}:", 'translation'); $this->reportProgress($this->color('green', $current), 'translation'); do { $this->reportProgress("Resolution: [S]kip [I]mport [C]onflict: ", 'foo'); // @todo Find an elegant way to use Maintenance::readconsole(). $action = fgets(STDIN); $action = strtoupper(trim($action)); if ($action === 'S') { break; } if ($action === 'I') { $this->import($title, $translation, 'Updating translation from external source'); break; } if ($action === 'C') { $this->import($title, TRANSLATE_FUZZY . $translation, 'Edit conflict between wiki and source'); break; } } while (true); } }
/** * @param MessageGroup $group * @param string $code Language code * @return int[] ( total, translated, fuzzy, proofread ) */ protected static function calculateGroup($group, $code) { global $wgTranslateDocumentationLanguageCode; # Calculate if missing and store in the db $collection = $group->initCollection($code); if ($code === $wgTranslateDocumentationLanguageCode) { $ffs = $group->getFFS(); if ($ffs instanceof GettextFFS) { $template = $ffs->read('en'); $infile = array(); foreach ($template['TEMPLATE'] as $key => $data) { if (isset($data['comments']['.'])) { $infile[$key] = '1'; } } $collection->setInFile($infile); } } $collection->filter('ignored'); $collection->filter('optional'); // Store the count of real messages for later calculation. $total = count($collection); // Count fuzzy first. $collection->filter('fuzzy'); $fuzzy = $total - count($collection); // Count the completed translations. $collection->filter('hastranslation', false); $translated = count($collection); // Count how many of the completed translations // have been proofread $collection->filter('reviewer', false); $proofread = count($collection); return array(self::TOTAL => $total, self::TRANSLATED => $translated, self::FUZZY => $fuzzy, self::PROOFREAD => $proofread); }
/** * Returns full path to the old cache file location. * @return string */ protected function getOldCacheFileName() { $cacheFileName = "translate_groupcache-{$this->group->getId()}-{$this->code}.cdb"; return TranslateUtils::cacheFile($cacheFileName); }
protected static function expandAggregates( MessageGroup $group ) { $flattened = array( $group->getId() ); if ( $group instanceof AggregateMessageGroup ) { foreach ( $group->getGroups() as $subgroup ) { $flattened = array_merge( $flattened, self::expandAggregates( $subgroup ) ); } } return $flattened; }
/** * Returns the message groups this message group is part of. * @since 2011-12-25 * @return array */ public static function getParentGroups(MessageGroup $group) { // Take the first message, get a handle for it and check // if that message belongs to other groups. Those are the // parent aggregate groups. Ideally we loop over all keys, // but this should be enough. $keys = array_keys($group->getDefinitions()); $title = Title::makeTitle($group->getNamespace(), $keys[0]); $handle = new MessageHandle($title); $ids = $handle->getGroupIds(); foreach ($ids as $index => $id) { if ($id === $group->getId()) { unset($ids[$index]); } } return $ids; }
/** * Actually creates the table for single message group, unless it * is blacklisted or hidden by filters. * @param MessageGroup $group * @param array $cache * @param MessageGroup $parent * @return string */ protected function makeGroupRow(MessageGroup $group, array $cache, MessageGroup $parent = null) { $groupId = $group->getId(); if ($this->table->isBlacklisted($groupId, $this->target) !== null) { return ''; } $stats = $cache[$groupId]; $total = $stats[MessageGroupStats::TOTAL]; $translated = $stats[MessageGroupStats::TRANSLATED]; $fuzzy = $stats[MessageGroupStats::FUZZY]; // Quick checks to see whether filters apply if ($this->noComplete && $fuzzy === 0 && $translated === $total) { return ''; } if ($this->noEmpty && $translated === 0 && $fuzzy === 0) { return ''; } // Calculation of summary row values if (!$group instanceof AggregateMessageGroup) { if (!isset($this->statsCounted[$groupId])) { $this->totals = MessageGroupStats::multiAdd($this->totals, $stats); $this->statsCounted[$groupId] = true; } } $state = $this->getWorkflowStateValue($groupId); $params = $stats; $params[] = $state; $params[] = $groupId; $params[] = $this->getLanguage()->getCode(); $params[] = $this->target; $cachekey = wfMemcKey(__METHOD__, implode('-', $params)); $cacheval = wfGetCache(CACHE_ANYTHING)->get($cachekey); if (!$this->purge && is_string($cacheval)) { return $cacheval; } $extra = array(); if ($total === null) { $this->incomplete = true; } elseif ($translated === $total) { $extra = array('action' => 'proofread'); } $rowParams = array(); $rowParams['data-groupid'] = $groupId; $rowParams['class'] = get_class($group); if ($parent) { $rowParams['data-parentgroup'] = $parent->getId(); } $out = "\t" . Html::openElement('tr', $rowParams); $out .= "\n\t\t" . Html::rawElement('td', array(), $this->table->makeGroupLink($group, $this->target, $extra)); $out .= $this->table->makeNumberColumns($stats); $out .= $this->getWorkflowStateCell($groupId, $state); $out .= "\n\t" . Html::closeElement('tr') . "\n"; wfGetCache(CACHE_ANYTHING)->set($cachekey, $out, 3600 * 24); return $out; }
/** * Get URLs for icons if available. * @param MessageGroup $g * @param int $size Length of the edge of a bounding box to fit the icon. * @return null|array * @since 2013-04-01 */ public static function getIcon(MessageGroup $g, $size) { $icon = $g->getIcon(); if (substr($icon, 0, 7) !== 'wiki://') { return null; } $formats = array(); $filename = substr($icon, 7); $file = wfFindFile($filename); if (!$file) { wfWarn("Unknown message group icon file {$icon}"); return null; } if ($file->isVectorized()) { $formats['vector'] = $file->getFullUrl(); } $formats['raster'] = $file->createThumb($size, $size); return $formats; }
/** * Gets the description of a group. This is a bit slow thing to do for * thousand+ groups, so some caching is involved. * @param $group MessageGroup * @return string Plain text */ public function getGroupDescription( MessageGroup $group ) { $code = $this->lang->getCode(); $cache = wfGetCache( CACHE_ANYTHING ); $key = wfMemckey( "translate-groupdesc-$code-" . $group->getId() ); $desc = $cache->get( $key ); if ( is_string( $desc ) ) { return $desc; } $realFunction = array( 'MessageCache', 'singleton' ); if ( is_callable( $realFunction ) ) { $mc = MessageCache::singleton(); } else { global $wgMessageCache; $mc = $wgMessageCache; } $desc = $mc->transform( $group->getDescription(), true, $this->lang ); $cache->set( $key, $desc ); return $desc; }
/** * @param MessageGroup $group * @param string $prefix * @return string */ protected function htmlIdForGroup(MessageGroup $group, $prefix = '') { $id = sha1($group->getId()); $id = substr($id, 5, 8); return $prefix . $id; }
/** * Given a group, message key and language code, creates a title for the * translation page. * * @param MessageGroup $group * @param string $key Message key * @param string $code Language code * @return Title */ public static function makeTranslationTitle($group, $key, $code) { $ns = $group->getNamespace(); return Title::makeTitleSafe($ns, "{$key}/{$code}"); }