Skip to content

Commit

Permalink
Remove locale files and refactor providers
Browse files Browse the repository at this point in the history
Deleted locale files for various services and refactored the code to use provider-specific classes. Added glossary support for DeepL.
  • Loading branch information
BenBornschein committed Oct 29, 2024
1 parent 8edf896 commit a785fa6
Show file tree
Hide file tree
Showing 29 changed files with 1,723 additions and 1,087 deletions.
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;
}
}
233 changes: 233 additions & 0 deletions classes/Providers/DeepLProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?php

declare(strict_types=1);

namespace GpMachineTranslate\Providers;

use WP_Error;

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

public const API_URL_FREE = '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';

protected const REQUIRES_AUTH_CLIENT_ID = false;

protected const REQUIRES_AUTH_KEY = false;

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

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

$targetLanguage = $this->getLocales()[$locale];
$requestHeaders = [
'Content-Type' => 'application/json',
];
$requestBody = [
'source_lang' => 'en',
'target_lang' => $targetLanguage,
'tag_handling' => 'xml',
'text' => [],
];
$apiUrl = self::API_URL_FREE;

if ($this->authKey !== null) {
$apiUrl = self::API_URL;
$requestHeaders['Authorization'] = 'DeepL-Auth-Key ' . $this->authKey;
$glossaryId = $this->getGlossaryId($targetLanguage);

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

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

$response = wp_remote_post(
$apiUrl . self::ENDPOINT_TRANSLATE,
[
'method' => 'POST',
'headers' => $requestHeaders,
'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));
}

// Setup an temporary array to use to process the response.
$translations = [];
$translatedStrings = array_column($json->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;
}

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

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 $targetLanguage): ?string
{
$glossaryId = null;
$glossaries = $this->getGlossaries();

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

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

0 comments on commit a785fa6

Please sign in to comment.