Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageHandle.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageLoading;
5
6use BadMethodCallException;
11use MediaWiki\Language\Language;
12use MediaWiki\Linker\LinkTarget;
13use MediaWiki\Logger\LoggerFactory;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Title\Title;
16use MessageGroup;
17use RuntimeException;
18
27 private LinkTarget $title;
28 private ?string $key = null;
29 private ?string $languageCode = null;
31 private ?array $groupIds = null;
32 private MessageIndex $messageIndex;
33
34 public function __construct( LinkTarget $title ) {
35 $this->title = $title;
36 $this->messageIndex = Services::getInstance()->getMessageIndex();
37 }
38
40 public function isMessageNamespace(): bool {
41 global $wgTranslateMessageNamespaces;
42 $namespace = $this->title->getNamespace();
43
44 return in_array( $namespace, $wgTranslateMessageNamespaces );
45 }
46
51 public function figureMessage(): array {
52 if ( $this->key === null ) {
53 // Check if this is a valid message first
54 $this->key = $this->title->getDBkey();
55 $known = $this->messageIndex->getGroupIds( $this ) !== [];
56
57 $pos = strrpos( $this->key, '/' );
58 if ( $known || $pos === false ) {
59 $this->languageCode = '';
60 } else {
61 $this->languageCode = substr( $this->key, $pos + 1 );
62 $this->key = substr( $this->key, 0, $pos );
63 }
64 }
65
66 return [ $this->key, $this->languageCode ];
67 }
68
70 public function getKey(): string {
71 $this->figureMessage();
72
73 return $this->key;
74 }
75
80 public function getCode(): string {
81 $this->figureMessage();
82
83 return $this->languageCode;
84 }
85
90 public function getEffectiveLanguage(): Language {
91 $code = $this->getCode();
92 $mwServices = MediaWikiServices::getInstance();
93 if ( !$mwServices->getLanguageNameUtils()->isKnownLanguageTag( $code ) ||
94 $this->isDoc()
95 ) {
96 return $mwServices->getContentLanguage();
97 }
98
99 return $mwServices->getLanguageFactory()->getLanguage( $code );
100 }
101
103 public function isDoc(): bool {
104 global $wgTranslateDocumentationLanguageCode;
105
106 return $this->getCode() === $wgTranslateDocumentationLanguageCode;
107 }
108
113 public function isPageTranslation(): bool {
114 return $this->title->inNamespace( NS_TRANSLATIONS );
115 }
116
124 public function getGroupIds() {
125 if ( $this->groupIds === null ) {
126 $this->groupIds = $this->messageIndex->getGroupIds( $this );
127 }
128
129 return $this->groupIds;
130 }
131
136 public function getGroup(): ?MessageGroup {
137 $ids = $this->getGroupIds();
138 if ( !isset( $ids[0] ) ) {
139 throw new BadMethodCallException( 'called before isValid' );
140 }
141 return MessageGroups::getGroup( $ids[0] );
142 }
143
145 public function isValid(): bool {
146 static $jobHasBeenScheduled = false;
147
148 if ( !$this->isMessageNamespace() ) {
149 return false;
150 }
151
152 $groups = $this->getGroupIds();
153 if ( !$groups ) {
154 return false;
155 }
156
157 // Do another check that the group actually exists
158 $group = $this->getGroup();
159 if ( !$group ) {
160 $logger = LoggerFactory::getInstance( LogNames::MAIN );
161 $logger->warning(
162 '[MessageHandle] MessageIndex is out of date. Page {pagename} refers to ' .
163 'unknown group {messagegroup}',
164 [
165 'pagename' => $this->getTitle()->getPrefixedText(),
166 'messagegroup' => $groups[0],
167 'exception' => new RuntimeException(),
168 'hasJobBeenScheduled' => $jobHasBeenScheduled
169 ]
170 );
171
172 if ( !$jobHasBeenScheduled ) {
173 // Schedule a job in the job queue (with deduplication)
174 $job = RebuildMessageIndexJob::newJob( __METHOD__ );
175 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $job );
176 $jobHasBeenScheduled = true;
177 }
178
179 return false;
180 }
181
182 return true;
183 }
184
186 public function getTitle(): Title {
187 return Title::newFromLinkTarget( $this->title );
188 }
189
191 public function getTitleForLanguage( string $languageCode ): Title {
192 return Title::makeTitle(
193 $this->title->getNamespace(),
194 $this->getKey() . "/$languageCode"
195 );
196 }
197
199 public function getTitleForBase(): Title {
200 return Title::makeTitle(
201 $this->title->getNamespace(),
202 $this->getKey()
203 );
204 }
205
211 public static function hasFuzzyString( string $text ): bool {
212 return str_contains( $text, TRANSLATE_FUZZY );
213 }
214
216 public static function makeFuzzyString( string $text ): string {
217 return self::hasFuzzyString( $text ) ? $text : TRANSLATE_FUZZY . $text;
218 }
219
221 public function isFuzzy(): bool {
222 $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
223
224 $res = $dbr->newSelectQueryBuilder()
225 ->select( 'rt_type' )
226 ->from( 'page' )
227 ->join( 'revtag', null, [
228 'page_id=rt_page',
229 'page_latest=rt_revision',
230 'rt_type' => RevTagStore::FUZZY_TAG,
231 ] )
232 ->where( [
233 'page_namespace' => $this->title->getNamespace(),
234 'page_title' => $this->title->getDBkey(),
235 ] )
236 ->caller( __METHOD__ )
237 ->fetchField();
238
239 return $res !== false;
240 }
241
247 public function getInternalKey(): string {
248 $mwServices = MediaWikiServices::getInstance();
249 $nsInfo = $mwServices->getNamespaceInfo();
250 $contentLanguage = $mwServices->getContentLanguage();
251
252 $key = $this->getKey();
253 $group = $this->getGroup();
254 $groupKeys = $group->getKeys();
255
256 if ( in_array( $key, $groupKeys, true ) ) {
257 return $key;
258 }
259
260 $namespace = $this->title->getNamespace();
261 if ( $nsInfo->isCapitalized( $namespace ) ) {
262 $lowercaseKey = $contentLanguage->lcfirst( $key );
263 if ( in_array( $lowercaseKey, $groupKeys, true ) ) {
264 return $lowercaseKey;
265 }
266 }
267
268 // Brute force all the keys to find the one. This one should always find a match
269 // if there is one.
270 foreach ( $groupKeys as $haystackKey ) {
271 $normalizedHaystackKey = Title::makeTitleSafe( $namespace, $haystackKey )->getDBkey();
272 if ( $normalizedHaystackKey === $key ) {
273 return $haystackKey;
274 }
275 }
276
277 return "BUG:$key";
278 }
279
281 public function needsFuzzy( string $text ): bool {
282 // Docs are exempt for checks
283 if ( $this->isDoc() ) {
284 return false;
285 }
286
287 // Check for explicit tag.
288 if ( self::hasFuzzyString( $text ) ) {
289 return true;
290 }
291
292 // Not all groups have validators
293 $group = $this->getGroup();
294 $validator = $group->getValidator();
295
296 // no validator set
297 if ( !$validator ) {
298 return false;
299 }
300
301 $code = $this->getCode();
302 $key = $this->getKey();
303 $en = $group->getMessage( $key, $group->getSourceLanguage() );
304 $message = new FatMessage( $key, $en );
305 // Take the contents from edit field as a translation.
306 $message->setTranslation( $text );
307 if ( $message->rawDefinition() === null ) {
308 // This should NOT happen, but add a check since it seems to be happening
309 // See: https://phabricator.wikimedia.org/T255669
310 LoggerFactory::getInstance( LogNames::MAIN )->warning(
311 'Message definition is empty! Title: {title}, group: {group}, key: {key}',
312 [
313 'title' => $this->getTitle()->getPrefixedText(),
314 'group' => $group->getId(),
315 'key' => $key
316 ]
317 );
318 return false;
319 }
320
321 $validationResult = $validator->quickValidate( $message, $code );
322 return $validationResult->hasIssues();
323 }
324}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), $services->getContentLanguageCode() ->toString(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, '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->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory(), $services->get( 'Translate:HookRunner'),);}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, '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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array
Constants for log channel names used in this extension.
Definition LogNames.php:13
Factory class for accessing message groups individually by id or all of them as a list.
Class to manage revision tags for translatable bundles.
Class for pointing to messages, like Title class is for titles.
static makeFuzzyString(string $text)
Check if a string has fuzzy string and if not, add it.
getTitleForLanguage(string $languageCode)
Get the original title with the passed language code.
getKey()
Returns the identified or guessed message key.
getEffectiveLanguage()
Return the Language object for the assumed language of the content, which might be different from the...
needsFuzzy(string $text)
Returns true if message is fuzzy, OR fails checks OR fails validations (error OR warning).
isPageTranslation()
Determine whether the current handle is for page translation feature.
getGroup()
Get the primary MessageGroup this message belongs to.
getInternalKey()
This returns the key that can be used for showMessage parameter for Special:Translate for regular mes...
isMessageNamespace()
Check if this handle is in a message namespace.
static hasFuzzyString(string $text)
Check if a string contains the fuzzy string.
getGroupIds()
Returns all message group ids this message belongs to.
figureMessage()
Recommended to use getCode and getKey instead.
isValid()
Checks if the handle corresponds to a known message.
isDoc()
Determine whether the current handle is for message documentation.
Creates a database of keys in all groups, so that namespace and key can be used to get the groups the...
Minimal service container.
Definition Services.php:60
Interface for message groups.