481 lines
13 KiB
PHP
481 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* @package Polylang-Pro
|
|
*/
|
|
|
|
namespace WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Settings;
|
|
|
|
use PLL_Language;
|
|
use PLL_Model;
|
|
use WP_Error;
|
|
use WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Ajax\Deepl as Ajax;
|
|
use WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Clients\Deepl as Client;
|
|
use WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Languages;
|
|
use WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Services\Deepl as Service;
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* Machine translation settings: DeepL.
|
|
*
|
|
* @since 3.6
|
|
*
|
|
* @phpstan-type DeeplOptions array{
|
|
* api_key: string,
|
|
* formality: 'default'|'prefer_more'|'prefer_less'
|
|
* }
|
|
*/
|
|
class Deepl implements Settings_Interface {
|
|
/**
|
|
* Name of the action to check the API key.
|
|
*
|
|
* @since 3.6
|
|
*
|
|
* @var string
|
|
*/
|
|
const API_KEY_ACTION = 'pll_deepl_check_api_key';
|
|
|
|
/**
|
|
* Name of the action to get the DeepL usage.
|
|
*
|
|
* @since 3.6
|
|
*
|
|
* @var string
|
|
*/
|
|
const USAGE_ACTION = 'pll_deepl_get_usage';
|
|
|
|
/**
|
|
* Service.
|
|
*
|
|
* @var Service
|
|
*/
|
|
private $service;
|
|
|
|
/**
|
|
* Polylang's model.
|
|
*
|
|
* @var PLL_Model
|
|
*/
|
|
private $model;
|
|
|
|
/**
|
|
* Base of the name attribute used by the inputs.
|
|
*
|
|
* @var string
|
|
*
|
|
* @phpstan-var non-falsy-string
|
|
*/
|
|
private $input_base_name;
|
|
|
|
/**
|
|
* Stores the fields' options.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $options;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 3.6
|
|
*
|
|
* @param string $input_base_name Base of the name attribute used by the inputs.
|
|
* Can contain a placeholder `{slug}` that will be replaced by the service's slug.
|
|
* Ex: `machine_translation_services[{slug}]`.
|
|
* @param array $options Service's options.
|
|
* @param Service $service Service.
|
|
* @param PLL_Model $model Polylang's model.
|
|
*
|
|
* @phpstan-param non-falsy-string $input_base_name
|
|
*/
|
|
public function __construct( string $input_base_name, array $options, Service $service, PLL_Model $model ) {
|
|
$this->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' ),
|
|
'<a href="https://www.deepl.com/pro-api">',
|
|
'<a href="https://www.deepl.com/account/summary">',
|
|
'</a>'
|
|
),
|
|
'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' ),
|
|
'<a href="https://www.deepl.com/account/summary">',
|
|
'</a>'
|
|
),
|
|
'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' ),
|
|
'<a href="https://www.deeplstatus.com/">',
|
|
'</a>'
|
|
),
|
|
),
|
|
'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<string>
|
|
*/
|
|
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<string>
|
|
*/
|
|
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( '<code>%s</code>', $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';
|
|
}
|
|
}
|