Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
EntitySearch.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorInterface;
5
7use Collation;
8use MalformedTitleException;
12use NamespaceInfo;
13use SplMinHeap;
14use TitleFormatter;
15use TitleParser;
16use WANObjectCache;
17use Wikimedia\LightweightObjectStore\ExpirationAwareness;
19
27 private const FIELD_DELIMITER = "\x7F";
28 private const ROW_DELIMITER = "\n";
29
30 private WANObjectCache $cache;
31 private Collation $collation;
32 private MessageGroups $messageGroupFactory;
33 private NamespaceInfo $namespaceInfo;
34 private MessageIndex $messageIndex;
35 private TitleParser $titleParser;
36 private TitleFormatter $titleFormatter;
37 private const TYPE_AGGREGATE = AggregateMessageGroup::class;
38 private const TYPE_MESSAGE_BUNDLE = MessageBundleMessageGroup::class;
39 private const TYPE_WIKIPAGE = WikiPageMessageGroup::class;
40 private const TYPE_OTHERS = 'o';
41 private const TYPE_MAPPING = [
42 self::TYPE_AGGREGATE => [ 'aggregate-groups', 'a' ],
43 self::TYPE_MESSAGE_BUNDLE => [ 'message-bundles', 'm' ],
44 self::TYPE_WIKIPAGE => [ 'translatable-pages', 'w' ]
45 ];
47 private array $mappedTypes;
48
49 public function __construct(
50 WANObjectCache $cache,
51 Collation $collation,
52 MessageGroups $messageGroupFactory,
53 NamespaceInfo $namespaceInfo,
54 MessageIndex $messageIndex,
55 TitleParser $titleParser,
56 TitleFormatter $titleFormatter
57 ) {
58 $this->cache = $cache;
59 $this->collation = $collation;
60 $this->messageGroupFactory = $messageGroupFactory;
61 $this->namespaceInfo = $namespaceInfo;
62 $this->messageIndex = $messageIndex;
63 $this->titleParser = $titleParser;
64 $this->titleFormatter = $titleFormatter;
65 $this->mappedTypes = $this->getGroupTypes();
66 }
67
68 public function searchStaticMessageGroups( string $query, int $maxResults, array $types = [] ): array {
69 $cache = $this->cache;
70 // None of the static groups currently use language-dependent labels. This
71 // may need revisiting later and splitting the cache by language.
72 $key = $cache->makeKey( 'Translate', 'EntitySearch', 'static-groups', '-v2' );
73 $haystack = $cache->getWithSetCallback(
74 $key,
75 ExpirationAwareness::TTL_WEEK,
76 function (): string {
77 return $this->getStaticMessageGroupsHaystack();
78 },
79 [
80 // Calling touchCheckKey() on this key purges the cache
81 'checkKeys' => [ $this->messageGroupFactory->getCacheKey() ],
82 // Avoid querying cache servers multiple times in a web request
83 'pcTTL' => ExpirationAwareness::TTL_PROC_LONG
84 ]
85 );
86
87 // Algorithm: Construct one big string with one entity per line. Then run
88 // preg_match_all twice over it, first to collect prefix match (to show them
89 // first), then to match words if more results are needed.
90 $results = [];
91
92 $delimiter = self::FIELD_DELIMITER;
93 $anything = "[^$delimiter\n]";
94 $query = preg_quote( $query, '/' );
95
96 $groupTypeFilter = [];
97 foreach ( $types as $filter ) {
98 $groupTypeFilter[] = $this->mappedTypes[$filter] ?? '';
99 }
100 $groupTypeFilter = count( $groupTypeFilter )
101 ? ( '[' . implode( $groupTypeFilter ) . ']' )
102 : $anything;
103
104 // Prefix match
105 $pattern = "/^$groupTypeFilter$delimiter($query$anything*)$delimiter($anything+)$/miu";
106 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
107 foreach ( $matches as [ , $label, $groupId ] ) {
108 // Index by $groupId to avoid duplicates from the prefix match and the word match
109 $results[$groupId] = [
110 'label' => $label,
111 'group' => $groupId,
112 ];
113
114 if ( count( $results ) >= $maxResults ) {
115 return array_values( $results );
116 }
117 }
118
119 // Word match
120 $pattern = "/^$groupTypeFilter$delimiter($anything*\b$query$anything*)$delimiter($anything+)$/miu";
121 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
122 foreach ( $matches as [ , $label, $groupId ] ) {
123 $results[$groupId] = [
124 'label' => $label,
125 'group' => $groupId,
126 ];
127
128 if ( count( $results ) >= $maxResults ) {
129 return array_values( $results );
130 }
131 }
132
133 return array_values( $results );
134 }
135
137 public function searchMessages( string $query, int $maxResults ): array {
138 // Optimized based on requirements:
139 // * "Natural" sorting of results
140 // * No need to show which message group things belong to
141 // * Match at any point in the message
142 // * Return full keys of prefixes that match multiple messages
143
144 // Algorithm: Construct one big string (haystack) with one entity per line.
145 // Then run preg_match_all over it. Because we will have many more matches
146 // than search results, this may be more efficient than calling preg_match
147 // repeatedly in a loop. On the other hand, it can use a lot of memory to
148 // construct the array for all the matches.
149 $haystack = $this->getMessagesHaystack();
150 $rowDelimiter = self::ROW_DELIMITER;
151 $anything = "[^$rowDelimiter]";
152 $query = preg_quote( $query, '/' );
153
154 // Word match
155 $pattern = "/^($anything*\b$query)$anything*$/miu";
156 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
157
158 $results = [];
159 $previousPrefixMatch = null;
160 foreach ( $matches as [ $full, $prefixMatch ] ) {
161 // This is a bit tricky. If we are at the maximum results, continue processing
162 // until the prefix changes, to get an accurate count
163 if ( count( $results ) >= $maxResults && $previousPrefixMatch !== $prefixMatch ) {
164 break;
165 }
166
167 if ( $full === $prefixMatch ) {
168 $results[$full] = [ $full, 1, true, $full ];
169 } else {
170 if ( !isset( $results["$prefixMatch*"] ) ) {
171 $results["$prefixMatch*"] = [ "$prefixMatch*", 0, false, $full ];
172 }
173 $results["$prefixMatch*"][1]++;
174 }
175 $previousPrefixMatch = $prefixMatch;
176 }
177
178 // Convert partial matches with single results to full match
179 foreach ( $results as $index => [ $label, $count, $isExact, $full ] ) {
180 if ( $count === 1 && !$isExact ) {
181 $results[$index][0] = $full;
182 }
183 }
184
185 // Drop unnecessary fields, pretty format title
186 foreach ( $results as &$value ) {
187 try {
188 $title = $this->titleParser->parseTitle( $value[0] );
189 $label = $this->titleFormatter->getPrefixedText( $title );
190 } catch ( MalformedTitleException $e ) {
191 $label = $value[0];
192 }
193 $value = [
194 'pattern' => $label,
195 'count' => $value[1]
196 ];
197 }
198
199 return array_values( $results );
200 }
201
203 public function matchMessages( string $query ): array {
204 $haystack = $this->getMessagesHaystack();
205 $rowDelimiter = self::ROW_DELIMITER;
206 $anything = "[^$rowDelimiter]*";
207
208 // Need something that's not affected by preg_quote and cannot be guessed
209 $placeholder = bin2hex( random_bytes( 16 ) );
210 $query = str_replace( '*', $placeholder, $query );
211 $query = preg_quote( $query, '/' );
212 $query = str_replace( $placeholder, $anything, $query );
213
214 $pattern = "/^$query/miu";
215 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
216 return array_column( $matches, 0 );
217 }
218
219 private function getStaticMessageGroupsHaystack(): string {
220 $groups = $this->messageGroupFactory->getGroups();
221 $data = new SplMinHeap();
222 foreach ( $groups as $group ) {
223 $label = $group->getLabel();
224 // Ensure there are no special chars that will break matching
225 $label = strtr( $label, [ self::FIELD_DELIMITER => '', self::ROW_DELIMITER => '' ] );
226 $sortKey = $this->collation->getSortKey( $label );
227 // It is unlikely that different groups have the same label (or sort key),
228 // but it's possible, so cannot use a hashmap here.
229 $groupType = get_class( $group );
230 $type = self::TYPE_MAPPING[$groupType][1] ?? self::TYPE_OTHERS;
231 $data->insert( [ $sortKey, $label, $group->getId(), $type ] );
232 }
233
234 $haystack = '';
235 foreach ( $data as [ , $label, $groupId, $type ] ) {
236 $haystack
237 .= $type
238 . self::FIELD_DELIMITER
239 . $label
240 . self::FIELD_DELIMITER
241 . $groupId
242 . self::ROW_DELIMITER;
243 }
244 return $haystack;
245 }
246
247 private function getMessageHaystackUncached(): string {
248 $namespaceMap = [];
249 $data = new SplMinHeap();
250 $keys = $this->messageIndex->getKeys();
251 foreach ( $keys as $key ) {
252 // Normalize "_" to " " so that \b in regexp matches words separated by underscores
253 $key = strtr( $key, [ '_' => ' ' ] );
254
255 [ $namespaceId, $label ] = explode( ':', $key, 2 );
256 if ( !isset( $namespaceMap[$namespaceId] ) ) {
257 $namespaceMap[$namespaceId] = $this->namespaceInfo->getCanonicalName( (int)$namespaceId );
258 }
259 $label = $namespaceMap[$namespaceId] . ":$label";
260
261 // Ensure there are no special chars that will break matching
262 $label = strtr( $label, [ self::ROW_DELIMITER => '' ] );
263 $sortKey = $this->collation->getSortKey( $label );
264 $data->insert( [ $sortKey, $label ] );
265 }
266
267 $haystack = '';
268 foreach ( $data as [ , $label ] ) {
269 $haystack .= $label . self::ROW_DELIMITER;
270 }
271
272 return $haystack;
273 }
274
275 private function getMessagesHaystack(): string {
276 $cache = $this->cache;
277 $key = $cache->makeKey( 'Translate', 'EntitySearch', 'messages' );
278 return $cache->getWithSetCallback(
279 $key,
280 ExpirationAwareness::TTL_WEEK,
281 function (): string {
282 // This can get rather large. On translatewiki.net it is multiple megabytes
283 // uncompressed. With compression (assumed to happen implicitly in the
284 // caching layer) it's under a megabyte.
285 return $this->getMessageHaystackUncached();
286 },
287 [
288 // Calling touchCheckKey() on this key purges the cache
289 'checkKeys' => [ $this->messageIndex->getStatusCacheKey() ],
290 // Avoid querying cache servers multiple times in a web request
291 'pcTTL' => ExpirationAwareness::TTL_PROC_LONG
292 ]
293 );
294 }
295
297 public function getGroupTypes(): array {
298 return array_column( self::TYPE_MAPPING, 1, 0 );
299 }
300}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager( $services->getTitleFactory());}, '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( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), 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: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->getDBLoadBalancer());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $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( 'Translate.MessageGroupSubscription'), 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->getDBLoadBalancerFactory());}, '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( 'Translate'), $services->getMainObjectStash(), $services->getDBLoadBalancerFactory(), $services->get( 'Translate:MessageGroupSubscription'), 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->getDBLoadBalancer(), $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->getDBLoadBalancer());}, '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->getDBLoadBalancer());}, '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());}, '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->getDBLoadBalancerFactory(), $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:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getDBLoadBalancer(), $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'));}, '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->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getDBLoadBalancerFactory(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getDBLoadBalancer(), $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 { $db=$services->getDBLoadBalancer() ->getConnection(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(), $services->getDBLoadBalancer());}, '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
Groups multiple message groups together as one group.
Factory class for accessing message groups individually by id or all of them as a list.
Creates a database of keys in all groups, so that namespace and key can be used to get the groups the...
Service for searching message groups and message keys.
searchMessages(string $query, int $maxResults)
Search message prefixes.
matchMessages(string $query)
Match messages matching a pattern.
Wraps the translatable page sections into a message group.