service = $service; $this->model = $model; $this->input_base_name = str_replace( '{slug}', $service::get_slug(), $input_base_name ); $this->options = $options; add_action( 'wp_ajax_' . self::API_KEY_ACTION, array( $this, 'check_api_key' ) ); add_action( 'wp_ajax_' . self::USAGE_ACTION, array( $this, 'update_characters_consumption_view' ) ); } /** * Ajax callback that checks for the API key validity. * * @since 3.6 * * @return void * * @phpstan-return never */ public function check_api_key() { check_ajax_referer( self::API_KEY_ACTION, '_pll_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( -1 ); } if ( empty( $_GET['api_key'] ) || ! is_string( $_GET['api_key'] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Please fill in the API key field.', 'polylang-pro' ), 'message_class' => 'pll-message-error-auth', // See `get_error_message_class()`. ) ); } $valid = $this->is_api_key_valid( array( 'api_key' => (string) sanitize_text_field( wp_unslash( $_GET['api_key'] ) ), 'formality' => 'default', ) ); if ( $valid->has_errors() ) { // The key is invalid or we had a failure while checking it. wp_send_json_error( array( 'message' => esc_html( $valid->get_error_message() ), 'message_class' => $this->get_error_message_class( $valid ), ) ); } wp_send_json_success(); } /** * Displays the characters consumption view. * * @since 3.6 * * @return void * * @phpstan-return never */ public function update_characters_consumption_view() { check_ajax_referer( self::USAGE_ACTION, '_pll_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( -1 ); } $usage = $this->service->get_client()->get_usage(); if ( is_wp_error( $usage ) ) { // Error while retrieving the data: display the error message. wp_send_json_error( array( 'message' => esc_html( sprintf( /* translators: %s is an error message. */ __( 'Error while retrieving the data: %s.', 'polylang-pro' ), $usage->get_error_message() ) ), ) ); } if ( ! $usage['character_limit'] ) { // The character limit is 0: display only the character count. wp_send_json_success( array( 'message' => esc_html( sprintf( /* translators: %s is a formatted count number. */ _n( '%s translated character.', '%s translated characters.', $usage['character_count'], 'polylang-pro' ), number_format_i18n( $usage['character_count'] ) ) ), ) ); } // Display a graphic. $percent = round( $usage['character_count'] * 100 / $usage['character_limit'], 1 ); $percent = (float) min( $percent, 100 ); $decimals = 1; if ( floor( $percent ) === $percent ) { $decimals = 0; } wp_send_json_success( array( 'percent_formatted' => number_format_i18n( $percent, $decimals ) . '%', 'percent' => (string) $percent . '%', 'message' => esc_html( sprintf( /* translators: %1$s is a formatted count number, %2$s is a formatted limit number. */ _n( '%1$s / %2$s translated character.', '%1$s / %2$s translated characters.', $usage['character_count'], 'polylang-pro' ), number_format_i18n( $usage['character_count'] ), number_format_i18n( $usage['character_limit'] ) ) ), ) ); } /** * Tells if the given service options contain a non-empty authentication key. * * @since 3.6 * * @param array $options Options for this service. * @return bool */ public function has_api_key( array $options ): bool { return ! empty( $options['api_key'] ) && is_string( $options['api_key'] ) && '' !== trim( $options['api_key'] ); } /** * Tells if the authentication key from the given service options is valid by contacting the service. * * @since 3.6 * * @param array $options Options for this service (must be sanitized beforehand). * @return WP_Error { * An empty `WP_Error` if the authentication succeeded. * In the other cases, the `WP_Error` data will contain an array as follow: * * @type string $type `'error'` if the API key is invalid, or `'warning'` if there was an error while * contacting the service. * @type string $field_id CSS ID of the field in fault. * } */ public function is_api_key_valid( array $options ): WP_Error { if ( ! $this->has_api_key( $options ) ) { $options['api_key'] = ''; } $error = ( new Service( $options, $this->model ) )->get_client()->is_api_key_valid(); if ( ! $error->has_errors() ) { // The key is valid. return $error; } $error->add_data( array( 'message_class' => $this->get_error_message_class( $error ), 'field_id' => 'pll-deepl-api-key', ) ); return $error; } /** * Sanitizes and validates the options for this service. * * @since 3.6 * * @param array $options Options for this service. * @return array Validated options. * * @phpstan-return DeeplOptions */ public function sanitize_options( array $options ): array { $new_options = array( 'api_key' => '', 'formality' => 'default', ); if ( $this->has_api_key( $options ) ) { $new_options['api_key'] = (string) sanitize_text_field( $options['api_key'] ); } if ( isset( $options['formality'] ) && in_array( $options['formality'], array( 'prefer_more', 'prefer_less' ), true ) ) { $new_options['formality'] = $options['formality']; } // Return only the validated options. return $new_options; } /** * Prints error notices. * * @since 3.6 * * @return void */ public function print_notices() { if ( $this->service->is_active() ) { $this->print_view( 'inner-notices-row', array( 'name' => $this->service->get_name(), 'languages' => $this->get_unsupported_languages(), ) ); } } /** * Prints settings fields. * * @since 3.6 * * @return void */ public function print_settings_fields() { if ( $this->service->is_active() ) { $this->print_view( 'characters-consumption-row', array( 'ajax_action' => self::USAGE_ACTION, ) ); } $this->print_view( 'service-authentication-row', array( 'ajax_action' => self::API_KEY_ACTION, /* translators: %s is a service name. */ 'button_label' => sprintf( __( 'Check connection to %s', 'polylang-pro' ), $this->service->get_name() ), 'id' => 'deepl-api-key', 'message_default' => sprintf( /* translators: %1$s is an opening link tag leading to account creation, %2$s is an opening link tag leading to the account page, %3$s is a closing link tag. */ __( '%1$sCreate your account on DeepL%3$s, then %2$sfind your API key at the bottom of your account page%3$s.', 'polylang-pro' ), '', '', '' ), 'messages_error' => array( 'pll-message-error-auth' => sprintf( /* translators: %1$s is an opening link tag leading to the service's account page, %2$s is a closing link tag. */ __( '%1$sVerify your API key at the bottom of your DeepL account page%2$s.', 'polylang-pro' ), '', '' ), 'pll-message-error-unavailable' => sprintf( /* translators: %1$s is an opening link tag leading to the service's status page, %2$s is a closing link tag. */ __( 'You can look at %1$sthe DeepL Pro/Free API\'s status%2$s.', 'polylang-pro' ), '', '' ), ), 'message_success' => __( 'Your API key is valid.', 'polylang-pro' ), 'option' => 'api_key', 'title' => __( 'API key', 'polylang-pro' ), ) ); $this->print_view( 'deepl-formality-row', array( 'option' => 'formality', 'formal' => $this->get_active_languages_by_formality( 'formal' ), 'informal' => $this->get_active_languages_by_formality( 'informal' ), ) ); } /** * Prints a view. * * @since 3.6 * * @param string $view Name of the view. * @param array $atts Optional. Data to print. See views headers. * @return void */ private function print_view( string $view, array $atts = array() ) { $atts['slug'] = $this->service::get_slug(); $atts['input_base_name'] = $this->input_base_name; if ( isset( $atts['option'] ) && ! isset( $atts['value'] ) ) { $atts['value'] = $this->options[ $atts['option'] ] ?? ''; } include __DIR__ . "/views/view-{$view}.php"; } /** * Returns the lists of languages that are not supported by the service. * * @since 3.6 * @return string[] Array of language names (and their locale). * * @phpstan-return list */ private function get_unsupported_languages(): array { $languages = array(); foreach ( $this->model->get_languages_list() as $language ) { if ( empty( $this->service::get_target_code( $language ) ) ) { $languages[] = $this->get_language_label( $language ); } } sort( $languages ); return $languages; } /** * Returns the lists of active formal or informal languages. * Formal languages have a locale with a `_formal` suffix (`de_DE_formal`, `nl_NL_formal`), * Informal languages have a `_informal` suffix (`de_CH_informal`). * * @since 3.6 * * @param string $formality Formality. * @return string[] Array of arrays of language names (and their locale). * * @phpstan-param 'formal'|'informal' $formality * @phpstan-return list */ private function get_active_languages_by_formality( string $formality ): array { $languages = array(); foreach ( $this->model->get_languages_list() as $language ) { if ( empty( $this->service::get_target_code( $language ) ) ) { continue; } if ( ! preg_match( "@_{$formality}$@", $language->locale ) ) { continue; } $languages[] = $this->get_language_label( $language ); } sort( $languages ); return $languages; } /** * Returns a language name and its locale. * * @since 3.6 * * @param PLL_Language $language A language object. * @return string */ private function get_language_label( PLL_Language $language ): string { return sprintf( /* translators: %1$s is a language name, %2$s is a language locale. */ _x( '%1$s (%2$s)', 'Language label', 'polylang-pro' ), $language->name, sprintf( '%s', $language->locale ) ); } /** * Returns the HTML class corresponding to the given error. * See the array keys for the 'messages_error' in `print_settings_fields()`. * * @since 3.6 * * @param WP_Error $error An error object. * @return string `'pll-message-error-auth'` in case of authentication failure, `'pll-message-error-unavailable'` in other cases. */ private function get_error_message_class( WP_Error $error ): string { return 'pll_deepl_authentication_failure' === $error->get_error_code() ? 'pll-message-error-auth' : 'pll-message-error-unavailable'; } }