Skip to content
wiki.fftac.org

Dictionaryvalidator - Source Excerpt 02

Back to Dictionaryvalidator

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;
	}
}