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 // For keys like Foo/, substr returns false instead of ''
62 $this->languageCode = (string)( substr( $this->key, $pos + 1 ) );
63 $this->key = substr( $this->key, 0, $pos );
64 }
65 }
66
67 return [ $this->key, $this->languageCode ];
68 }
69
71 public function getKey(): string {
72 $this->figureMessage();
73
74 return $this->key;
75 }
76
81 public function getCode(): string {
82 $this->figureMessage();
83
84 return $this->languageCode;
85 }
86
91 public function getEffectiveLanguage(): Language {
92 $code = $this->getCode();
93 $mwServices = MediaWikiServices::getInstance();
94 if ( !$mwServices->getLanguageNameUtils()->isKnownLanguageTag( $code ) ||
95 $this->isDoc()
96 ) {
97 return $mwServices->getContentLanguage();
98 }
99
100 return $mwServices->getLanguageFactory()->getLanguage( $code );
101 }
102
104 public function isDoc(): bool {
105 global $wgTranslateDocumentationLanguageCode;
106
107 return $this->getCode() === $wgTranslateDocumentationLanguageCode;
108 }
109
114 public function isPageTranslation(): bool {
115 return $this->title->inNamespace( NS_TRANSLATIONS );
116 }
117
125 public function getGroupIds() {
126 if ( $this->groupIds === null ) {
127 $this->groupIds = $this->messageIndex->getGroupIds( $this );
128 }
129
130 return $this->groupIds;
131 }
132
137 public function getGroup(): ?MessageGroup {
138 $ids = $this->getGroupIds();
139 if ( !isset( $ids[0] ) ) {
140 throw new BadMethodCallException( 'called before isValid' );
141 }
142 return MessageGroups::getGroup( $ids[0] );
143 }
144
146 public function isValid(): bool {
147 static $jobHasBeenScheduled = false;
148
149 if ( !$this->isMessageNamespace() ) {
150 return false;
151 }
152
153 $groups = $this->getGroupIds();
154 if ( !$groups ) {
155 return false;
156 }
157
158 // Do another check that the group actually exists
159 $group = $this->getGroup();
160 if ( !$group ) {
161 $logger = LoggerFactory::getInstance( LogNames::MAIN );
162 $logger->warning(
163 '[MessageHandle] MessageIndex is out of date. Page {pagename} refers to ' .
164 'unknown group {messagegroup}',
165 [
166 'pagename' => $this->getTitle()->getPrefixedText(),
167 'messagegroup' => $groups[0],
168 'exception' => new RuntimeException(),
169 'hasJobBeenScheduled' => $jobHasBeenScheduled
170 ]
171 );
172
173 if ( !$jobHasBeenScheduled ) {
174 // Schedule a job in the job queue (with deduplication)
175 $job = RebuildMessageIndexJob::newJob( __METHOD__ );
176 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $job );
177 $jobHasBeenScheduled = true;
178 }
179
180 return false;
181 }
182
183 return true;
184 }
185
187 public function getTitle(): Title {
188 return Title::newFromLinkTarget( $this->title );
189 }
190
192 public function getTitleForLanguage( string $languageCode ): Title {
193 return Title::makeTitle(
194 $this->title->getNamespace(),
195 $this->getKey() . "/$languageCode"
196 );
197 }
198
200 public function getTitleForBase(): Title {
201 return Title::makeTitle(
202 $this->title->getNamespace(),
203 $this->getKey()
204 );
205 }
206
212 public static function hasFuzzyString( string $text ): bool {
213 return str_contains( $text, TRANSLATE_FUZZY );
214 }
215
217 public static function makeFuzzyString( string $text ): string {
218 return self::hasFuzzyString( $text ) ? $text : TRANSLATE_FUZZY . $text;
219 }
220
222 public function isFuzzy(): bool {
223 $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
224
225 $res = $dbr->newSelectQueryBuilder()
226 ->select( 'rt_type' )
227 ->from( 'page' )
228 ->join( 'revtag', null, [
229 'page_id=rt_page',
230 'page_latest=rt_revision',
231 'rt_type' => RevTagStore::FUZZY_TAG,
232 ] )
233 ->where( [
234 'page_namespace' => $this->title->getNamespace(),
235 'page_title' => $this->title->getDBkey(),
236 ] )
237 ->caller( __METHOD__ )
238 ->fetchField();
239
240 return $res !== false;
241 }
242
248 public function getInternalKey(): string {
249 $mwServices = MediaWikiServices::getInstance();
250 $nsInfo = $mwServices->getNamespaceInfo();
251 $contentLanguage = $mwServices->getContentLanguage();
252
253 $key = $this->getKey();
254 $group = $this->getGroup();
255 $groupKeys = $group->getKeys();
256
257 if ( in_array( $key, $groupKeys, true ) ) {
258 return $key;
259 }
260
261 $namespace = $this->title->getNamespace();
262 if ( $nsInfo->isCapitalized( $namespace ) ) {
263 $lowercaseKey = $contentLanguage->lcfirst( $key );
264 if ( in_array( $lowercaseKey, $groupKeys, true ) ) {
265 return $lowercaseKey;
266 }
267 }
268
269 // Brute force all the keys to find the one. This one should always find a match
270 // if there is one.
271 foreach ( $groupKeys as $haystackKey ) {
272 $normalizedHaystackKey = Title::makeTitleSafe( $namespace, $haystackKey )->getDBkey();
273 if ( $normalizedHaystackKey === $key ) {
274 return $haystackKey;
275 }
276 }
277
278 return "BUG:$key";
279 }
280
282 public function needsFuzzy( string $text ): bool {
283 // Docs are exempt for checks
284 if ( $this->isDoc() ) {
285 return false;
286 }
287
288 // Check for explicit tag.
289 if ( self::hasFuzzyString( $text ) ) {
290 return true;
291 }
292
293 // Not all groups have validators
294 $group = $this->getGroup();
295 $validator = $group->getValidator();
296
297 // no validator set
298 if ( !$validator ) {
299 return false;
300 }
301
302 $code = $this->getCode();
303 $key = $this->getKey();
304 $en = $group->getMessage( $key, $group->getSourceLanguage() );
305 $message = new FatMessage( $key, $en );
306 // Take the contents from edit field as a translation.
307 $message->setTranslation( $text );
308 if ( $message->definition() === null ) {
309 // This should NOT happen, but add a check since it seems to be happening
310 // See: https://phabricator.wikimedia.org/T255669
311 LoggerFactory::getInstance( LogNames::MAIN )->warning(
312 'Message definition is empty! Title: {title}, group: {group}, key: {key}',
313 [
314 'title' => $this->getTitle()->getPrefixedText(),
315 'group' => $group->getId(),
316 'key' => $key
317 ]
318 );
319 return false;
320 }
321
322 $validationResult = $validator->quickValidate( $message, $code );
323 return $validationResult->hasIssues();
324 }
325}
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(), 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 { 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());}, '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);}]
@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:59
Interface for message groups.