Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
Utilities.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Utilities;
5
6use Content;
7use LanguageCode;
10use MediaWiki\Languages\LanguageNameUtils;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Revision\RevisionRecord;
13use MediaWiki\Revision\SlotRecord;
14use MessageGroup;
16use MWException;
17use RequestContext;
18use TextContent;
19use Title;
20use UnexpectedValueException;
21use Wikimedia\Rdbms\IDatabase;
22use Xml;
23use XmlSelect;
24
30class Utilities {
39 public static function title( string $message, string $code, int $ns = NS_MEDIAWIKI ): string {
40 // Cache some amount of titles for speed.
41 static $cache = [];
42 $key = $ns . ':' . $message;
43
44 if ( !isset( $cache[$key] ) ) {
45 $cache[$key] = Title::capitalize( $message, $ns );
46 }
47
48 if ( $code ) {
49 return $cache[$key] . '/' . $code;
50 } else {
51 return $cache[$key];
52 }
53 }
54
61 public static function figureMessage( string $text ): array {
62 $pos = strrpos( $text, '/' );
63 $code = substr( $text, $pos + 1 );
64 $key = substr( $text, 0, $pos );
65
66 return [ $key, $code ];
67 }
68
76 public static function getMessageContent( string $key, string $language, int $namespace = NS_MEDIAWIKI ): ?string {
77 $title = self::title( $key, $language, $namespace );
78 $data = self::getContents( [ $title ], $namespace );
79
80 return $data[$title][0] ?? null;
81 }
82
91 public static function getContents( $titles, int $namespace ): array {
92 $mwServices = MediaWikiServices::getInstance();
93 $dbr = $mwServices->getDBLoadBalancer()->getConnection( DB_REPLICA );
94 $revStore = $mwServices->getRevisionStore();
95 $titleContents = [];
96
97 $query = $revStore->getQueryInfo( [ 'page', 'user' ] );
98 $rows = $dbr->select(
99 $query['tables'],
100 $query['fields'],
101 [
102 'page_namespace' => $namespace,
103 'page_title' => $titles,
104 'page_latest=rev_id',
105 ],
106 __METHOD__,
107 [],
108 $query['joins']
109 );
110
111 $revisions = $revStore->newRevisionsFromBatch( $rows, [
112 'slots' => true,
113 'content' => true
114 ] )->getValue();
115
116 foreach ( $rows as $row ) {
118 $rev = $revisions[$row->rev_id];
119 if ( $rev ) {
121 $content = $rev->getContent( SlotRecord::MAIN );
122 if ( $content ) {
123 $titleContents[$row->page_title] = [
124 $content->getText(),
125 $row->rev_user_text
126 ];
127 }
128 }
129 }
130
131 $rows->free();
132
133 return $titleContents;
134 }
135
142 public static function getContentForTitle( Title $title, bool $addFuzzy = false ): ?string {
143 $store = MediaWikiServices::getInstance()->getRevisionStore();
144 $revision = $store->getRevisionByTitle( $title );
145
146 if ( $revision === null ) {
147 return null;
148 }
149
150 $content = $revision->getContent( SlotRecord::MAIN );
151 $wiki = ( $content instanceof TextContent ) ? $content->getText() : null;
152
153 // Either unexpected content type, or the revision content is hidden
154 if ( $wiki === null ) {
155 return null;
156 }
157
158 if ( $addFuzzy ) {
159 $handle = new MessageHandle( $title );
160 if ( $handle->isFuzzy() ) {
161 $wiki = TRANSLATE_FUZZY . str_replace( TRANSLATE_FUZZY, '', $wiki );
162 }
163 }
164
165 return $wiki;
166 }
167
168 /* Some other helpers for output */
169
176 public static function getLanguageName( string $code, ?string $language = 'en' ): string {
177 $languages = self::getLanguageNames( $language );
178 return $languages[$code] ?? $code;
179 }
180
181 // TODO remove languageSelector() after Sunsetting of TranslateSvg extension
182
189 public static function languageSelector( $language, $selectedId ) {
190 $selector = self::getLanguageSelector( $language );
191 $selector->setDefault( $selectedId );
192 $selector->setAttribute( 'id', 'language' );
193 $selector->setAttribute( 'name', 'language' );
194
195 return $selector->getHTML();
196 }
197
204 public static function getLanguageSelector( $language, ?string $labelOption = null ) {
205 $languages = self::getLanguageNames( $language );
206 ksort( $languages );
207
208 $selector = new XmlSelect();
209 if ( $labelOption !== null ) {
210 $selector->addOption( $labelOption, '-' );
211 }
212
213 foreach ( $languages as $code => $name ) {
214 $selector->addOption( "$code - $name", $code );
215 }
216
217 return $selector;
218 }
219
227 public static function getLanguageNames( ?string $code ): array {
228 $mwServices = MediaWikiServices::getInstance();
229 $languageNames = $mwServices->getLanguageNameUtils()->getLanguageNames( $code );
230
231 $deprecatedCodes = LanguageCode::getDeprecatedCodeMapping();
232 foreach ( array_keys( $deprecatedCodes ) as $deprecatedCode ) {
233 unset( $languageNames[ $deprecatedCode ] );
234 }
235 $mwServices->getHookContainer()->run( 'TranslateSupportedLanguages', [ &$languageNames, $code ] );
236
237 return $languageNames;
238 }
239
246 public static function messageKeyToGroup( int $namespace, string $key ): ?string {
247 $groups = self::messageKeyToGroups( $namespace, $key );
248
249 return count( $groups ) ? $groups[0] : null;
250 }
251
256 public static function messageKeyToGroups( int $namespace, string $key ): array {
257 $mi = Services::getInstance()->getMessageIndex()->retrieve();
258 $normkey = self::normaliseKey( $namespace, $key );
259
260 if ( isset( $mi[$normkey] ) ) {
261 return (array)$mi[$normkey];
262 } else {
263 return [];
264 }
265 }
266
268 public static function normaliseKey( int $namespace, string $key ): string {
269 $key = lcfirst( $key );
270
271 return strtr( "$namespace:$key", ' ', '_' );
272 }
273
281 public static function fieldset( string $legend, string $contents, array $attributes = [] ): string {
282 return Xml::openElement( 'fieldset', $attributes ) .
283 Xml::tags( 'legend', null, $legend ) . $contents .
284 Xml::closeElement( 'fieldset' );
285 }
286
298 public static function convertWhiteSpaceToHTML( string $message ): string {
299 $msg = htmlspecialchars( $message );
300 $msg = preg_replace( '/^ /m', '&#160;', $msg );
301 $msg = preg_replace( '/ $/m', '&#160;', $msg );
302 $msg = preg_replace( '/ /', '&#160; ', $msg );
303 $msg = str_replace( "\n", '<br />', $msg );
304
305 return $msg;
306 }
307
314 public static function cacheFile( string $filename ): string {
315 global $wgTranslateCacheDirectory, $wgCacheDirectory;
316
317 if ( $wgTranslateCacheDirectory !== false ) {
318 $dir = $wgTranslateCacheDirectory;
319 } elseif ( $wgCacheDirectory !== false ) {
320 $dir = $wgCacheDirectory;
321 } else {
322 throw new MWException( "\$wgCacheDirectory must be configured" );
323 }
324
325 return "$dir/$filename";
326 }
327
329 public static function getPlaceholder(): string {
330 static $i = 0;
331
332 return "\x7fUNIQ" . dechex( mt_rand( 0, 0x7fffffff ) ) .
333 dechex( mt_rand( 0, 0x7fffffff ) ) . '-' . $i++;
334 }
335
342 public static function getIcon( MessageGroup $g, int $size ): ?array {
343 $icon = $g->getIcon();
344 if ( !$icon || substr( $icon, 0, 7 ) !== 'wiki://' ) {
345 return null;
346 }
347
348 $formats = [];
349
350 $filename = substr( $icon, 7 );
351 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $filename );
352 if ( !$file ) {
353 wfWarn( "Unknown message group icon file $icon" );
354
355 return null;
356 }
357
358 if ( $file->isVectorized() ) {
359 $formats['vector'] = $file->getFullUrl();
360 }
361
362 $formats['raster'] = $file->createThumb( $size, $size );
363
364 return $formats;
365 }
366
373 public static function getSafeReadDB(): IDatabase {
374 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
375 $index = self::shouldReadFromPrimary() ? DB_PRIMARY : DB_REPLICA;
376
377 return $lb->getConnection( $index );
378 }
379
381 public static function shouldReadFromPrimary(): bool {
382 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
383 // Parsing APIs need POST for payloads but are read-only, so avoid spamming
384 // the primary then. No good way to check this at the moment...
385 if ( PageTranslationHooks::$renderingContext ) {
386 return false;
387 }
388
389 return PHP_SAPI === 'cli' ||
390 RequestContext::getMain()->getRequest()->wasPosted() ||
391 $lb->hasOrMadeRecentPrimaryChanges();
392 }
393
399 public static function getEditorUrl( MessageHandle $handle ): string {
400 if ( !$handle->isValid() ) {
401 return $handle->getTitle()->getLocalURL( [ 'action' => 'edit' ] );
402 }
403
404 $title = MediaWikiServices::getInstance()
405 ->getSpecialPageFactory()->getPage( 'Translate' )->getPageTitle();
406 return $title->getFullURL( [
407 'showMessage' => $handle->getInternalKey(),
408 'group' => $handle->getGroup()->getId(),
409 'language' => $handle->getCode(),
410 ] );
411 }
412
417 public static function serialize( $value ): string {
418 return serialize( $value );
419 }
420
425 public static function deserialize( string $str, ?array $opts = [ 'allowed_classes' => false ] ) {
426 return unserialize( $str, $opts );
427 }
428
429 public static function getVersion(): string {
430 // Avoid parsing JSON multiple time per request
431 static $version = null;
432 if ( $version === null ) {
433 $version = json_decode( file_get_contents( __DIR__ . '../../../extension.json' ) )->version;
434 }
435 return $version;
436 }
437
445 public static function allowsSubpages( Title $title ): bool {
446 $mwInstance = MediaWikiServices::getInstance();
447 $namespaceInfo = $mwInstance->getNamespaceInfo();
448 return $namespaceInfo->hasSubpages( $title->getNamespace() );
449 }
450
457 public static function isSupportedLanguageCode( string $code ): bool {
458 $all = self::getLanguageNames( LanguageNameUtils::AUTONYMS );
459 return isset( $all[ $code ] );
460 }
461
462 public static function getTextFromTextContent( ?Content $content ): string {
463 if ( !$content ) {
464 throw new UnexpectedValueException( 'Expected $content to be TextContent, got null instead.' );
465 }
466
467 if ( $content instanceof TextContent ) {
468 return $content->getText();
469 }
470
471 throw new UnexpectedValueException( 'Expected $content to be TextContent, but got ' . get_class( $content ) );
472 }
473
480 public static function getTranslations( MessageHandle $handle ): array {
481 $namespace = $handle->getTitle()->getNamespace();
482 $base = $handle->getKey();
483
484 $dbr = MediaWikiServices::getInstance()
485 ->getDBLoadBalancer()
486 ->getConnection( DB_REPLICA );
487
488 $titles = $dbr->newSelectQueryBuilder()
489 ->select( 'page_title' )
490 ->from( 'page' )
491 ->where( [
492 'page_namespace' => $namespace,
493 'page_title ' . $dbr->buildLike( "$base/", $dbr->anyString() ),
494 ] )
495 ->caller( __METHOD__ )
496 ->orderBy( 'page_title' )
497 ->fetchFieldValues();
498
499 if ( $titles === [] ) {
500 return [];
501 }
502
503 return self::getContents( $titles, $namespace );
504 }
505}
506
507class_alias( Utilities::class, 'TranslateUtils' );
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Minimal service container.
Definition Services.php:40
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:30
static getPlaceholder()
Returns a random string that can be used as placeholder in strings.
static messageKeyToGroup(int $namespace, string $key)
Returns the primary group message belongs to.
static allowsSubpages(Title $title)
Checks if the namespace that the title belongs to allows subpages.
static getIcon(MessageGroup $g, int $size)
Get URLs for icons if available.
static getMessageContent(string $key, string $language, int $namespace=NS_MEDIAWIKI)
Loads page content without side effects.
Definition Utilities.php:76
static getLanguageName(string $code, ?string $language='en')
Returns a localised language name.
static getEditorUrl(MessageHandle $handle)
Get a URL that points to an editor for this message handle.
static fieldset(string $legend, string $contents, array $attributes=[])
Constructs a fieldset with contents.
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 deserialize(string $str, ?array $opts=[ 'allowed_classes'=> false])
Deserialize the given string.
static getTranslations(MessageHandle $handle)
Returns all translations of a given message.
static getContentForTitle(Title $title, bool $addFuzzy=false)
Returns the content for a given title and adds the fuzzy tag if requested.
static getSafeReadDB()
Get a DB handle suitable for read and read-for-write cases.
static getContents( $titles, int $namespace)
Fetches contents for pagenames in given namespace without side effects.
Definition Utilities.php:91
static messageKeyToGroups(int $namespace, string $key)
Returns the all the groups message belongs to.
static shouldReadFromPrimary()
Check whether primary should be used for reads to avoid reading stale data.
static serialize( $value)
Serialize the given value.
static cacheFile(string $filename)
Gets the path for cache files.
static convertWhiteSpaceToHTML(string $message)
Escapes the message, and does some mangling to whitespace, so that it is preserved when outputted as-...
static normaliseKey(int $namespace, string $key)
Converts page name and namespace to message index format.
static title(string $message, string $code, int $ns=NS_MEDIAWIKI)
Does quick normalisation of message name so that in can be looked from the database.
Definition Utilities.php:39
static getLanguageSelector( $language, ?string $labelOption=null)
Standard language selector in Translate extension.
static figureMessage(string $text)
Splits page name into message key and language code.
Definition Utilities.php:61
static getLanguageNames(?string $code)
Get translated language names for the languages generally supported for translation in the current wi...
Class for pointing to messages, like Title class is for titles.
getTitle()
Get the original title.
getKey()
Returns the identified or guessed message key.
Interface for message groups.