Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ValidationRunner.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Extension\Translate\Validation;
13
14use Exception;
15use InvalidArgumentException;
20use MediaWiki\Json\FormatJson;
21use RuntimeException;
22
41 protected $validators = [];
43 protected $groupId;
45 private static $ignorePatterns;
46
47 public function __construct( string $groupId ) {
48 if ( self::$ignorePatterns === null ) {
49 // TODO: Review if this logic belongs in this class.
50 self::reloadIgnorePatterns();
51 }
52
53 $this->groupId = $groupId;
54 }
55
57 protected static function foldValue( string $value ): string {
58 return str_replace( ' ', '_', strtolower( $value ) );
59 }
60
69 public function setValidators( array $validatorConfigs ): void {
70 $this->validators = [];
71 foreach ( $validatorConfigs as $config ) {
72 $this->addValidator( $config );
73 }
74 }
75
80 public function addValidator( array $validatorConfig ): void {
81 $validatorId = $validatorConfig['id'] ?? null;
82 $className = $validatorConfig['class'] ?? null;
83
84 if ( $validatorId !== null ) {
85 $validator = ValidatorFactory::get(
86 $validatorId,
87 $validatorConfig['params'] ?? null
88 );
89 } elseif ( $className !== null ) {
90 $validator = ValidatorFactory::loadInstance(
91 $className,
92 $validatorConfig['params'] ?? null
93 );
94 } else {
95 throw new InvalidArgumentException(
96 'Validator configuration does not specify the \'class\' or \'id\'.'
97 );
98 }
99
100 $isInsertable = $validatorConfig['insertable'] ?? false;
101 if ( $isInsertable && !$validator instanceof InsertablesSuggester ) {
102 $actualClassName = get_class( $validator );
103 throw new InvalidArgumentException(
104 "Insertable validator $actualClassName does not implement InsertablesSuggester interface."
105 );
106 }
107
108 $this->validators[] = [
109 'instance' => $validator,
110 'insertable' => $isInsertable,
111 'enforce' => $validatorConfig['enforce'] ?? false,
112 'include' => $validatorConfig['keymatch'] ?? $validatorConfig['include'] ?? false,
113 'exclude' => $validatorConfig['exclude'] ?? false
114 ];
115 }
116
122 public function getValidators(): array {
123 return array_column( $this->validators, 'instance' );
124 }
125
132 public function getInsertableValidators(): array {
133 $insertableValidators = [];
134 foreach ( $this->validators as $validator ) {
135 if ( $validator['insertable'] === true ) {
136 $insertableValidators[] = $validator['instance'];
137 }
138 }
139
140 return $insertableValidators;
141 }
142
148 public function validateMessage(
149 Message $message,
150 string $code,
151 bool $ignoreWarnings = false
153 $errors = new ValidationIssues();
154 $warnings = new ValidationIssues();
155
156 foreach ( $this->validators as $validator ) {
157 $this->runValidation(
158 $validator,
159 $message,
160 $code,
161 $errors,
162 $warnings,
163 $ignoreWarnings
164 );
165 }
166
167 $errors = $this->filterValidations( $message->key(), $errors, $code );
168 $warnings = $this->filterValidations( $message->key(), $warnings, $code );
169
170 return new ValidationResult( $errors, $warnings );
171 }
172
174 public function quickValidate(
175 Message $message,
176 string $code,
177 bool $ignoreWarnings = false
179 $errors = new ValidationIssues();
180 $warnings = new ValidationIssues();
181
182 foreach ( $this->validators as $validator ) {
183 $this->runValidation(
184 $validator,
185 $message,
186 $code,
187 $errors,
188 $warnings,
189 $ignoreWarnings
190 );
191
192 $errors = $this->filterValidations( $message->key(), $errors, $code );
193 $warnings = $this->filterValidations( $message->key(), $warnings, $code );
194
195 if ( $warnings->hasIssues() || $errors->hasIssues() ) {
196 break;
197 }
198 }
199
200 return new ValidationResult( $errors, $warnings );
201 }
202
204 public static function reloadIgnorePatterns(): void {
205 $validationExclusionFile = Services::getInstance()->getConfigHelper()->getValidationExclusionFile();
206
207 if ( $validationExclusionFile === false ) {
208 self::$ignorePatterns = [];
209 return;
210 }
211
212 $list = PHPVariableLoader::loadVariableFromPHPFile(
213 $validationExclusionFile,
214 'validationExclusionList'
215 );
216 $keys = [ 'group', 'check', 'subcheck', 'code', 'message' ];
217
218 if ( $list && !is_array( $list ) ) {
219 throw new InvalidArgumentException(
220 "validationExclusionList defined in $validationExclusionFile must be an array"
221 );
222 }
223
224 foreach ( $list as $key => $pattern ) {
225 foreach ( $keys as $checkKey ) {
226 if ( !isset( $pattern[$checkKey] ) ) {
227 $list[$key][$checkKey] = '#';
228 } elseif ( is_array( $pattern[$checkKey] ) ) {
229 $list[$key][$checkKey] =
230 array_map(
231 [ self::class, 'foldValue' ],
232 $pattern[$checkKey]
233 );
234 } else {
235 $list[$key][$checkKey] = self::foldValue( $pattern[$checkKey] );
236 }
237 }
238 }
239
240 self::$ignorePatterns = $list;
241 }
242
244 private function filterValidations(
245 string $messageKey,
246 ValidationIssues $issues,
247 string $targetLanguage
248 ): ValidationIssues {
249 $filteredIssues = new ValidationIssues();
250
251 foreach ( $issues as $issue ) {
252 foreach ( self::$ignorePatterns as $pattern ) {
253 if ( $this->shouldIgnore( $messageKey, $issue, $this->groupId, $targetLanguage, $pattern ) ) {
254 continue 2;
255 }
256 }
257 $filteredIssues->add( $issue );
258 }
259
260 return $filteredIssues;
261 }
262
263 private function shouldIgnore(
264 string $messageKey,
265 ValidationIssue $issue,
266 string $messageGroupId,
267 string $targetLanguage,
268 array $pattern
269 ): bool {
270 return $this->matchesIgnorePattern( $pattern['group'], $messageGroupId )
271 && $this->matchesIgnorePattern( $pattern['check'], $issue->type() )
272 && $this->matchesIgnorePattern( $pattern['subcheck'], $issue->subType() )
273 && $this->matchesIgnorePattern( $pattern['message'], $messageKey )
274 && $this->matchesIgnorePattern( $pattern['code'], $targetLanguage );
275 }
276
284 private function matchesIgnorePattern( $pattern, string $value ): bool {
285 if ( $pattern === '#' ) {
286 return true;
287 } elseif ( is_array( $pattern ) ) {
288 return in_array( strtolower( $value ), $pattern, true );
289 } else {
290 return strtolower( $value ) === $pattern;
291 }
292 }
293
302 protected function doesKeyMatch( string $key, array $keyMatches ): bool {
303 $normalizedKey = lcfirst( $key );
304 foreach ( $keyMatches as $match ) {
305 if ( is_string( $match ) ) {
306 if ( lcfirst( $match ) === $normalizedKey ) {
307 return true;
308 }
309 continue;
310 }
311
312 // The value is neither a string nor an array, should never happen but still handle it.
313 if ( !is_array( $match ) ) {
314 throw new InvalidArgumentException(
315 "Invalid key matcher configuration passed. Expected type: array or string. " .
316 "Received: " . get_debug_type( $match ) . ". match value: " . FormatJson::encode( $match )
317 );
318 }
319
320 $matcherType = $match['type'];
321 $pattern = $match['pattern'];
322
323 // If regex matches, or wildcard matches return true, else continue processing.
324 if (
325 ( $matcherType === 'regex' && preg_match( $pattern, $normalizedKey ) === 1 ) ||
326 ( $matcherType === 'wildcard' && fnmatch( $pattern, $normalizedKey ) )
327 ) {
328 return true;
329 }
330 }
331
332 return false;
333 }
334
340 private function runValidation(
341 array $validatorData,
342 Message $message,
343 string $targetLanguage,
344 ValidationIssues $errors,
345 ValidationIssues $warnings,
346 bool $ignoreWarnings
347 ): void {
348 // Check if key match has been specified, and then check if the key matches it.
350 $validator = $validatorData['instance'];
351
352 $definition = $message->definition();
353
354 try {
355 $includedKeys = $validatorData['include'];
356 if ( $includedKeys !== false && !$this->doesKeyMatch( $message->key(), $includedKeys ) ) {
357 return;
358 }
359
360 $excludedKeys = $validatorData['exclude'];
361 if ( $excludedKeys !== false && $this->doesKeyMatch( $message->key(), $excludedKeys ) ) {
362 return;
363 }
364
365 if ( $validatorData['enforce'] === true ) {
366 $errors->merge( $validator->getIssues( $message, $targetLanguage ) );
367 } elseif ( !$ignoreWarnings ) {
368 $warnings->merge( $validator->getIssues( $message, $targetLanguage ) );
369 }
370 // else: caller does not want warnings, skip running the validator
371 } catch ( Exception $e ) {
372 throw new RuntimeException(
373 'An error occurred while validating message: ' . $message->key() . '; group: ' .
374 $this->groupId . "; validator: " . get_class( $validator ) . "\n. Exception: $e"
375 );
376 }
377 }
378}
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
Interface for message objects used by MessageCollection.
Definition Message.php:13
Minimal service container.
Definition Services.php:60
Stuff for handling configuration files in PHP format.
Container for validation issues returned by MessageValidator.
Message validator is used to run validators to find common mistakes so that translators can fix them ...
quickValidate(Message $message, string $code, bool $ignoreWarnings=false)
Validate a message, and return as soon as any validation fails.
static foldValue(string $value)
Normalise validator keys.
getValidators()
Return the currently set validators for this group.
addValidator(array $validatorConfig)
Add a validator for this group.
setValidators(array $validatorConfigs)
Set the validators for this group.
getInsertableValidators()
Return currently set validators that are insertable.
doesKeyMatch(string $key, array $keyMatches)
Check if key matches validator's key patterns.
validateMessage(Message $message, string $code, bool $ignoreWarnings=false)
Validate a translation of a message.