Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function gp_machine_translate_action_callback() {
$locale = $_POST['locale'];
$strings = array( $_POST['original'] );

$new_string = $gp_machine_translate->translate_batch( $locale, $strings );
$new_string = $gp_machine_translate->batchTranslate( $locale, $strings );

if( is_wp_error( $new_string ) ) {
$translations = array( 'success' => false, 'error' => array( 'message' => $new_string->get_error_message(), 'reason' => $new_string->get_error_data() ) );
Expand Down
86 changes: 86 additions & 0 deletions classes/Providers/AbstractProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace GpMachineTranslate\Providers;

use GpMachineTranslate\Providers\Traits\Normalizer;
use WP_Error;

abstract class AbstractProvider implements ProviderInterface
{
use Normalizer;

protected ?string $authClientId = null;

protected ?string $authKey = null;

public function __construct(?string $authClientId, ?string $authKey)
{
$this->authClientId = $authClientId;
$this->authKey = $authKey;
}

public function getAuthClientId(): ?string
{
return $this->authClientId;
}

public function getAuthKey(): ?string
{
return $this->authKey;
}

public function getDisplayName(): string
{
return static::NAME;
}

public function getLocales(): array
{
return static::LOCALE_MAPPING;
}

/**
* Checks if the setup requirements for authentication are met.
*
* The method verifies if the authentication key and client ID are set
* when they are required. If either of these required values are missing,
* the setup is considered incomplete.
*
* @return bool true if the setup requirements are met; false otherwise
*/
public function isSetUp(): bool
{
if ($this->requiresAuthKey() && $this->authKey === null) {
return false;
}

return !($this->requiresAuthClientId() && $this->authClientId === null);
}

public function requiresAuthClientId(): bool
{
return static::REQUIRES_AUTH_CLIENT_ID;
}

public function requiresAuthKey(): bool
{
return static::REQUIRES_AUTH_KEY;
}

public function validateTranslationArguments(string $locale, array $strings): ?WP_Error
{
// If we don't have a supported translation code, throw an error.
if (!array_key_exists($locale, $this->getLocales())) {
return new WP_Error('gp_machine_translate', sprintf("The locale %s isn't supported by %s.", $locale, $this->getDisplayName()));
}

// If we don't have any strings, throw an error.
if (count($strings) == 0) {
return new WP_Error('gp_machine_translate', 'No strings found to translate.');
}

return null;
}
}
94 changes: 94 additions & 0 deletions classes/Providers/DeepLProProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace GpMachineTranslate\Providers;

use WP_Error;

class DeepLProProvider extends DeepLProvider
{
public const API_URL = 'https://api.deepl.com';

public const IDENTIFIER = 'DeepL Pro';

protected const NAME = 'DeepL - Pro';

/**
* @return array|WP_Error
*/
public function batchTranslate(string $locale, array $strings)
{
$isValid = $this->validateTranslationArguments($locale, $strings);

if ($isValid !== null) {
return $isValid;
}

$glossaryId = $this->getGlossaryId($locale);
$translationData = $this->performTranslationRequest(
$this->getRequestBody($strings, $locale, $glossaryId),
);

if ($translationData instanceof WP_Error) {
return $translationData;
}

return $this->getTranslatedStringsArray($translationData, $strings);
}

private function getGlossaries(): ?array
{
$glossaries = null;

if ($this->authKey === null) {
return $glossaries;
}

$response = wp_remote_get(
self::API_URL . self::ENDPOINT_GLOSSARIES,
[
'headers' => [
'Authorization' => 'DeepL-Auth-Key ' . $this->authKey,
],
],
);

if (is_wp_error($response)) {
return $response;
}

$jsonResponse = json_decode(
wp_remote_retrieve_body($response),
);

foreach ($jsonResponse->glossaries as $glossary) {
if (isset($glossary->glossary_id, $glossary->target_lang)) {
$glossaries[] = $glossary;
}
}

return $glossaries;
}

private function getGlossaryId(string $locale): ?string
{
$glossaryId = null;
$glossaries = $this->getGlossaries();
$targetLanguage = $this->getLocales()[$locale];

if ($glossaries === null) {
return null;
}

foreach ($glossaries as $glossary) {
if (isset($glossary->glossary_id, $glossary->target_lang) && $glossary->target_lang === $targetLanguage) {
$glossaryId = $glossary->glossary_id;

break;
}
}

return is_string($glossaryId) ? $glossaryId : null;
}
}
206 changes: 206 additions & 0 deletions classes/Providers/DeepLProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

declare(strict_types=1);

namespace GpMachineTranslate\Providers;

use WP_Error;

class DeepLProvider extends AbstractProvider
{
public const API_URL = 'https://api-free.deepl.com';

public const ENDPOINT_GLOSSARIES = '/v2/glossaries';

public const ENDPOINT_TRANSLATE = '/v2/translate';

public const IDENTIFIER = 'DeepL';

protected const LOCALE_MAPPING = [
'ar' => 'ar',
'bg' => 'bg',
'cs' => 'cs',
'da' => 'da',
'de' => 'de',
'el' => 'el',
'en' => 'en_us',
'en_gb' => 'en_gb',
'es' => 'es',
'et' => 'et',
'fi' => 'fi',
'fr' => 'fr',
'hu' => 'hu',
'it' => 'it',
'id' => 'id',
'ja' => 'ja',
'ko' => 'ko',
'lt' => 'lt',
'lv' => 'lv',
'nb' => 'nb',
'no' => 'nb',
'nl' => 'nl',
'pl' => 'pl',
'pt' => 'pt-pt',
'pt_br' => 'pt-br',
'ro' => 'ro',
'ru' => 'ru',
'sk' => 'sk',
'sl' => 'sl',
'sv' => 'sv',
'tr' => 'tr',
'uk' => 'uk',
'zh_cn' => 'zh',
];

protected const NAME = 'DeepL - Free';

protected const REQUIRES_AUTH_CLIENT_ID = false;

protected const REQUIRES_AUTH_KEY = true;

private const SPECIAL_CHARACTERS = [
'original' => [
' & ',
'»',
'&raquo;',
],
'replacement' => [
' <mask-amp> ',
'<mask-raquo>',
'<mask-raquo>',
],
];

/**
* @return array|WP_Error
*/
public function batchTranslate(string $locale, array $strings)
{
$isValid = $this->validateTranslationArguments($locale, $strings);

if ($isValid !== null) {
return $isValid;
}

$translationData = $this->performTranslationRequest(
$this->getRequestBody($strings, $locale),
);

if ($translationData instanceof WP_Error) {
return $translationData;
}

return $this->getTranslatedStringsArray($translationData, $strings);
}

public function validateTranslationArguments(string $locale, array $strings): ?WP_Error
{
$isValid = parent::validateTranslationArguments($locale, $strings);

if ($isValid !== null) {
return $isValid;
}

// If we have too many strings, throw an error.
if (count($strings) > 50) {
return new WP_Error('gp_machine_translate', 'Only 50 strings allowed.');
}

return null;
}

protected function escapeSpecialCharacters(string $text): string
{
return str_replace(self::SPECIAL_CHARACTERS['original'], self::SPECIAL_CHARACTERS['replacement'], $text);
}

protected function getRequestBody(array $strings, string $locale, ?string $glossaryId = null): array
{
$requestBody = [
'source_lang' => 'en',
'target_lang' => $this->getLocales()[$locale],
'tag_handling' => 'xml',
'text' => [],
];

if ($glossaryId) {
$requestBody['glossary_id'] = $glossaryId;
}

foreach ($strings as $string) {
$requestBody['text'][] = $this->escapeSpecialCharacters($string);
}

return $requestBody;
}

protected function getTranslatedStringsArray(object $deepLTranslationData, array $strings)
{
// Setup an temporary array to use to process the response.
$translations = [];
$translatedStrings = array_column($deepLTranslationData->translations, 'text');

// Merge the originals and translations arrays.
$items = gp_array_zip($strings, $translatedStrings);

// If there are no items, throw an error.
if (!$items) {
return new WP_Error('gp_machine_translate', 'Error merging arrays');
}

// Loop through the items and clean up the responses.
foreach ($items as $item) {
list($string, $translation) = $item;

$translations[] = $this->unescapeSpecialCharacters(
$this->normalizePlaceholders($translation),
);
}

return $translations;
}

/**
* @return object|WP_Error
*/
protected function performTranslationRequest(array $requestBody)
{
$response = wp_remote_post(
static::API_URL . self::ENDPOINT_TRANSLATE,
[
'method' => 'POST',
'headers' => [
'Authorization' => 'DeepL-Auth-Key ' . $this->authKey,
'Content-Type' => 'application/json',
],
'body' => json_encode($requestBody),
],
);

// Did we get an error?
if (is_wp_error($response)) {
return $response;
}

// Decode the response from DeepL.
$json = json_decode(
wp_remote_retrieve_body($response),
);

// If something went wrong with the response from DeepL, throw an error.
if (!$json || !isset($json->translations)) {
return new WP_Error('gp_machine_translate', 'Error decoding JSON from DeepL Translate.');
}

if (isset($json->error)) {
return new WP_Error('gp_machine_translate', sprintf('Error auto-translating: %1$s', $json->error->errors[0]->message));
}

return $json;
}

protected function unescapeSpecialCharacters(string $text): string
{
return str_replace(self::SPECIAL_CHARACTERS['replacement'], self::SPECIAL_CHARACTERS['original'], $text);
}
}
Loading