Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslateUtils.php
Go to the documentation of this file.
1<?php
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Revision\RevisionRecord;
13use MediaWiki\Revision\SlotRecord;
14
27 public static function title( $message, $code, $ns = NS_MEDIAWIKI ) {
28 // Cache some amount of titles for speed.
29 static $cache = [];
30 $key = $ns . ':' . $message;
31
32 if ( !isset( $cache[$key] ) ) {
33 $cache[$key] = Title::capitalize( $message, $ns );
34 }
35
36 if ( $code ) {
37 return $cache[$key] . '/' . $code;
38 } else {
39 return $cache[$key];
40 }
41 }
42
49 public static function figureMessage( $text ) {
50 $pos = strrpos( $text, '/' );
51 $code = substr( $text, $pos + 1 );
52 $key = substr( $text, 0, $pos );
53
54 return [ $key, $code ];
55 }
56
64 public static function getMessageContent( $key, $language, $namespace = NS_MEDIAWIKI ) {
65 $title = self::title( $key, $language, $namespace );
66 $data = self::getContents( [ $title ], $namespace );
67
68 return $data[$title][0] ?? null;
69 }
70
79 public static function getContents( $titles, $namespace ) {
80 $dbr = wfGetDB( DB_REPLICA );
81 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
82 $titleContents = [];
83
84 $query = $revStore->getQueryInfo( [ 'page', 'user' ] );
85 $rows = $dbr->select(
86 $query['tables'],
87 $query['fields'],
88 [
89 'page_namespace' => $namespace,
90 'page_title' => $titles,
91 'page_latest=rev_id',
92 ],
93 __METHOD__,
94 [],
95 $query['joins']
96 );
97
98 $revisions = $revStore->newRevisionsFromBatch( $rows, [
99 'slots' => true,
100 'content' => true
101 ] )->getValue();
102
103 foreach ( $rows as $row ) {
105 $rev = $revisions[$row->rev_id];
106 if ( $rev ) {
108 $content = $rev->getContent( SlotRecord::MAIN );
109 if ( $content ) {
110 $titleContents[$row->page_title] = [
111 $content->getText(),
112 $row->rev_user_text
113 ];
114 }
115 }
116 }
117
118 $rows->free();
119
120 return $titleContents;
121 }
122
129 public static function getContentForTitle( Title $title, $addFuzzy = false ): ?string {
130 $store = MediaWikiServices::getInstance()->getRevisionStore();
131 $revision = $store->getRevisionByTitle( $title );
132
133 if ( $revision === null ) {
134 return null;
135 }
136
137 $content = $revision->getContent( SlotRecord::MAIN );
138 $wiki = ( $content instanceof TextContent ) ? $content->getText() : null;
139
140 // Either unexpected content type, or the revision content is hidden
141 if ( $wiki === null ) {
142 return null;
143 }
144
145 if ( $addFuzzy ) {
146 $handle = new MessageHandle( $title );
147 if ( $handle->isFuzzy() ) {
148 $wiki = TRANSLATE_FUZZY . str_replace( TRANSLATE_FUZZY, '', $wiki );
149 }
150 }
151
152 return $wiki;
153 }
154
164 public static function translationChanges(
165 $hours = 24, $bots = false, $ns = null, array $extraFields = []
166 ) {
167 global $wgTranslateMessageNamespaces;
168
169 $dbr = wfGetDB( DB_REPLICA );
170
171 $hours = (int)$hours;
172 $cutoff_unixtime = time() - ( $hours * 3600 );
173 $cutoff = $dbr->timestamp( $cutoff_unixtime );
174
175 $conds = [
176 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff ),
177 'rc_namespace' => $ns ?: $wgTranslateMessageNamespaces,
178 ];
179 if ( $bots ) {
180 $conds['rc_bot'] = 0;
181 }
182
183 $res = $dbr->select(
184 [ 'recentchanges', 'actor' ],
185 array_merge( [
186 'rc_namespace', 'rc_title', 'rc_timestamp',
187 'rc_user_text' => 'actor_name',
188 ], $extraFields ),
189 $conds,
190 __METHOD__,
191 [],
192 [ 'actor' => [ 'JOIN', 'actor_id=rc_actor' ] ]
193 );
194 $rows = iterator_to_array( $res );
195
196 // Calculate 'lang', then sort by it and rc_timestamp
197 foreach ( $rows as &$row ) {
198 $pos = strrpos( $row->rc_title, '/' );
199 $row->lang = $pos === false ? $row->rc_title : substr( $row->rc_title, $pos + 1 );
200 }
201 unset( $row );
202
203 usort( $rows, static function ( $a, $b ) {
204 $x = strcmp( $a->lang, $b->lang );
205 if ( !$x ) {
206 // descending order
207 $x = strcmp(
208 wfTimestamp( TS_MW, $b->rc_timestamp ),
209 wfTimestamp( TS_MW, $a->rc_timestamp )
210 );
211 }
212 return $x;
213 } );
214
215 return $rows;
216 }
217
218 /* Some other helpers for output */
219
226 public static function getLanguageName( $code, $language = 'en' ) {
227 $languages = self::getLanguageNames( $language );
228 return $languages[$code] ?? $code;
229 }
230
237 public static function languageSelector( $language, $selectedId ) {
238 $selector = self::getLanguageSelector( $language );
239 $selector->setDefault( $selectedId );
240 $selector->setAttribute( 'id', 'language' );
241 $selector->setAttribute( 'name', 'language' );
242
243 return $selector->getHTML();
244 }
245
252 public static function getLanguageSelector( $language, $labelOption = false ) {
253 $languages = self::getLanguageNames( $language );
254 ksort( $languages );
255
256 $selector = new XmlSelect();
257 if ( $labelOption !== false ) {
258 $selector->addOption( $labelOption, '-' );
259 }
260
261 foreach ( $languages as $code => $name ) {
262 $selector->addOption( "$code - $name", $code );
263 }
264
265 return $selector;
266 }
267
275 public static function getLanguageNames( $code ) {
276 $languageNames = MediaWikiServices::getInstance()->getLanguageNameUtils()->getLanguageNames( $code );
277
278 $deprecatedCodes = LanguageCode::getDeprecatedCodeMapping();
279 foreach ( array_keys( $deprecatedCodes ) as $deprecatedCode ) {
280 unset( $languageNames[ $deprecatedCode ] );
281 }
282
283 Hooks::run( 'TranslateSupportedLanguages', [ &$languageNames, $code ] );
284
285 return $languageNames;
286 }
287
294 public static function messageKeyToGroup( $namespace, $key ) {
295 $groups = self::messageKeyToGroups( $namespace, $key );
296
297 return count( $groups ) ? $groups[0] : null;
298 }
299
306 public static function messageKeyToGroups( $namespace, $key ) {
307 $mi = MessageIndex::singleton()->retrieve();
308 $normkey = self::normaliseKey( $namespace, $key );
309
310 if ( isset( $mi[$normkey] ) ) {
311 return (array)$mi[$normkey];
312 } else {
313 return [];
314 }
315 }
316
323 public static function normaliseKey( $namespace, $key ) {
324 $key = lcfirst( $key );
325
326 return strtr( "$namespace:$key", ' ', '_' );
327 }
328
336 public static function fieldset( $legend, $contents, array $attributes = [] ) {
337 return Xml::openElement( 'fieldset', $attributes ) .
338 Xml::tags( 'legend', null, $legend ) . $contents .
339 Xml::closeElement( 'fieldset' );
340 }
341
353 public static function convertWhiteSpaceToHTML( $message ) {
354 $msg = htmlspecialchars( $message );
355 $msg = preg_replace( '/^ /m', '&#160;', $msg );
356 $msg = preg_replace( '/ $/m', '&#160;', $msg );
357 $msg = preg_replace( '/ /', '&#160; ', $msg );
358 $msg = str_replace( "\n", '<br />', $msg );
359
360 return $msg;
361 }
362
368 public static function assetPath( $path ) {
369 global $wgExtensionAssetsPath;
370 return "$wgExtensionAssetsPath/Translate/$path";
371 }
372
379 public static function cacheFile( $filename ) {
380 global $wgTranslateCacheDirectory, $wgCacheDirectory;
381
382 if ( $wgTranslateCacheDirectory !== false ) {
383 $dir = $wgTranslateCacheDirectory;
384 } elseif ( $wgCacheDirectory !== false ) {
385 $dir = $wgCacheDirectory;
386 } else {
387 throw new MWException( "\$wgCacheDirectory must be configured" );
388 }
389
390 return "$dir/$filename";
391 }
392
398 public static function getPlaceholder() {
399 static $i = 0;
400
401 return "\x7fUNIQ" . dechex( mt_rand( 0, 0x7fffffff ) ) .
402 dechex( mt_rand( 0, 0x7fffffff ) ) . '-' . $i++;
403 }
404
412 public static function getIcon( MessageGroup $g, $size ) {
413 $icon = $g->getIcon();
414 if ( substr( $icon, 0, 7 ) !== 'wiki://' ) {
415 return null;
416 }
417
418 $formats = [];
419
420 $filename = substr( $icon, 7 );
421 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $filename );
422 if ( !$file ) {
423 wfWarn( "Unknown message group icon file $icon" );
424
425 return null;
426 }
427
428 if ( $file->isVectorized() ) {
429 $formats['vector'] = $file->getFullUrl();
430 }
431
432 $formats['raster'] = $file->createThumb( $size, $size );
433
434 return $formats;
435 }
436
443 public static function getSafeReadDB() {
444 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
445 $index = self::shouldReadFromPrimary() ? DB_PRIMARY : DB_REPLICA;
446
447 return $lb->getConnectionRef( $index );
448 }
449
455 public static function shouldReadFromPrimary() {
456 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
457 // Parsing APIs need POST for payloads but are read-only, so avoid spamming
458 // the primary then. No good way to check this at the moment...
459 if ( PageTranslationHooks::$renderingContext ) {
460 return false;
461 }
462
463 return PHP_SAPI === 'cli' ||
464 RequestContext::getMain()->getRequest()->wasPosted() ||
465 $lb->hasOrMadeRecentPrimaryChanges();
466 }
467
474 public static function getEditorUrl( MessageHandle $handle ) {
475 if ( !$handle->isValid() ) {
476 return $handle->getTitle()->getLocalURL( [ 'action' => 'edit' ] );
477 }
478
479 $title = MediaWikiServices::getInstance()
480 ->getSpecialPageFactory()->getPage( 'Translate' )->getPageTitle();
481 return $title->getFullURL( [
482 'showMessage' => $handle->getInternalKey(),
483 'group' => $handle->getGroup()->getId(),
484 'language' => $handle->getCode(),
485 ] );
486 }
487
493 public static function serialize( $value ) {
494 return serialize( $value );
495 }
496
503 public static function deserialize( $str, $opts = [ 'allowed_classes' => false ] ) {
504 return unserialize( $str, $opts );
505 }
506
511 public static function getVersion(): string {
512 // Avoid parsing JSON multiple time per request
513 static $version = null;
514 if ( $version === null ) {
515 $version = json_decode( file_get_contents( __DIR__ . '/extension.json' ) )->version;
516 }
517 return $version;
518 }
519
527 public static function allowsSubpages( Title $title ): bool {
528 $mwInstance = MediaWikiServices::getInstance();
529 $namespaceInfo = $mwInstance->getNamespaceInfo();
530 return $namespaceInfo->hasSubpages( $title->getNamespace() );
531 }
532
542 public static function isSupportedLanguageCode( string $code ): bool {
543 $all = self::getLanguageNames( null );
544 return isset( $all[ $code ] );
545 }
546
547 public static function getTextFromTextContent( ?Content $content ): string {
548 if ( !$content ) {
549 throw new UnexpectedValueException( 'Expected $content to be TextContent, got null instead.' );
550 }
551
552 if ( $content instanceof TextContent ) {
553 return $content->getText();
554 }
555
556 throw new UnexpectedValueException( 'Expected $content to be TextContent, but got ' . get_class( $content ) );
557 }
558
566 public static function getTranslations( MessageHandle $handle ): array {
567 $namespace = $handle->getTitle()->getNamespace();
568 $base = $handle->getKey();
569
570 $dbr = MediaWikiServices::getInstance()
571 ->getDBLoadBalancer()
572 ->getConnection( DB_REPLICA );
573
574 $titles = $dbr->newSelectQueryBuilder()
575 ->select( 'page_title' )
576 ->from( 'page' )
577 ->where( [
578 'page_namespace' => $namespace,
579 'page_title ' . $dbr->buildLike( "$base/", $dbr->anyString() ),
580 ] )
581 ->caller( __METHOD__ )
582 ->orderBy( 'page_title' )
583 ->fetchFieldValues();
584
585 if ( $titles === [] ) {
586 return [];
587 }
588
589 $pageInfo = self::getContents( $titles, $namespace );
590
591 return $pageInfo;
592 }
593}
Class for pointing to messages, like Title class is for titles.
getGroup()
Get the primary MessageGroup this message belongs to.
isValid()
Checks if the handle corresponds to a known message.
getTitle()
Get the original title.
getCode()
Returns the language code.
getInternalKey()
This returns the key that can be used for showMessage parameter for Special:Translate for regular mes...
getKey()
Returns the identified or guessed message key.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
static messageKeyToGroup( $namespace, $key)
Returns the primary group message belongs to.
static serialize( $value)
Serialize the given value.
static getSafeReadDB()
Get a DB handle suitable for read and read-for-write cases.
static getMessageContent( $key, $language, $namespace=NS_MEDIAWIKI)
Loads page content without side effects.
static shouldReadFromPrimary()
Check whether primary should be used for reads to avoid reading stale data.
static getLanguageNames( $code)
Get translated language names for the languages generally supported for translation in the current wi...
static getContents( $titles, $namespace)
Fetches contents for pagenames in given namespace without side effects.
static getPlaceholder()
Returns a random string that can be used as placeholder in strings.
static isSupportedLanguageCode(string $code)
Checks whether a language code is supported for translation at the wiki level.
static languageSelector( $language, $selectedId)
Returns a language selector.
static getLanguageName( $code, $language='en')
Returns a localised language name.
static translationChanges( $hours=24, $bots=false, $ns=null, array $extraFields=[])
Fetches recent changes for titles in given namespaces.
static cacheFile( $filename)
Gets the path for cache files.
static getContentForTitle(Title $title, $addFuzzy=false)
Returns the content for a given title and adds the fuzzy tag if requested.
static convertWhiteSpaceToHTML( $message)
Escapes the message, and does some mangling to whitespace, so that it is preserved when outputted as-...
static getLanguageSelector( $language, $labelOption=false)
Standard language selector in Translate extension.
static title( $message, $code, $ns=NS_MEDIAWIKI)
Does quick normalisation of message name so that in can be looked from the database.
static fieldset( $legend, $contents, array $attributes=[])
Constructs a fieldset with contents.
static getIcon(MessageGroup $g, $size)
Get URLs for icons if available.
static allowsSubpages(Title $title)
Checks if the namespace that the title belongs to allows subpages.
static getEditorUrl(MessageHandle $handle)
Get an URL that points to an editor for this message handle.
static messageKeyToGroups( $namespace, $key)
Returns the all the groups message belongs to.
static normaliseKey( $namespace, $key)
Converts page name and namespace to message index format.
static deserialize( $str, $opts=[ 'allowed_classes'=> false])
Deserialize the given string.
static getTranslations(MessageHandle $handle)
Returns all translations of a given message.
static assetPath( $path)
Construct the web address to given asset.
static figureMessage( $text)
Splits page name into message key and language code.
Interface for message groups.
getIcon()
Returns an icon for this message group if any.