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 private static $ignorePatterns;
44
45 public function __construct(
46 private readonly string $groupId,
47 ) {
48 if ( self::$ignorePatterns === null ) {
49 // TODO: Review if this logic belongs in this class.
50 self::reloadIgnorePatterns();
51 }
52 }
53
55 protected static function foldValue( string $value ): string {
56 return str_replace( ' ', '_', strtolower( $value ) );
57 }
58
67 public function setValidators( array $validatorConfigs ): void {
68 $this->validators = [];
69 foreach ( $validatorConfigs as $config ) {
70 $this->addValidator( $config );
71 }
72 }
73
78 public function addValidator( array $validatorConfig ): void {
79 $validatorId = $validatorConfig['id'] ?? null;
80 $className = $validatorConfig['class'] ?? null;
81
82 if ( $validatorId !== null ) {
83 $validator = ValidatorFactory::get(
84 $validatorId,
85 $validatorConfig['params'] ?? null
86 );
87 } elseif ( $className !== null ) {
88 $validator = ValidatorFactory::loadInstance(
89 $className,
90 $validatorConfig['params'] ?? null
91 );
92 } else {
93 throw new InvalidArgumentException(
94 'Validator configuration does not specify the \'class\' or \'id\'.'
95 );
96 }
97
98 $isInsertable = $validatorConfig['insertable'] ?? false;
99 if ( $isInsertable && !$validator instanceof InsertablesSuggester ) {
100 $actualClassName = get_class( $validator );
101 throw new InvalidArgumentException(
102 "Insertable validator $actualClassName does not implement InsertablesSuggester interface."
103 );
104 }
105
106 $this->validators[] = [
107 'instance' => $validator,
108 'insertable' => $isInsertable,
109 'enforce' => $validatorConfig['enforce'] ?? false,
110 'include' => $validatorConfig['keymatch'] ?? $validatorConfig['include'] ?? false,
111 'exclude' => $validatorConfig['exclude'] ?? false
112 ];
113 }
114
120 public function getValidators(): array {
121 return array_column( $this->validators, 'instance' );
122 }
123
130 public function getInsertableValidators(): array {
131 $insertableValidators = [];
132 foreach ( $this->validators as $validator ) {
133 if ( $validator['insertable'] === true ) {
134 $insertableValidators[] = $validator['instance'];
135 }
136 }
137
138 return $insertableValidators;
139 }
140
146 public function validateMessage(
147 Message $message,
148 string $code,
149 bool $ignoreWarnings = false
151 $errors = new ValidationIssues();
152 $warnings = new ValidationIssues();
153
154 foreach ( $this->validators as $validator ) {
155 $this->runValidation(
156 $validator,
157 $message,
158 $code,
159 $errors,
160 $warnings,
161 $ignoreWarnings
162 );
163 }
164
165 $errors = $this->filterValidations( $message->key(), $errors, $code );
166 $warnings = $this->filterValidations( $message->key(), $warnings, $code );
167
168 return new ValidationResult( $errors, $warnings );
169 }
170
172 public function quickValidate(
173 Message $message,
174 string $code,
175 bool $ignoreWarnings = false
177 $errors = new ValidationIssues();
178 $warnings = new ValidationIssues();
179
180 foreach ( $this->validators as $validator ) {
181 $this->runValidation(
182 $validator,
183 $message,
184 $code,
185 $errors,
186 $warnings,
187 $ignoreWarnings
188 );
189
190 $errors = $this->filterValidations( $message->key(), $errors, $code );
191 $warnings = $this->filterValidations( $message->key(), $warnings, $code );
192
193 if ( $warnings->hasIssues() || $errors->hasIssues() ) {
194 break;
195 }
196 }
197
198 return new ValidationResult( $errors, $warnings );
199 }
200
202 public static function reloadIgnorePatterns(): void {
203 $validationExclusionFile = Services::getInstance()->getConfigHelper()->getValidationExclusionFile();
204
205 if ( $validationExclusionFile === false ) {
206 self::$ignorePatterns = [];
207 return;
208 }
209
210 $list = PHPVariableLoader::loadVariableFromPHPFile(
211 $validationExclusionFile,
212 'validationExclusionList'
213 );
214 $keys = [ 'group', 'check', 'subcheck', 'code', 'message' ];
215
216 if ( $list && !is_array( $list ) ) {
217 throw new InvalidArgumentException(
218 "validationExclusionList defined in $validationExclusionFile must be an array"
219 );
220 }
221
222 foreach ( $list as $key => $pattern ) {
223 foreach ( $keys as $checkKey ) {
224 if ( !isset( $pattern[$checkKey] ) ) {
225 $list[$key][$checkKey] = '#';
226 } elseif ( is_array( $pattern[$checkKey] ) ) {
227 $list[$key][$checkKey] =
228 array_map(
229 [ self::class, 'foldValue' ],
230 $pattern[$checkKey]
231 );
232 } else {
233 $list[$key][$checkKey] = self::foldValue( $pattern[$checkKey] );
234 }
235 }
236 }
237
238 self::$ignorePatterns = $list;
239 }
240
242 private function filterValidations(
243 string $messageKey,
244 ValidationIssues $issues,
245 string $targetLanguage
246 ): ValidationIssues {
247 $filteredIssues = new ValidationIssues();
248
249 foreach ( $issues as $issue ) {
250 foreach ( self::$ignorePatterns as $pattern ) {
251 if ( $this->shouldIgnore( $messageKey, $issue, $this->groupId, $targetLanguage, $pattern ) ) {
252 continue 2;
253 }
254 }
255 $filteredIssues->add( $issue );
256 }
257
258 return $filteredIssues;
259 }
260
261 private function shouldIgnore(
262 string $messageKey,
263 ValidationIssue $issue,
264 string $messageGroupId,
265 string $targetLanguage,
266 array $pattern
267 ): bool {
268 return $this->matchesIgnorePattern( $pattern['group'], $messageGroupId )
269 && $this->matchesIgnorePattern( $pattern['check'], $issue->type() )
270 && $this->matchesIgnorePattern( $pattern['subcheck'], $issue->subType() )
271 && $this->matchesIgnorePattern( $pattern['message'], $messageKey )
272 && $this->matchesIgnorePattern( $pattern['code'], $targetLanguage );
273 }
274
282 private function matchesIgnorePattern( $pattern, string $value ): bool {
283 if ( $pattern === '#' ) {
284 return true;
285 } elseif ( is_array( $pattern ) ) {
286 return in_array( strtolower( $value ), $pattern, true );
287 } else {
288 return strtolower( $value ) === $pattern;
289 }
290 }
291
300 protected function doesKeyMatch( string $key, array $keyMatches ): bool {
301 $normalizedKey = lcfirst( $key );
302 foreach ( $keyMatches as $match ) {
303 if ( is_string( $match ) ) {
304 if ( lcfirst( $match ) === $normalizedKey ) {
305 return true;
306 }
307 continue;
308 }
309
310 // The value is neither a string nor an array, should never happen but still handle it.
311 if ( !is_array( $match ) ) {
312 throw new InvalidArgumentException(
313 "Invalid key matcher configuration passed. Expected type: array or string. " .
314 "Received: " . get_debug_type( $match ) . ". match value: " . FormatJson::encode( $match )
315 );
316 }
317
318 $matcherType = $match['type'];
319 $pattern = $match['pattern'];
320
321 // If regex matches, or wildcard matches return true, else continue processing.
322 if (
323 ( $matcherType === 'regex' && preg_match( $pattern, $normalizedKey ) === 1 ) ||
324 ( $matcherType === 'wildcard' && fnmatch( $pattern, $normalizedKey ) )
325 ) {
326 return true;
327 }
328 }
329
330 return false;
331 }
332
338 private function runValidation(
339 array $validatorData,
340 Message $message,
341 string $targetLanguage,
342 ValidationIssues $errors,
343 ValidationIssues $warnings,
344 bool $ignoreWarnings
345 ): void {
346 // Check if key match has been specified, and then check if the key matches it.
348 $validator = $validatorData['instance'];
349
350 $definition = $message->definition();
351
352 try {
353 $includedKeys = $validatorData['include'];
354 if ( $includedKeys !== false && !$this->doesKeyMatch( $message->key(), $includedKeys ) ) {
355 return;
356 }
357
358 $excludedKeys = $validatorData['exclude'];
359 if ( $excludedKeys !== false && $this->doesKeyMatch( $message->key(), $excludedKeys ) ) {
360 return;
361 }
362
363 if ( $validatorData['enforce'] === true ) {
364 $errors->merge( $validator->getIssues( $message, $targetLanguage ) );
365 } elseif ( !$ignoreWarnings ) {
366 $warnings->merge( $validator->getIssues( $message, $targetLanguage ) );
367 }
368 // else: caller does not want warnings, skip running the validator
369 } catch ( Exception $e ) {
370 throw new RuntimeException(
371 'An error occurred while validating message: ' . $message->key() . '; group: ' .
372 $this->groupId . "; validator: " . get_class( $validator ) . "\n. Exception: $e"
373 );
374 }
375 }
376}
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->getConnectionProvider());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.