47 protected $limit = 25;
49 private $ttmServerFactory;
52 parent::__construct(
'SearchTranslations' );
54 TranslateUtils::getPlaceholder(),
55 TranslateUtils::getPlaceholder(),
58 $this->ttmServerFactory = $ttmServerFactory;
61 public function execute( $par ) {
62 global $wgLanguageCode;
64 $this->checkPermissions();
66 $server = $this->ttmServerFactory->getDefault();
68 throw new ErrorPageError(
'tux-sst-nosolr-title',
'tux-sst-nosolr-body' );
71 $out = $this->getOutput();
72 $out->addModuleStyles(
'jquery.uls.grid' );
73 $out->addModuleStyles(
'ext.translate.specialpages.styles' );
74 $out->addModuleStyles(
'ext.translate.special.translate.styles' );
75 $out->addModuleStyles( [
'mediawiki.ui.button',
'mediawiki.ui.input',
'mediawiki.ui.checkbox' ] );
76 $out->addModules(
'ext.translate.special.searchtranslations' );
77 $out->addModules(
'ext.translate.special.searchtranslations.operatorsuggest' );
78 $out->addHelpLink(
'Help:Extension:Translate#searching' );
79 $out->addJsConfigVars(
'wgTranslateLanguages', TranslateUtils::getLanguageNames(
null ) );
81 $this->opts = $opts =
new FormOptions();
82 $opts->add(
'query',
'' );
83 $opts->add(
'sourcelanguage', $wgLanguageCode );
84 $opts->add(
'language',
'' );
85 $opts->add(
'group',
'' );
86 $opts->add(
'grouppath',
'' );
87 $opts->add(
'filter',
'' );
88 $opts->add(
'match',
'' );
89 $opts->add(
'case',
'' );
90 $opts->add(
'limit', $this->limit );
91 $opts->add(
'offset', 0 );
93 $opts->fetchValuesFromRequest( $this->getRequest() );
95 $queryString = $opts->getValue(
'query' );
97 if ( $queryString ===
'' ) {
98 $this->showEmptySearch();
102 $search = $this->getSearchInput( $queryString );
104 $options = $params = $opts->getAllValues();
105 $filter = $opts->getValue(
'filter' );
107 if ( $opts->getValue(
'language' ) ===
'' ) {
108 $options[
'language'] = $this->getLanguage()->getCode();
111 if ( in_array( $filter, $translationSearch->getAvailableFilters() ) ) {
112 if ( $options[
'language'] === $options[
'sourcelanguage'] ) {
113 $this->showSearchError( $search, $this->msg(
'tux-sst-error-language' ) );
117 $opts->setValue(
'language', $options[
'language'] );
118 $documents = $translationSearch->getDocuments();
119 $total = $translationSearch->getTotalHits();
120 $resultset = $translationSearch->getResultSet();
122 $resultset = $server->search( $queryString, $params, $this->hl );
123 $documents = $server->getDocuments( $resultset );
124 $total = $server->getTotalHits( $resultset );
127 $message = $e->getMessage();
129 if ( preg_match(
'/^Result window is too large/', $message ) ) {
130 $this->showSearchError( $search, $this->msg(
'tux-sst-error-offset' ) );
135 error_log(
'Translation search server unavailable: ' . $e->getMessage() );
136 throw new ErrorPageError(
'tux-sst-solr-offline-title',
'tux-sst-solr-offline-body' );
140 $facets = $server->getFacets( $resultset );
143 if ( $facets[
'language'] !== [] ) {
144 if ( $filter !==
'' ) {
145 $facets[
'language'] = array_merge(
147 [ $opts->getValue(
'language' ) => $total ]
150 $facetHtml = Html::element(
'div',
151 [
'class' =>
'row facet languages',
152 'data-facets' => FormatJson::encode( $this->getLanguages( $facets[
'language'] ) ),
153 'data-language' => $opts->getValue(
'language' ),
155 $this->msg(
'tux-sst-facet-language' )->text()
159 if ( $facets[
'group'] !== [] ) {
160 $facetHtml .= Html::element(
'div',
161 [
'class' =>
'row facet groups',
162 'data-facets' => FormatJson::encode( $this->getGroups( $facets[
'group'] ) ),
163 'data-group' => $opts->getValue(
'group' ) ],
164 $this->msg(
'tux-sst-facet-group' )->text()
171 $title = Title::newFromText( $queryString );
172 if ( $title && !in_array( $filter, $translationSearch->getAvailableFilters() ) ) {
174 $code = $handle->getCode();
175 $language = $opts->getValue(
'language' );
176 if ( $code !==
'' && $code !== $language && $handle->isValid() ) {
185 'wiki' => WikiMap::getCurrentWikiId(),
186 'localid' => $handle->getTitleForBase()->getPrefixedText(),
187 'content' => $aid->getData()[
'value'],
188 'language' => $handle->getCode(),
190 array_unshift( $documents, $document );
195 foreach ( $documents as $document ) {
196 $text = $document[
'content'];
197 $text = TranslateUtils::convertWhiteSpaceToHTML( $text );
199 list( $pre, $post ) = $this->hl;
200 $text = str_replace( $pre,
'<strong class="tux-search-highlight">', $text );
201 $text = str_replace( $post,
'</strong>', $text );
203 $title = Title::newFromText( $document[
'localid'] .
'/' . $document[
'language'] );
210 'class' =>
'row tux-message',
211 'data-title' => $title->getPrefixedText(),
212 'data-language' => $document[
'language'],
217 if ( $handle->isValid() ) {
218 $uri = TranslateUtils::getEditorUrl( $handle );
219 $link = Html::element(
222 $this->msg(
'tux-sst-edit' )->text()
225 $url = wfParseUrl( $document[
'uri'] );
226 $domain = $url[
'host'];
227 $link = Html::element(
229 [
'href' => $document[
'uri'] ],
230 $this->msg(
'tux-sst-view-foreign', $domain )->text()
234 $access = Html::rawElement(
236 [
'class' =>
'row tux-edit tux-message-item' ],
240 $titleText = $title->getPrefixedText();
242 'class' =>
'row tux-title',
246 $language = Language::factory( $document[
'language'] );
248 'class' =>
'row tux-text',
249 'lang' => $language->getHtmlCode(),
250 'dir' => $language->getDir(),
253 $resultsHtml .= Html::openElement(
'div', $resultAttribs )
254 . Html::rawElement(
'div', $textAttribs, $text )
255 . Html::element(
'div', $titleAttribs, $titleText )
257 . Html::closeElement(
'div' );
260 $resultsHtml .= Html::rawElement(
'hr', [
'class' =>
'tux-pagination-line' ] );
263 $offset = $this->opts->getValue(
'offset' );
264 $params = $this->opts->getChangedValues();
266 if ( $total - $offset > $this->limit ) {
267 $newParams = [
'offset' => $offset + $this->limit ] + $params;
269 'class' =>
'mw-ui-button pager-next',
270 'href' => $this->getPageTitle()->getLocalURL( $newParams ),
272 $next = Html::element(
'a', $attribs, $this->msg(
'tux-sst-next' )->text() );
275 $newParams = [
'offset' => max( 0, $offset - $this->limit ) ] + $params;
277 'class' =>
'mw-ui-button pager-prev',
278 'href' => $this->getPageTitle()->getLocalURL( $newParams ),
280 $prev = Html::element(
'a', $attribs, $this->msg(
'tux-sst-prev' )->text() );
283 $resultsHtml .= Html::rawElement(
'div', [
'class' =>
'tux-pagination-links' ],
287 $count = $this->msg(
'tux-sst-count' )->numParams( $total )->escaped();
289 $this->showSearch( $search, $count, $facetHtml, $resultsHtml, $total );
292 private function getLanguages( array $facet ): array {
295 $nondefaults = $this->opts->getChangedValues();
296 $selected = $this->opts->getValue(
'language' );
297 $filter = $this->opts->getValue(
'filter' );
299 foreach ( $facet as $key => $value ) {
300 if ( $filter !==
'' && $key === $selected ) {
301 unset( $nondefaults[
'language'] );
302 unset( $nondefaults[
'filter'] );
303 } elseif ( $filter !==
'' ) {
304 $nondefaults[
'language'] = $key;
305 $nondefaults[
'filter'] = $filter;
306 } elseif ( $key === $selected ) {
307 unset( $nondefaults[
'language'] );
309 $nondefaults[
'language'] = $key;
312 $url = $this->getPageTitle()->getLocalURL( $nondefaults );
313 $value = $this->getLanguage()->formatNum( $value );
324 private function getGroups( array $facet ): array {
325 $structure = MessageGroups::getGroupStructure();
326 return $this->makeGroupFacetRows( $structure, $facet );
329 private function makeGroupFacetRows(
333 string $pathString =
''
337 $nondefaults = $this->opts->getChangedValues();
338 $selected = $this->opts->getValue(
'group' );
339 $path = explode(
'|', $this->opts->getValue(
'grouppath' ) );
341 foreach ( $groups as $mixed ) {
342 $subgroups = $group = $mixed;
344 if ( is_array( $mixed ) ) {
345 $group = array_shift( $subgroups );
349 '@phan-var \MessageGroup $group';
350 $id = $group->getId();
352 if ( $id !== $selected && !isset( $counts[$id] ) ) {
356 if ( $id === $selected ) {
357 unset( $nondefaults[
'group'] );
358 $nondefaults[
'grouppath'] = $pathString;
360 $nondefaults[
'group'] = $id;
361 $nondefaults[
'grouppath'] = $pathString . $id;
364 $value = $counts[$id] ?? 0;
369 'label' => $group->getLabel(),
372 if ( isset( $path[$level] ) && $path[$level] === $id ) {
373 $output[$id][
'groups'] = $this->makeGroupFacetRows(
385 private function showSearch(
392 $messageSelector = $this->messageSelector();
393 $this->getOutput()->addHTML( <<<HTML
394<div
class=
"grid tux-searchpage">
395 <div
class=
"row tux-searchboxform">
396 <div
class=
"tux-search-tabs offset-by-three">$messageSelector</div>
397 <div
class=
"row tux-search-options">
398 <div
class=
"offset-by-three nine columns tux-search-inputs">
399 <div
class=
"row searchinput">$search</div>
400 <div
class=
"row count">$count</div>
407 $query = trim( $this->opts->getValue(
'query' ) );
408 $hasSpace = preg_match(
'/\s/', $query );
409 $match = $this->opts->getValue(
'match' );
411 if ( $total > $size && $match !==
'all' && $hasSpace ) {
412 $params = $this->opts->getChangedValues();
413 $params = [
'match' =>
'all' ] + $params;
414 $linkText = $this->msg(
'tux-sst-link-all-match' )->text();
415 $link = $this->getPageTitle()->getFullURL( $params );
416 $link =
"<span class='plainlinks'>[$link $linkText]</span>";
418 $out = $this->getOutput();
421 $out->msg(
'tux-sst-match-message', $link )->parse()
426 $this->getOutput()->addHTML( <<<HTML
427 <div
class=
"row searchcontent">
428 <div
class=
"three columns facets">$facets</div>
429 <div
class=
"nine columns results">$results</div>
436 private function showEmptySearch():
void {
437 $search = $this->getSearchInput(
'' );
438 $this->getOutput()->addHTML( <<<HTML
439<div
class=
"grid tux-searchpage">
440 <div
class=
"row searchinput">
441 <div
class=
"nine columns offset-by-three">$search</div>
448 private function showSearchError(
string $search, Message $message ):
void {
449 $messageSelector = $this->messageSelector();
450 $messageHTML = Html::errorBox(
455 $this->getOutput()->addHTML( <<<HTML
456<div
class=
"grid tux-searchpage">
457 <div
class=
"row tux-searchboxform">
458 <div
class=
"tux-search-tabs offset-by-three">$messageSelector</div>
459 <div
class=
"row tux-search-options">
460 <div
class=
"offset-by-three nine columns tux-search-inputs">
461 <div
class=
"row searchinput">$search</div>
472 private function ellipsisSelector(
string $key,
string $value ):
string {
473 $nondefaults = $this->opts->getChangedValues();
474 $taskParams = [
'filter' => $value ] + $nondefaults;
475 ksort( $taskParams );
476 $href = $this->getPageTitle()->getLocalURL( $taskParams );
477 $link = Html::element(
'a',
482 $this->msg(
'tux-sst-ellipsis-' . $key )->text()
485 $container = Html::rawElement(
'li', [
487 'data-filter' => $value,
488 'data-title' => $key,
495 private function messageSelector():
string {
496 $nondefaults = $this->opts->getChangedValues();
497 $output = Html::openElement(
'div', [
'class' =>
'row tux-messagetable-header' ] );
498 $output .= Html::openElement(
'div', [
'class' =>
'nine columns' ] );
499 $output .= Html::openElement(
'ul', [
'class' =>
'row tux-message-selector' ] );
502 'translated' =>
'translated',
503 'untranslated' =>
'untranslated'
507 'outdated' =>
'fuzzy'
510 $selected = $this->opts->getValue(
'filter' );
511 if ( in_array( $selected, $ellipsisOptions ) ) {
512 $ellipsisOptions = array_slice( $tabs, -1 );
516 $tabs = array_merge( $tabs, [
'outdated' => $selected ] );
517 } elseif ( !in_array( $selected, $tabs ) ) {
521 $container = Html::openElement(
'ul', [
'class' =>
'column tux-message-selector' ] );
522 foreach ( $ellipsisOptions as $optKey => $optValue ) {
523 $container .= $this->ellipsisSelector( $optKey, $optValue );
526 $sourcelanguage = $this->opts->getValue(
'sourcelanguage' );
527 $sourcelanguage = TranslateUtils::getLanguageName( $sourcelanguage );
528 foreach ( $tabs as $tab => $filter ) {
534 $tabClass =
"tux-sst-$tab";
535 $taskParams = [
'filter' => $filter ] + $nondefaults;
536 ksort( $taskParams );
537 $href = $this->getPageTitle()->getLocalURL( $taskParams );
538 if ( $tab ===
'default' ) {
539 $link = Html::element(
542 $this->msg( $tabClass )->text()
545 $link = Html::element(
548 $this->msg( $tabClass, $sourcelanguage )->text()
552 if ( $selected === $filter ) {
553 $tabClass .=
' selected';
555 $output .= Html::rawElement(
'li', [
556 'class' => [
'column', $tabClass ],
557 'data-filter' => $filter,
558 'data-title' => $tab,
563 $output .= Html::openElement(
'li', [
'class' =>
'column more' ] ) .
566 Html::closeElement(
'li' );
568 $output .= Html::closeElement(
'ul' ) . Html::closeElement(
'div' ) . Html::closeElement(
'div' );
573 private function getSearchInput(
string $query ) {
575 'placeholder' => $this->msg(
'tux-sst-search-ph' )->text(),
576 'class' =>
'searchinputbox mw-ui-input',
577 'dir' => $this->getLanguage()->getDir(),
580 $title = Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() );
581 $input = Xml::input(
'query',
false, $query, $attribs );
582 $submit = Xml::submitButton(
583 $this->msg(
'tux-sst-search' )->text(),
584 [
'class' =>
'mw-ui-button mw-ui-progressive' ]
587 $nondefaults = $this->opts->getChangedValues();
588 $checkLabel = Xml::checkLabel(
589 $this->msg(
'tux-sst-case-sensitive' )->text(),
591 'tux-case-sensitive',
592 isset( $nondefaults[
'case'] )
594 $checkLabel = Html::openElement(
596 [
'class' =>
'tux-search-operators mw-ui-checkbox' ]
599 Html::closeElement(
'div' );
601 $lang = $this->getRequest()->getVal(
'language' );
602 $language = $lang ===
null ?
'' : Html::hidden(
'language', $lang );
604 $form = Html::rawElement(
'form', [
'action' => wfScript(),
'name' =>
'searchform' ],
605 $title . $input . $submit . $checkLabel . $language
611 protected function getGroupName() {
612 return 'translation';