Dictionaryvalidator - Source Excerpt 02
Summary
This source excerpt preserves a bounded section of Antichrist.net/wp-content/plugins/uaix-locale-router/src/DictionaryValidator.php so readers can inspect the evidence without opening the full source file.
**Source path:** Antichrist.net/wp-content/plugins/uaix-locale-router/src/DictionaryValidator.php
if ( ! empty( $expected_key_map ) ) {
$same_as_source_ratio = $same_as_source / count( $expected_key_map );
if ( $same_as_source_ratio > 0.8 && '' !== $locale_tag && 'en-US' !== $locale_tag ) {
$issues[] = self::issue( 'warning', 'same_as_source_ratio', 'Most translated values still match the template source.' );
}
}
if ( strlen( (string) $filename ) > 0 && strlen( (string) $filename ) > 190 ) {
$issues[] = self::issue( 'warning', 'filename_length', 'The uploaded filename is unusually long.' );
}
$result = array(
'localeTag' => $locale_tag,
'templateVersion' => $template_version,
'qualityStatus' => $quality_status,
'dictionaryKeyMode' => $key_mode,
'issues' => $issues,
'normalized_dictionary' => array(
'meta' => array_merge(
array(
'locale' => $locale_tag,
'sourceLocale' => 'en-US',
'version' => isset( $dictionary['meta']['version'] ) ? absint( $dictionary['meta']['version'] ) : $template_version,
'fallback' => isset( $dictionary['meta']['fallback'] ) ? LocaleValidator::canonicalize_tag( $dictionary['meta']['fallback'] ) : LocaleRepository::get_default_locale(),
'generatedFrom' => isset( $dictionary['meta']['generatedFrom'] ) ? (string) $dictionary['meta']['generatedFrom'] : 'template',
'qualityStatus' => $quality_status,
'keyMode' => 'source-text',
),
isset( $normalized['translatorDictionary']['meta'] ) && is_array( $normalized['translatorDictionary']['meta'] ) ? $normalized['translatorDictionary']['meta'] : array()
),
'strings' => isset( $normalized['translatorDictionary']['strings'] ) && is_array( $normalized['translatorDictionary']['strings'] )
? $normalized['translatorDictionary']['strings']
: array(),
),
);
return self::finalize_result( $result );
}
/**
* Validate all stored dictionaries.
*
* @return array
*/
public static function validate_all() {
$results = array();
foreach ( DictionaryRepository::stored_dictionary_files() as $file_path ) {
$results[] = self::validate_file( $file_path );
}
return $results;
}
/**
* Record a validation result in the history table.
*
* @param array $result Validation result.
* @param int $user_id User ID.
* @param string $file_hash File hash.
* @return void
*/
public static function record_validation_result( array $result, $user_id = 0, $file_hash = '' ) {
global $wpdb;
if ( ! Plugin::validation_table_exists() ) {
Plugin::maybe_create_validation_table();
}
if ( ! Plugin::validation_table_exists() ) {
Plugin::log_bootstrap_issue( 'validation_record', 'Validation history table is unavailable; skipping history write.' );
return;
}
$issues = isset( $result['issues'] ) && is_array( $result['issues'] ) ? $result['issues'] : array();
$wpdb->insert(
Plugin::validation_table_name(),
array(
'LocaleTag' => isset( $result['localeTag'] ) ? (string) $result['localeTag'] : '',
'FileHash' => (string) $file_hash,
'TemplateVersion' => isset( $result['templateVersion'] ) ? absint( $result['templateVersion'] ) : 1,
'Status' => isset( $result['status'] ) ? (string) $result['status'] : 'draft',
'IssueCount' => count( $issues ),
'IssuesJson' => wp_json_encode( $issues ),
'CreatedUtc' => gmdate( 'Y-m-d H:i:s' ),
'CreatedByUserId' => absint( $user_id ),
),
array( '%s', '%s', '%d', '%s', '%d', '%s', '%s', '%d' )
);
}
/**
* Extract named placeholders from a value.
*
* @param string $value Text value.
* @return array
*/
private static function extract_placeholders( $value ) {
if ( ! is_string( $value ) || '' === $value ) {
return array();
}
preg_match_all( '/\{([a-zA-Z0-9_.-]+)\}/', $value, $matches );
$placeholders = isset( $matches[1] ) && is_array( $matches[1] ) ? array_values( array_unique( $matches[1] ) ) : array();
sort( $placeholders );
return $placeholders;
}
/**
* Build a normalized issue structure.
*
* @param string $severity Severity label.
* @param string $code Machine code.
* @param string $message Human-readable message.
* @param array $context Extra context.
* @return array
*/
private static function issue( $severity, $code, $message, array $context = array() ) {
return array(
'severity' => $severity,
'code' => $code,
'message' => $message,
'context' => $context,
);
}
/**
* Validate one dictionary key before it is stored or reported.
*
* @param mixed $key Dictionary key.
* @return bool
*/
private static function is_safe_dictionary_key( $key ) {
if ( ! is_string( $key ) || '' === $key || strlen( $key ) > 400 ) {
return false;
}
if ( false !== strpos( $key, '..' ) || preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $key ) ) {
return false;
}
return true;
}
/**
* Finalize a validation result.
*
* @param array $result Partial result.
* @return array
*/
private static function finalize_result( array $result ) {
$issues = isset( $result['issues'] ) && is_array( $result['issues'] ) ? $result['issues'] : array();
$has_errors = false;
$has_warnings = false;
foreach ( $issues as $issue ) {
if ( isset( $issue['severity'] ) && 'error' === $issue['severity'] ) {
$has_errors = true;
}
if ( isset( $issue['severity'] ) && 'warning' === $issue['severity'] ) {
$has_warnings = true;
}
}
$result['status'] = $has_errors ? 'rejected' : ( $has_warnings ? 'reviewed' : 'approved' );
$result['issueCount'] = count( $issues );
return $result;
}
}