diff --git a/ajax.php b/ajax.php index 3052b36..44a43c8 100644 --- a/ajax.php +++ b/ajax.php @@ -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() ) ); diff --git a/classes/Providers/AbstractProvider.php b/classes/Providers/AbstractProvider.php new file mode 100644 index 0000000..a9719a3 --- /dev/null +++ b/classes/Providers/AbstractProvider.php @@ -0,0 +1,86 @@ +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; + } +} diff --git a/classes/Providers/DeepLProProvider.php b/classes/Providers/DeepLProProvider.php new file mode 100644 index 0000000..8155187 --- /dev/null +++ b/classes/Providers/DeepLProProvider.php @@ -0,0 +1,94 @@ +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; + } +} diff --git a/classes/Providers/DeepLProvider.php b/classes/Providers/DeepLProvider.php new file mode 100644 index 0000000..7edd1f1 --- /dev/null +++ b/classes/Providers/DeepLProvider.php @@ -0,0 +1,206 @@ + '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' => [ + ' & ', + 'ยป', + '»', + ], + 'replacement' => [ + ' ', + '', + '', + ], + ]; + + /** + * @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); + } +} diff --git a/classes/Providers/GoogleTranslateProvider.php b/classes/Providers/GoogleTranslateProvider.php new file mode 100644 index 0000000..90c2a87 --- /dev/null +++ b/classes/Providers/GoogleTranslateProvider.php @@ -0,0 +1,195 @@ + 'af', + 'ar' => 'ar', + 'az' => 'az', + 'be' => 'be', + 'bg' => 'bg', + 'bn_bd' => 'bn', + 'bs' => 'bs', + 'ca' => 'ca', + 'cs' => 'cs', + 'cy' => 'cy', + 'da' => 'da', + 'de' => 'de', + 'el' => 'el', + 'en' => 'en', + 'en_au' => 'en', + 'en_ca' => 'en', + 'en_gb' => 'en', + 'en_nz' => 'en', + 'en_za' => 'en', + 'eo' => 'eo', + 'es' => 'es', + 'es_ar' => 'es', + 'es_cl' => 'es', + 'es_co' => 'es', + 'es_gt' => 'es', + 'es_mx' => 'es', + 'es_pe' => 'es', + 'es_pr' => 'es', + 'es_ve' => 'es', + 'et' => 'et', + 'eu' => 'eu', + 'fa' => 'fa', + 'fa_af' => 'fa', + 'fi' => 'fi', + 'fr' => 'fr', + 'ga' => 'ga', + 'gd' => 'gd', + 'gl' => 'gl', + 'gu' => 'gu', + 'ha' => 'ha', + 'he' => 'iw', + 'hi' => 'hi', + 'hr' => 'hr', + 'hu' => 'hu', + 'hy' => 'hy', + 'id' => 'id', + 'is' => 'is', + 'it' => 'it', + 'ja' => 'ja', + 'jv' => 'jw', + 'ka' => 'ka', + 'kk' => 'kk', + 'km' => 'km', + 'kn' => 'kn', + 'ko' => 'ko', + 'la' => 'la', + 'lo' => 'lo', + 'lt' => 'lt', + 'lv' => 'lv', + 'mg' => 'mg', + 'mk' => 'mk', + 'ml' => 'ml', + 'mn' => 'mn', + 'mr' => 'mr', + 'mri' => 'mi', + 'ms' => 'ms', + 'my' => 'my', + 'nb' => 'no', + 'ne' => 'ne', + 'nl' => 'nl', + 'nl_be' => 'nl', + 'nn' => 'no', + 'no' => 'no', + 'pa' => 'pa', + 'pl' => 'pl', + 'pt' => 'pt-PT', + 'pt_br' => 'pt-BR', + 'ro' => 'ro', + 'ru' => 'ru', + 'si' => 'si', + 'sk' => 'sk', + 'sl' => 'sl', + 'so' => 'so', + 'sq' => 'sq', + 'sr' => 'sr', + 'su' => 'su', + 'sv' => 'sv', + 'sw' => 'sw', + 'ta' => 'ta', + 'ta_lk' => 'ta', + 'te' => 'te', + 'tg' => 'tg', + 'th' => 'th', + 'tl' => 'tl', + 'tr' => 'tr', + 'uk' => 'uk', + 'ur' => 'ur', + 'uz' => 'uz', + 'vi' => 'vi', + 'yi' => 'yi', + 'yor' => 'yo', + 'zh_cn' => 'zh-CN', + 'zh_tw' => 'zh-TW', + ]; + + protected const NAME = 'Google Translate'; + + protected const REQUIRES_AUTH_CLIENT_ID = false; + + protected const REQUIRES_AUTH_KEY = true; + + /** + * @return array|WP_Error + */ + public function batchTranslate(string $locale, array $strings) + { + $isValid = $this->validateTranslationArguments($locale, $strings); + + if ($isValid !== null) { + return $isValid; + } + + // This is the URL of the Google API. + $url = 'https://www.googleapis.com/language/translate/v2?key=' . $this->getAuthKey() . '&source=en&target=' . urlencode($this->getLocales()[$locale]); + + // Loop through the stings and add them to the $url as a query string. + foreach ($strings as $string) { + $url .= '&q=' . urlencode($string); + } + + // If we just have a single string, add an extra q= to the end so Google things we're doing multiple strings. + if (count($strings) == 1) { + $url .= '&q='; + } + + // Get the response from Google. + $response = wp_remote_get($url); + + // Did we get an error? + if (is_wp_error($response)) { + return $response; + } + + // Decode the response from Google. + $json = json_decode(wp_remote_retrieve_body($response)); + + // If something went wrong with the response from Google, throw an error. + if (!$json) { + return new WP_Error('gp_machine_translate', 'Error decoding JSON from Google 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 = []; + + // If the translations have been return as a single entry, make it an array so it's easier to process later. + if (!is_array($json->data->translations)) { + $json->data->translations = [$json->data->translations]; + } + + // Merge the originals and translations arrays. + $items = gp_array_zip($strings, $json->data->translations); + + // 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->normalizePlaceholders($translation->translatedText); + } + + // Return the results. + return $translations; + } +} diff --git a/classes/Providers/MicrosoftTranslatorProvider.php b/classes/Providers/MicrosoftTranslatorProvider.php new file mode 100644 index 0000000..1a1b9ca --- /dev/null +++ b/classes/Providers/MicrosoftTranslatorProvider.php @@ -0,0 +1,91 @@ + 'af', + 'ar' => 'ar', + 'bg' => 'bg', + 'bs' => 'bs-Latn', + 'ca' => 'ca', + 'cs' => 'cs', + 'cy' => 'cy', + 'da' => 'da', + 'de' => 'de', + 'el' => 'el', + 'es' => 'es', + 'et' => 'et', + 'fa' => 'fa', + 'fi' => 'fi', + 'fr' => 'fr', + 'he' => 'iw', + 'hi' => 'hi', + 'hr' => 'hr', + 'ht' => 'ht', + 'hu' => 'hu', + 'id' => 'id', + 'it' => 'it', + 'ja' => 'ja', + 'ko' => 'ko', + 'lt' => 'lt', + 'lv' => 'lv', + 'ms' => 'ms', + 'mt' => 'mt', + 'mww' => 'mww', + 'nl' => 'nl', + 'no' => 'no', + 'otq' => 'otq', + 'pl' => 'pl', + 'pt' => 'pt-PT', + 'ro' => 'ro', + 'ru' => 'ru', + 'sk' => 'sk', + 'sl' => 'sl', + 'sr' => 'sr-Cyrl', + 'sv' => 'sv', + 'sw' => 'sw', + 'th' => 'th', + 'tlh' => 'tlh', + 'tr' => 'tr', + 'uk' => 'uk', + 'ur' => 'ur', + 'vi' => 'vi', + 'yua' => 'yua', + 'zh_cn' => 'zh-CHS', + ]; + + protected const NAME = 'Microsoft Translator'; + + protected const REQUIRES_AUTH_CLIENT_ID = true; + + protected const REQUIRES_AUTH_KEY = true; + + /** + * @return array|WP_Error + */ + public function batchTranslate(string $locale, array $strings) + { + $isValid = $this->validateTranslationArguments($locale, $strings); + + if ($isValid !== null) { + return $isValid; + } + + $config = [ + 'clientID' => $this->getAuthClientId(), + 'clientSecret' => $this->getAuthKey(), + ]; + $t = new Translate($config); + + return $t->translate($strings, $this->getLocales()[$locale], 'en'); + } +} diff --git a/classes/Providers/ProviderInterface.php b/classes/Providers/ProviderInterface.php new file mode 100644 index 0000000..b239021 --- /dev/null +++ b/classes/Providers/ProviderInterface.php @@ -0,0 +1,26 @@ + DeepLProvider::class, + DeepLProProvider::IDENTIFIER => DeepLProProvider::class, + GoogleTranslateProvider::IDENTIFIER => GoogleTranslateProvider::class, + MicrosoftTranslatorProvider::IDENTIFIER => MicrosoftTranslatorProvider::class, + YandexTranslateProvider::IDENTIFIER => YandexTranslateProvider::class, + ]; + + public function __construct(?string $authClientId, ?string $authKey) + { + $this->authClientId = !empty($authClientId) ? $authClientId : null; + $this->authKey = !empty($authKey) ? $authKey : null; + } + + public function getOrCreateProviderInstance(string $providerIdentifier): AbstractProvider + { + $this->ensureProviderExists($providerIdentifier); + + if (!isset($this->instances[$providerIdentifier])) { + $this->instances[$providerIdentifier] = new $this->registry[$providerIdentifier]($this->authClientId, $this->authKey); + } + + return $this->instances[$providerIdentifier]; + } + + /** + * @return array + */ + public function getProviderIdentifiers(): array + { + $providersIdentifiers = []; + + foreach ($this->registry as $providerIdentifier => $providerClass) { + /** @var \GpMachineTranslate\Providers\ProviderInterface $provider */ + $providerInstance = $this->getOrCreateProviderInstance($providerIdentifier); + $providersIdentifiers[] = $providerInstance::IDENTIFIER; + } + + return $providersIdentifiers; + } + + /** + * @return array + */ + public function getProvidersDisplayName(): array + { + $list = []; + + foreach ($this->registry as $providerIdentifier => $providerClass) { + /** @var \GpMachineTranslate\Providers\ProviderInterface $provider */ + $providerInstance = $this->getOrCreateProviderInstance($providerIdentifier); + $list[$providerIdentifier] = $providerInstance->getDisplayName(); + } + + return $list; + } + + public function updateOrCreateProviderInstance(string $providerIdentifier, string $authClientId, string $authKey): AbstractProvider + { + $this->ensureProviderExists($providerIdentifier); + + $this->authClientId = !empty($authClientId) ? $authClientId : null; + $this->authKey = !empty($authKey) ? $authKey : null; + $this->instances[$providerIdentifier] = new $this->registry[$providerIdentifier]($this->authClientId, $this->authKey); + + return $this->instances[$providerIdentifier]; + } + + private function ensureProviderExists(string $providerIdentifier) + { + if (!isset($this->registry[$providerIdentifier])) { + throw new LogicException('Provider with identifier ' . $providerIdentifier . ' does not exist.'); + } + } +} diff --git a/classes/Providers/Traits/Normalizer.php b/classes/Providers/Traits/Normalizer.php new file mode 100644 index 0000000..635579b --- /dev/null +++ b/classes/Providers/Traits/Normalizer.php @@ -0,0 +1,28 @@ + 'be', + 'ca' => 'ca', + 'cs' => 'cs', + 'da' => 'da', + 'de' => 'de', + 'el' => 'el', + 'es' => 'es', + 'et' => 'et', + 'fi' => 'fi', + 'fr' => 'fr', + 'hu' => 'hu', + 'it' => 'it', + 'lt' => 'lt', + 'lv' => 'lv', + 'mk' => 'mk', + 'nl' => 'nl', + 'no' => 'no', + 'pt' => 'pt', + 'ru' => 'ru', + 'sk' => 'sk', + 'sl' => 'sl', + 'sv' => 'sv', + 'tr' => 'tr', + 'uk' => 'uk', + ]; + + protected const NAME = 'Powered by Yandex.Translate'; + + protected const REQUIRES_AUTH_CLIENT_ID = false; + + protected const REQUIRES_AUTH_KEY = true; + + /** + * @return array|WP_Error + */ + public function batchTranslate(string $locale, array $strings) + { + $isValid = $this->validateTranslationArguments($locale, $strings); + + if ($isValid !== null) { + return $isValid; + } + + // This is the URL of the Yandex API. + $url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' . $this->getAuthKey() . '&lang=en-' . urlencode($this->getLocales()[$locale]); + + // Loop through the stings and add them to the $url as a query string. + foreach ($strings as $string) { + $url .= '&text=' . urlencode($string); + } + + // Get the response from Yandex. + $response = wp_remote_get($url); + + // Did we get an error? + if (is_wp_error($response)) { + return $response; + } + + // Decode the response from Yandex. + $json = json_decode(wp_remote_retrieve_body($response)); + + // If something went wrong with the response from Yandex, throw an error. + if (!$json) { + return new WP_Error('gp_machine_translate', 'Error decoding JSON from Yandex 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 = []; + + // If the translations have been return as a single entry, make it an array so it's easier to process later. + if (!is_array($json->text)) { + $json->text = [$json->text]; + } + + // Merge the originals and translations arrays. + $items = gp_array_zip($strings, $json->text); + + // 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->normalizePlaceholders($translation); + } + + // Return the results. + return $translations; + } +} diff --git a/classes/Template.php b/classes/Template.php new file mode 100644 index 0000000..5730a8b --- /dev/null +++ b/classes/Template.php @@ -0,0 +1,38 @@ +templateFiles[$templateName] = $file; + } + } + + public function render(string $template, array $templateData = []): string + { + if (!isset($this->templateFiles[$template])) { + return str_replace( + '{{ templateFile }}', + $template, + __('Unknown template file "{{ templateFile }}".', 'gp-machine-translate'), + ); + } + + ob_start(); + + include $this->templateFiles[$template]; + + return ob_get_clean(); + } +} diff --git a/composer.json b/composer.json index 9ad11c9..af2ef4e 100644 --- a/composer.json +++ b/composer.json @@ -1,4 +1,9 @@ { + "autoload": { + "psr-4": { + "GpMachineTranslate\\": "classes" + } + }, "require": { "natxet/microsoft-translation-api": "*" }, diff --git a/deepl.locales.php b/deepl.locales.php deleted file mode 100644 index a65585d..0000000 --- a/deepl.locales.php +++ /dev/null @@ -1,37 +0,0 @@ - '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', -); diff --git a/google.locales.php b/google.locales.php deleted file mode 100644 index 1bb6818..0000000 --- a/google.locales.php +++ /dev/null @@ -1,108 +0,0 @@ - 'af', - 'ar' => 'ar', - 'az' => 'az', - 'be' => 'be', - 'bg' => 'bg', - 'bn_bd' => 'bn', - 'bs' => 'bs', - 'ca' => 'ca', - 'cs' => 'cs', - 'cy' => 'cy', - 'da' => 'da', - 'de' => 'de', - 'de' => 'de', - 'el' => 'el', - 'en' => 'en', - 'en_au' => 'en', - 'en_ca' => 'en', - 'en_gb' => 'en', - 'en_nz' => 'en', - 'en_za' => 'en', - 'eo' => 'eo', - 'es' => 'es', - 'es_ar' => 'es', - 'es_cl' => 'es', - 'es_co' => 'es', - 'es_gt' => 'es', - 'es_mx' => 'es', - 'es_pe' => 'es', - 'es_pr' => 'es', - 'es_ve' => 'es', - 'et' => 'et', - 'eu' => 'eu', - 'fa' => 'fa', - 'fa_af' => 'fa', - 'fi' => 'fi', - 'fr' => 'fr', - 'ga' => 'ga', - 'gd' => 'gd', - 'gl' => 'gl', - 'gu' => 'gu', - 'ha' => 'ha', - 'he' => 'iw', - 'hi' => 'hi', - 'hr' => 'hr', - 'hu' => 'hu', - 'hy' => 'hy', - 'id' => 'id', - 'is' => 'is', - 'it' => 'it', - 'ja' => 'ja', - 'jv' => 'jw', - 'ka' => 'ka', - 'kk' => 'kk', - 'km' => 'km', - 'kn' => 'kn', - 'ko' => 'ko', - 'la' => 'la', - 'lo' => 'lo', - 'lt' => 'lt', - 'lv' => 'lv', - 'mg' => 'mg', - 'mk' => 'mk', - 'ml' => 'ml', - 'mn' => 'mn', - 'mr' => 'mr', - 'mri' => 'mi', - 'ms' => 'ms', - 'my' => 'my', - 'nb' => 'no', - 'ne' => 'ne', - 'nl' => 'nl', - 'nl_be' => 'nl', - 'nn' => 'no', - 'no' => 'no', - 'pa' => 'pa', - 'pl' => 'pl', - 'pt' => 'pt-PT', - 'pt_br' => 'pt-BR', - 'ro' => 'ro', - 'ru' => 'ru', - 'si' => 'si', - 'sk' => 'sk', - 'sl' => 'sl', - 'so' => 'so', - 'sq' => 'sq', - 'sr' => 'sr', - 'su' => 'su', - 'sv' => 'sv', - 'sw' => 'sw', - 'ta' => 'ta', - 'ta_lk' => 'ta', - 'te' => 'te', - 'tg' => 'tg', - 'th' => 'th', - 'tl' => 'tl', - 'tr' => 'tr', - 'uk' => 'uk', - 'ur' => 'ur', - 'uz' => 'uz', - 'vi' => 'vi', - 'yi' => 'yi', - 'yor' => 'yo', - 'zh_cn' => 'zh-CN', - 'zh_tw' => 'zh-TW', -); \ No newline at end of file diff --git a/gp-machine-translate.php b/gp-machine-translate.php index 961b34c..67ebb72 100644 --- a/gp-machine-translate.php +++ b/gp-machine-translate.php @@ -1,4 +1,6 @@ providers = array( 'DeepL', 'Google Translate', 'Microsoft Translator', 'Yandex.Translate' ); - $this->banners = array( 'DeepL' => 'DeepL', 'Google Translate' => 'Google Translate', 'Microsoft Translator' => 'Microsoft Translator', 'Yandex.Translate' => 'Powered by Yandex.Translate' ); - $provider_includes = array( 'DeepL' => 'deepl.locales.php', 'Yandex.Translate' => 'yandex.locales.php', 'Microsoft Translator' => 'microsoft.locales.php', 'Google Translate' => 'google.locales.php' ); - $provider_key_required = array( 'DeepL' => true, 'Google Translate' => true, 'Microsoft Translator' => true, 'Yandex.Translate' => true ); - - if( get_option( 'gp_machine_translate_version', '0.7' ) != $this->version ) { - $this->upgrade(); - } - - // Get the global translate provider from the WordPress options table. - $this->provider = get_option( 'gp_machine_translate_provider', 'DeepL' ); - - // Set the key requirement. - $this->key_required = $provider_key_required[$this->provider]; - - // Get the global translate key from the WordPress options table. - $this->key = get_option( 'gp_machine_translate_key' ); - - // Get the global translate key from the WordPress options table. - $this->client_id = get_option('gp_machine_translate_client_id'); - - $gp_machine_translate_locales = array(); - - // Include the provider code, otherwise bail out. - if( in_array( $this->provider, $this->providers ) ) { - include( $provider_includes[$this->provider] ); - } else { - return; - } - - $this->locales = $gp_machine_translate_locales; - - // Check to see if there is a user currently logged in. - if ( is_user_logged_in() ) { - // If someone is logged in, get their user object. - $user_obj = wp_get_current_user(); - - // Load the user translate key from the WordPress user meta table, using the currently logged in user id. - $user_key = get_user_meta( $user_obj->ID, 'gp_machine_translate_key', true ); - $user_client_id = get_user_meta( $user_obj->ID, 'gp_machine_translate_client_id', true ); - - // If there is a user key, override the global key. - if( $user_key ) { - $this->key = $user_key; - $this->client_id = $user_client_id; - } - } - - // If we didn't find a global or user key and one is required, return and don't setup and of the actions. - if( false === $this->key && true == $this->key_required) { return; } - - wp_register_script( 'gp-machine-translate-js', plugins_url( 'gp-machine-translate.js', __FILE__ ), array( 'jquery', 'editor', 'gp-common' ) ); - - // If the user has write permissions to the projects, add the bulk translate option to the projects menu. - if( GP::$permission->user_can( wp_get_current_user(), 'write', 'project' ) ) { - add_action( 'gp_project_actions', array( $this, 'gp_project_actions'), 10, 2 ); - } - - // Add the actions to handle adding the translate menu to the various parts of GlotPress. - add_action( 'gp_pre_tmpl_load', array( $this, 'gp_pre_tmpl_load'), 10, 2); - add_filter( 'gp_entry_actions', array( $this, 'gp_entry_actions' ), 10, 1 ); - add_action( 'gp_translation_set_bulk_action', array( $this, 'gp_translation_set_bulk_action'), 10, 1); - add_action( 'gp_translation_set_bulk_action_post', array( $this, 'gp_translation_set_bulk_action_post'), 10, 4); - - // We can't use the filter in the defaults route code because plugins don't load until after - // it has already run, so instead add the routes directly to the global GP_Router object. - GP::$router->add( "/bulk-translate/(.+?)", array( $this, 'bulk_translate' ), 'get' ); - GP::$router->add( "/bulk-translate/(.+?)", array( $this, 'bulk_translate' ), 'post' ); - } - - // Generate the HTML when a user views their profile. - public function show_user_profile( $user ) { - // Show and edit are virtually identical, so just call the edit function. - $this->edit_user_profile( $user ); - } - - // Generate the HTML when a user profile is edited. Note the $user parameter is a full user object for this function. - public function edit_user_profile( $user ) { - // Get the current user key from the WordPress options table. - $user_key = get_user_meta( $user->ID, 'gp_machine_translate_key', true ); - - // Get the current user client id from the WordPress options table. - $user_client_id = get_user_meta( $user->ID, 'gp_machine_translate_client_id', true ); - - // If the user cannot edit their profile, then don't show the settings. - if ( !current_user_can( 'edit_user', $user_id ) ) { return false; } -?> -

- - - - - - - - - -
- -

provider );?>

-
- -

-
-personal_options_update( $user ); - } - - // This function adds the "Machine Translate" option to the projects menu. - public function gp_project_actions( $actions, $project ) { - $actions[] .= gp_link_get( gp_url( 'bulk-translate/' . $project->slug), __( 'Machine Translate', 'gp-machine-translate' ) . ' (' . $this->banners[$this->provider] . ')' ); - - return $actions; - } - - // This function is here as placeholder to support adding the bulk translate option to the router. - // Without this placeholder there is a fatal error generated. - public function before_request() { - } - - // This function handles the actual bulk translate as passed in by the router for the projects menu. - public function bulk_translate( $project_path ) { - // First let's ensure we have decoded the project path for use later. - $project_path = urldecode( $project_path ); - - // Get the URL to the project for use later. - $url = gp_url_project( $project_path ); - - // If we don't have rights, just redirect back to the project. - if( !GP::$permission->user_can( wp_get_current_user(), 'write', 'project' ) ) { - wp_redirect( $url ); - } - - // Create a project class to use to get the project object. - $project_class = new GP_Project; - - // Get the project object from the project path that was passed in. - $project_obj = $project_class->by_path( $project_path ); - - // Get the translations sets from the project ID. - $translation_sets = GP::$translation_set->by_project_id( $project_obj->id ); - - // Since there might be a lot of translations to process in a batch, let's setup some time limits - // to make sure we don't give a white screen of death to the user. - $time_start = microtime( true ); - $max_exec_time = ini_get( 'max_execution_time' ) * 0.7; - - // Loop through all the sets. - foreach( $translation_sets as $set ) { - // Check to see how our time is doing, if we're over out time limit, stop processing. - if ( microtime( true ) - $time_start > $max_exec_time ) { - gp_notice_set( __( 'Not all strings translated as we ran out of execution time!', 'gp-machine-translate' ) ); - break; - } - - // Get the locale we're working with. - $locale = GP_Locales::by_slug( $set->locale ); - - // If the current translation provider doesn't support this locale, skip it. - if ( ! array_key_exists( $locale->slug, $this->locales ) ) { - continue; - } - // Create a template array to pass in to the worker function at the end of the loop. - $bulk = array( 'action' => 'gp_machine_translate', 'priority' => 0, 'row-ids' => array() ); - - // Create a new GP_Translation object to use. - $translation = new GP_Translation; - - // Get the strings for the current translation. - $strings = $translation->for_translation( $project_obj, $set, 'no-limit', array( 'status' => 'untranslated') ); - - // Add the strings to the $bulk template we setup earlier. - foreach ( $strings as $string ) { - $bulk['row-ids'][] .= $string->row_id; - } - - // If we don't have any strings to translate, don't bother calling the translation function. - if ( count( $bulk['row-ids'] ) > 0 ) { - // Do the actual bulk translation. - $this->gp_translation_set_bulk_action_post( $project_obj, $locale, $set, $bulk ); - } - } - - // Redirect back to the project home. - wp_redirect( $url ); - } - - // This function is here as placeholder to support adding the bulk translate option to the router. - // Without this placeholder there is a fatal error generated. - public function after_request() { - } - - // This function loads the javascript when required. - public function gp_pre_tmpl_load( $template, $args ) { - // If we don't have a translation key, just return without doing anything. - if( ! $this->key && $this->key_required ) { - return; - } - - // If we're not on the translation template, just return without doing anything. - if ( 'translations' != $template ) { - return; - } - - // If the current locale isn't supported by the translation provider, just return without doing anything. - if ( ! array_key_exists( $args['locale']->slug, $this->locales ) ) { - return; - } - - // Create options for the localization script. - $options = array( - 'key' => $this->key, - 'locale' => $this->locales[$args['locale']->slug], - 'ajaxurl' => admin_url( 'admin-ajax.php'), - ); - - // Set the current Google code to the locale we're dealing with. - $this->provider_code = $this->locales[$args['locale']->slug]; - - // Enqueue the translation JavaScript and translate it. - gp_enqueue_script( 'gp-machine-translate-js' ); - wp_localize_script( 'gp-machine-translate-js', 'gp_machine_translate', $options ); - } - - // This function adds the "Machine Translate" to the individual translation items. - public function gp_entry_actions( $actions ) { - // Make sure we are currently on a supported locale. - if ( $this->provider_code ) { - $actions[] = '' . __( 'Machine Translate', 'gp-machine-translate' ) . ' (' . $this->banners[$this->provider] . ')'; - } - - return $actions; - } - - // This function adds the "Machine Translate" to the bulk actions dropdown in the translation set list. - public function gp_translation_set_bulk_action() { - // Make sure we are currently on a supported locale. - if ( $this->provider_code ) { - echo ''; - } - } - - // This function handles the actual bulk translation as passed in by the translation set list. - public function gp_translation_set_bulk_action_post( $project, $locale, $translation_set, $bulk ) { - // If we're not doing a bulk translation, just return. - if ( 'gp_machine_translate' != $bulk['action'] ) { - return; - } - - // Setup some variables to be used during the translation. - $provider_errors = 0; - $insert_errors = 0; - $ok = 0; - $skipped = 0; - - $singulars = array(); - $original_ids = array(); - - // Loop through each of the passed in strings and translate them. - foreach ( $bulk['row-ids'] as $row_id ) { - // Split the $row_id by '-' and get the first one (which will be the id of the original). - $original_id = gp_array_get( explode( '-', $row_id ), 0 ); - // Get the original based on the above id. - $original = GP::$original->get( $original_id ); - - // If there is no original or it's a plural, skip it. - if ( ! $original || $original->plural ) { - $skipped++; - continue; - } - - // Add the original to the queue to translate. - $singulars[] = $original->singular; - $original_ids[] = $original_id; - } - - // Translate all the originals that we found. - $results = $this->translate_batch( $locale, $singulars ); - - // Did we get an error? - if ( is_wp_error( $results ) ) { - error_log( print_r( $results, true ) ); - gp_notice_set( $results->get_error_message(), 'error' ); - return; - - } - - // Merge the results back in to the original id's and singulars, this will create an array like ($items = array( array( id, single, result), array( id, single, result), ... ). - $items = gp_array_zip( $original_ids, $singulars, $results ); - - // If we have no items, something went wrong and stop processing. - if ( ! $items ) { - return; - } - - // Loop through the items and store them in the database. - foreach ( $items as $item ) { - // Break up the item back in to individual components. - list( $original_id, $singular, $translation ) = $item; - - // Did we get an error? - if ( is_wp_error( $translation ) ) { - $provider_errors++; - error_log( $translation->get_error_message() ); - continue; - } - - // Build a data array to store - $data = compact( 'original_id' ); - $data['user_id'] = get_current_user_id(); - $data['translation_set_id'] = $translation_set->id; - $data['translation_0'] = $translation; - $data['status'] = 'fuzzy'; - $data['warnings'] = GP::$translation_warnings->check( $singular, null, array( $translation ), $locale ); - - // Insert the item in to the database. - $inserted = GP::$translation->create( $data ); - $inserted? $ok++ : $insert_errors++; - } - - // Did we get an error? If so let's let the user know about them. - if ( $provider_errors > 0 || $insert_errors > 0 ) { - // Create a message array to use later. - $message = array(); - - // Did we have any strings translated successfully? - if ( $ok ) { - $message[] = sprintf( __( 'Added: %d.', 'gp-machine-translate' ), $ok ); - } - - // Did we have any provider errors. - if ( $provider_errors ) { - $message[] = sprintf( __( 'Error from %s: %d.', 'gp-machine-translate' ), $this->provider, $provider_errors ); - } - - // Did we have any errors when we saved everything to the database? - if ( $insert_errors ) { - $message[] = sprintf( __( 'Error adding: %d.', 'gp-machine-translate' ), $insert_errors ); - } - - // Did we skip any items? - if ( $skipped ) { - $message[] = sprintf( __( 'Skipped: %d.', 'gp-machine-translate' ), $skipped ); - } - - // Create a message string and add it to the GlotPress notices. - gp_notice_set( implode( '', $message ), 'error' ); - } - else { - // If we didn't get any errors, then we just need to let the user know how many translations were added. - gp_notice_set( sprintf( __( '%d fuzzy translation from Machine Translate were added.', 'gp-machine-translate' ), $ok ) ); - } - } - - public function translate_batch( $locale, $strings ) { - - if( is_object( $locale ) ) { - $locale = $locale->slug; - } - - switch( $this->provider ) { - case 'DeepL': - return $this->deepl_translate_batch( $locale, $strings ); - - case 'Google Translate': - return $this->google_translate_batch( $locale, $strings ); - - break; - case 'Microsoft Translator': - return $this->microsoft_translate_batch( $locale, $strings ); - - break; - case 'Yandex.Translate': - return $this->yandex_translate_batch( $locale, $strings ); - - break; - } - } - - private function microsoft_translate_batch( $locale, $strings ) { - // If we don't have a supported Microsoft Translator translation code, throw an error. - if ( ! array_key_exists( $locale, $this->locales ) ) { - return new WP_Error( 'gp_machine_translate', sprintf( "The locale %s isn't supported by %s.", $locale, $this->provider ) ); - } - - // 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." ); - } - - include( dirname( __FILE__ ) . '/vendor/autoload.php' ); - - $config = array( - 'clientID' => $this->client_id, - 'clientSecret' => $this->key, - ); - $t = new \MicrosoftTranslator\Translate( $config ); - $translation = $t->translate( $strings, $this->locales[$locale], 'en' ); - - return $translation; - - } - - private function deepl_translate_batch( $locale, $strings ) { - // If we don't have a supported DeepL translation code, throw an error. - if ( ! array_key_exists( $locale, $this->locales ) ) { - return new WP_Error( 'gp_machine_translate', sprintf( "The locale %s isn't supported by %s.", $locale, $this->provider ) ); + private string $selectedProviderIdentifier; + + private Template $template; + + private $version = '1.2'; + + public function __construct() + { + $this->template = new Template(); + } + + // This function displays the admin settings page in WordPress. + public function adminPage() + { + // If the current user can't manage options, display a message and return immediately. + if (!current_user_can('manage_options')) { + _e('You do not have permissions to this page!', 'gp-machine-translate'); + + return; } - // 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." ); + // If the user has saved the settings, commit them to the database. + if (array_key_exists('save_gp_machine_translate', $_POST)) { + // Flush the global key, in case the user is removing the API key. + $authKey = ''; + + // If the API key value is being saved, store it in the global key setting. + if (array_key_exists('gp_machine_translate_key', $_POST)) { + // Make sure to sanitize the data before saving it. + $authKey = sanitize_text_field($_POST['gp_machine_translate_key']); + } + + // Flush the global client id, in case the user is removing the API client id. + $authClientId = ''; + + // If the client ID value is being saved, store it in the global key setting. + if (array_key_exists('gp_machine_translate_client_id', $_POST)) { + // Make sure to sanitize the data before saving it. + $authClientId = sanitize_text_field($_POST['gp_machine_translate_client_id']); + } + + $provider = $_POST['gp_machine_translate_provider']; + + if ($provider != __('*Select*', 'gp-machine-translate') && in_array($provider, $this->providerList, true)) { + update_option('gp_machine_translate_provider', $provider); + $this->selectedProviderIdentifier = $provider; + } + + // Update the option in the database. + update_option('gp_machine_translate_key', $authKey); + + // Update the client ID + update_option('gp_machine_translate_client_id', $authClientId); + + $this->provider = $this->providerManager->updateOrCreateProviderInstance( + $this->selectedProviderIdentifier, + $authClientId, + $authKey, + ); } - // If we have too many strings, throw an error. - if ( count( $strings ) > 50 ) { - return new WP_Error( 'gp_machine_translate', "Only 50 strings allowed." ); + echo $this->template->render( + 'admin-settings', + [ + 'provider' => $this->provider, + 'providerList' => $this->providerManager->getProviderIdentifiers(), + ], + ); + } + + // This function is here as placeholder to support adding the bulk translate option to the router. + // Without this placeholder there is a fatal error generated. + public function after_request() + { + } + + public function batchTranslate($locale, $strings) + { + if (is_object($locale)) { + $locale = $locale->slug; } - $postFields = http_build_query([ - 'auth_key' => $this->key, - 'source_lang' => 'en', - 'target_lang' => urlencode( $this->locales[$locale] ), - 'tag_handling' => 'xml', - ]); + return $this->provider->batchTranslate($locale, $strings); + } + + // This function is here as placeholder to support adding the bulk translate option to the router. + // Without this placeholder there is a fatal error generated. + public function before_request() + { + } - foreach ($strings as $string) { - $postFields .= '&text='.$string; + // This function handles the actual bulk translate as passed in by the router for the projects menu. + public function bulkTranslate($projectPath) + { + // First let's ensure we have decoded the project path for use later. + $projectPath = urldecode($projectPath); + + // Get the URL to the project for use later. + $url = gp_url_project($projectPath); + + // If we don't have rights, just redirect back to the project. + if (!GP::$permission->user_can(wp_get_current_user(), 'write', 'project')) { + wp_redirect($url); + } + + // Create a project class to use to get the project object. + $project_class = new GP_Project(); + + // Get the project object from the project path that was passed in. + $project_obj = $project_class->by_path($projectPath); + + // Get the translations sets from the project ID. + $translation_sets = GP::$translation_set->by_project_id($project_obj->id); + + // Since there might be a lot of translations to process in a batch, let's setup some time limits + // to make sure we don't give a white screen of death to the user. + $time_start = microtime(true); + $max_exec_time = ini_get('max_execution_time') * 0.7; + + // Loop through all the sets. + foreach ($translation_sets as $set) { + // Check to see how our time is doing, if we're over out time limit, stop processing. + if (microtime(true) - $time_start > $max_exec_time) { + gp_notice_set(__('Not all strings translated as we ran out of execution time!', 'gp-machine-translate')); + + break; + } + + // Get the locale we're working with. + $locale = GP_Locales::by_slug($set->locale); + + // If the current translation provider doesn't support this locale, skip it. + if (!array_key_exists($locale->slug, $this->provider->getLocales())) { + continue; + } + // Create a template array to pass in to the worker function at the end of the loop. + $bulk = ['action' => 'gp_machine_translate', 'priority' => 0, 'row-ids' => []]; + + // Create a new GP_Translation object to use. + $translation = new GP_Translation(); + + // Get the strings for the current translation. + $strings = $translation->for_translation($project_obj, $set, 'no-limit', ['status' => 'untranslated']); + + // Add the strings to the $bulk template we setup earlier. + foreach ($strings as $string) { + $bulk['row-ids'][] .= $string->row_id; + } + + // If we don't have any strings to translate, don't bother calling the translation function. + if (count($bulk['row-ids']) > 0) { + // Do the actual bulk translation. + $this->gpTranslationSetBulkActionPost($project_obj, $locale, $set, $bulk); + } + } + + // Redirect back to the project home. + wp_redirect($url); + } + + // Generate the HTML when a user profile is edited. Note the $user parameter is a full user object for this function. + public function editUserProfile($user) + { + // Get the current user client id from the WordPress options table. + $userAuthClientId = get_user_meta($user->ID, 'gp_machine_translate_client_id', true); + + // Get the current user key from the WordPress options table. + $userAuthKey = get_user_meta($user->ID, 'gp_machine_translate_key', true); + + // If the user cannot edit their profile, then don't show the settings. + if (!current_user_can('edit_user', $user->ID)) { + return; } - $response = wp_remote_post('https://api-free.deepl.com/v2/translate', ['body' => $postFields]); + echo $this->template->render( + 'profile-settings', + [ + 'provider' => $this->provider, + 'userAuthClientId' => $userAuthClientId, + 'userAuthKey' => $userAuthKey, + ], + ); + } + + // Once a user profile has been edited, this function saves the settings to the WordPress options table. + public function editUserProfileUpdate($user) + { + // Since the profile and user edit code is identical, just call the profile update code. + $this->personalOptionsUpdate($user); + } + + // This function adds the "Machine Translate" to the individual translation items. + public function gpEntryActions($actions) + { + // Make sure we are currently on a supported locale. + if ($this->languageCodeIsSupportedByProvider) { + $actions[] = '' . __('Machine Translate', 'gp-machine-translate') . ' (' . $this->providersDisplayName[$this->provider::IDENTIFIER] . ')'; + } + + return $actions; + } + + // This function adds the "Machine Translate" option to the projects menu. + public function gpProjectActions($actions, $project) + { + $actions[] .= gp_link_get(gp_url('bulk-translate/' . $project->slug), __('Machine Translate', 'gp-machine-translate') . ' (' . $this->providersDisplayName[$this->provider::IDENTIFIER] . ')'); + + return $actions; + } + + // This function adds the "Machine Translate" to the bulk actions dropdown in the translation set list. + public function gpTranslationSetBulkAction() + { + // Make sure we are currently on a supported locale. + if ($this->languageCodeIsSupportedByProvider) { + echo ''; + } + } + + // This function handles the actual bulk translation as passed in by the translation set list. + public function gpTranslationSetBulkActionPost($project, $locale, $translation_set, $bulk) + { + // If we're not doing a bulk translation, just return. + if ($bulk['action'] != 'gp_machine_translate') { + return; + } + + // Setup some variables to be used during the translation. + $provider_errors = 0; + $insert_errors = 0; + $ok = 0; + $skipped = 0; + + $singulars = []; + $original_ids = []; + + // Loop through each of the passed in strings and translate them. + foreach ($bulk['row-ids'] as $row_id) { + // Split the $row_id by '-' and get the first one (which will be the id of the original). + $original_id = gp_array_get(explode('-', $row_id), 0); + // Get the original based on the above id. + $original = GP::$original->get($original_id); + + // If there is no original or it's a plural, skip it. + if (!$original || $original->plural) { + ++$skipped; + + continue; + } + + // Add the original to the queue to translate. + $singulars[] = $original->singular; + $original_ids[] = $original_id; + } + + // Translate all the originals that we found. + $results = $this->batchTranslate($locale, $singulars); // Did we get an error? - if ( is_wp_error( $response ) ) { - return $response; + if (is_wp_error($results)) { + error_log(print_r($results, true)); + gp_notice_set($results->get_error_message(), 'error'); + + return; + } + + // Merge the results back in to the original id's and singulars, this will create an array like ($items = array( array( id, single, result), array( id, single, result), ... ). + $items = gp_array_zip($original_ids, $singulars, $results); + + // If we have no items, something went wrong and stop processing. + if (!$items) { + return; + } + + // Loop through the items and store them in the database. + foreach ($items as $item) { + // Break up the item back in to individual components. + list($original_id, $singular, $translation) = $item; + + // Did we get an error? + if (is_wp_error($translation)) { + ++$provider_errors; + error_log($translation->get_error_message()); + + continue; + } + + // Build a data array to store + $data = compact('original_id'); + $data['user_id'] = get_current_user_id(); + $data['translation_set_id'] = $translation_set->id; + $data['translation_0'] = $translation; + $data['status'] = 'fuzzy'; + $data['warnings'] = GP::$translation_warnings->check($singular, null, [$translation], $locale); + + // Insert the item in to the database. + $inserted = GP::$translation->create($data); + $inserted ? $ok++ : $insert_errors++; + } + + // Did we get an error? If so let's let the user know about them. + if ($provider_errors > 0 || $insert_errors > 0) { + // Create a message array to use later. + $message = []; + + // Did we have any strings translated successfully? + if ($ok) { + $message[] = sprintf(__('Added: %d.', 'gp-machine-translate'), $ok); + } + + // Did we have any provider errors. + if ($provider_errors) { + $message[] = sprintf(__('Error from %s: %d.', 'gp-machine-translate'), $this->selectedProviderIdentifier, $provider_errors); + } + + // Did we have any errors when we saved everything to the database? + if ($insert_errors) { + $message[] = sprintf(__('Error adding: %d.', 'gp-machine-translate'), $insert_errors); + } + + // Did we skip any items? + if ($skipped) { + $message[] = sprintf(__('Skipped: %d.', 'gp-machine-translate'), $skipped); + } + + // Create a message string and add it to the GlotPress notices. + gp_notice_set(implode('', $message), 'error'); + } else { + // If we didn't get any errors, then we just need to let the user know how many translations were added. + gp_notice_set(sprintf(__('%d fuzzy translation from Machine Translate were added.', 'gp-machine-translate'), $ok)); + } + } + + // Once a profile has been updated, this function saves the settings to the WordPress options table. + public function personalOptionsUpdate($user_id) + { + // If the user cannot edit their profile, then don't save the settings + if (!current_user_can('edit_user', $user_id)) { + return; + } + + // Unlike the profile edit function, we only get the user id passed in as a parameter. + update_user_meta($user_id, 'gp_machine_translate_key', sanitize_text_field($_POST['gp_machine_translate_user_key'])); + update_user_meta($user_id, 'gp_machine_translate_client_id', sanitize_text_field($_POST['gp_machine_translate_user_client_id'])); + } + + public function register() + { + // Load the plugin's translated strings. + load_plugin_textdomain('gp-machine-translate'); + + // Handle the WordPress user profile items + add_action('show_user_profile', [$this, 'showUserProfile'], 10, 1); + add_action('edit_user_profile', [$this, 'editUserProfile'], 10, 1); + add_action('personal_options_update', [$this, 'personalOptionsUpdate'], 10, 1); + add_action('edit_user_profile_update', [$this, 'editUserProfileUpdate'], 10, 1); + + // Add the admin page to the WordPress settings menu. + add_action('admin_menu', [$this, 'registerAdminMenu'], 10, 1); + + if (get_option('gp_machine_translate_version', '0.7') != $this->version) { + $this->upgrade(); + } + + $authClientId = get_option('gp_machine_translate_client_id', null); + $authKey = get_option('gp_machine_translate_key', null); + + // Check to see if there is a user currently logged in. + if (is_user_logged_in()) { + // If someone is logged in, get their user object. + $user = wp_get_current_user(); + + // Load the user translate key from the WordPress user meta table, using the currently logged in user id. + $userAuthClientId = get_user_meta($user->ID, 'gp_machine_translate_client_id', true); + $userAuthKey = get_user_meta($user->ID, 'gp_machine_translate_key', true); + + // If there is a user key, override the global key. + if ($userAuthKey) { + $authClientId = $userAuthClientId; + $authKey = $userAuthKey; + } } - // Decode the response from DeepL. - $json = json_decode( wp_remote_retrieve_body( $response ) ); + $this->providerManager = new ProviderManager($authClientId, $authKey); + $this->selectedProviderIdentifier = get_option('gp_machine_translate_provider', 'DeepL'); + $this->providerList = $this->providerManager->getProviderIdentifiers(); + $this->providersDisplayName = $this->providerManager->getProvidersDisplayName(); + $this->provider = $this->providerManager->getOrCreateProviderInstance($this->selectedProviderIdentifier); - // If something went wrong with the response from DeepL, throw an error. - if ( ! $json ) { - return new WP_Error( 'gp_machine_translate', 'Error decoding JSON from DeepL Translate.' ); + // If the provider is not set up, the system will not proceed. + if (!$this->provider->isSetUp()) { + return; } - if ( isset( $json->error ) ) { - return new WP_Error( 'gp_machine_translate', sprintf( 'Error auto-translating: %1$s', $json->error->errors[0]->message ) ); + wp_register_script('gp-machine-translate-js', plugins_url('gp-machine-translate.js', __FILE__), ['jquery', 'editor', 'gp-common']); + + // If the user has write permissions to the projects, add the bulk translate option to the projects menu. + if (GP::$permission->user_can(wp_get_current_user(), 'write', 'project')) { + add_action('gp_project_actions', [$this, 'gpProjectActions'], 10, 2); } - // Setup an temporary array to use to process the response. - $translations = array(); - $translatedStrings = array_column($json->translations, 'text'); + // Add the actions to handle adding the translate menu to the various parts of GlotPress. + add_action('gp_pre_tmpl_load', [$this, 'registerJavaScript'], 10, 2); + add_filter('gp_entry_actions', [$this, 'gpEntryActions'], 10, 1); + add_action('gp_translation_set_bulk_action', [$this, 'gpTranslationSetBulkAction'], 10, 1); + add_action('gp_translation_set_bulk_action_post', [$this, 'gpTranslationSetBulkActionPost'], 10, 4); + + // We can't use the filter in the defaults route code because plugins don't load until after + // it has already run, so instead add the routes directly to the global GP_Router object. + GP::$router->add('/bulk-translate/(.+?)', [$this, 'bulkTranslate'], 'get'); + GP::$router->add('/bulk-translate/(.+?)', [$this, 'bulkTranslate'], 'post'); + } - // Merge the originals and translations arrays. - $items = gp_array_zip( $strings, $translatedStrings ); + // This function adds the admin settings page to WordPress. + public function registerAdminMenu() + { + add_options_page( + __('GP Machine Translate', 'gp-machine-translate'), + __('GP Machine Translate', 'gp-machine-translate'), + 'manage_options', + basename(__FILE__), + [$this, 'adminPage'], + ); + } - // If there are no items, throw an error. - if ( ! $items ) { - return new WP_Error( 'gp_machine_translate', 'Error merging arrays' ); + // This function loads the javascript when required. + public function registerJavaScript($template, $args) + { + // If we don't have a translation key, just return without doing anything. + if (!isset($this->provider) || $this->provider->isSetUp() === false) { + return; } - // Loop through the items and clean up the responses. - foreach ( $items as $item ) { - list( $string, $translation ) = $item; + // If we're not on the translation template, just return without doing anything. + if ($template != 'translations') { + return; + } - $translations[] = $this->google_translate_fix( $translation ); + // If the current locale isn't supported by the translation provider, just return without doing anything. + if (!array_key_exists($args['locale']->slug, $this->provider->getLocales())) { + return; } - // Return the results. - return $translations; + // Create options for the localization script. + $options = [ + 'locale' => $this->provider->getLocales()[$args['locale']->slug], + 'ajaxurl' => admin_url('admin-ajax.php'), + ]; + + // Set the current Google code to the locale we're dealing with. + $this->languageCodeIsSupportedByProvider = isset($this->provider->getLocales()[$args['locale']->slug]); + + // Enqueue the translation JavaScript and translate it. + gp_enqueue_script('gp-machine-translate-js'); + wp_localize_script('gp-machine-translate-js', 'gp_machine_translate', $options); } - private function yandex_translate_batch( $locale, $strings ) { - // If we don't have a supported Yandex translation code, throw an error. - if ( ! array_key_exists( $locale, $this->locales ) ) { - return new WP_Error( 'gp_machine_translate', sprintf( "The locale %s isn't supported by %s.", $locale, $this->provider ) ); - } - - // 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." ); - } - - // This is the URL of the Yandex API. - $url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' . $this->key . '&lang=en-' . urlencode( $this->locales[$locale] ); + // Generate the HTML when a user views their profile. + public function showUserProfile($user) + { + // Show and edit are virtually identical, so just call the edit function. + $this->editUserProfile($user); + } - // Loop through the stings and add them to the $url as a query string. - foreach ( $strings as $string ) { - $url .= '&text=' . urlencode( $string ); - } - - // Get the response from Yandex. - $response = wp_remote_get( $url ); + private function upgrade() + { + global $wpdb; + + // If the old google key exists, update it to the new option name and remove it. + // On the next upgrade this code will not run. + // To be removed in a future version once we're well past version 0.7. + if (get_option('gp_google_translate_key', false) !== false) { + // Rename the global translation key name. + update_option('gp_machine_translate_key', get_option('gp_google_translate_key', false)); + delete_option('gp_google_translate_key'); + } - // Did we get an error? - if ( is_wp_error( $response ) ) { - return $response; - } - - // Decode the response from Yandex. - $json = json_decode( wp_remote_retrieve_body( $response ) ); - - // If something went wrong with the response from Yandex, throw an error. - if ( ! $json ) { - return new WP_Error( 'gp_machine_translate', 'Error decoding JSON from Yandex 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 = array(); - - // If the translations have been return as a single entry, make it an array so it's easier to process later. - if ( ! is_array( $json->text ) ) { - $json->text = array( $json->text ); - } - - // Merge the originals and translations arrays. - $items = gp_array_zip( $strings, $json->text ); - - // 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->google_translate_fix( $translation ); - } - - // Return the results. - return $translations; - } - - // This function contacts Google and translate a set of strings. - private function google_translate_batch( $locale, $strings ) { - // If we don't have a supported Google translation code, throw an error. - if ( ! array_key_exists( $locale, $this->locales ) ) { - return new WP_Error( 'gp_machine_translate', sprintf( "The locale %s isn't supported by %s.", $locale, $this->provider ) ); - } - - // 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." ); - } - - // This is the URL of the Google API. - $url = 'https://www.googleapis.com/language/translate/v2?key=' . $this->key . '&source=en&target=' . urlencode( $this->locales[$locale] ); - - // Loop through the stings and add them to the $url as a query string. - foreach ( $strings as $string ) { - $url .= '&q=' . urlencode( $string ); - } - - // If we just have a single string, add an extra q= to the end so Google things we're doing multiple strings. - if ( count( $strings ) == 1 ) { - $url .= '&q='; - } - - // Get the response from Google. - $response = wp_remote_get( $url ); - - // Did we get an error? - if ( is_wp_error( $response ) ) { - return $response; - } - - // Decode the response from Google. - $json = json_decode( wp_remote_retrieve_body( $response ) ); - - // If something went wrong with the response from Google, throw an error. - if ( ! $json ) { - return new WP_Error( 'gp_machine_translate', 'Error decoding JSON from Google 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 = array(); - - // If the translations have been return as a single entry, make it an array so it's easier to process later. - if ( ! is_array( $json->data->translations ) ) { - $json->data->translations = array( $json->data->translations ); - } - - // Merge the originals and translations arrays. - $items = gp_array_zip( $strings, $json->data->translations ); - - // 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->google_translate_fix( $translation->translatedText ); - } - - // Return the results. - return $translations; - } - - // This function cleans up the results from Google. - private function google_translate_fix( $string ) { - $string = preg_replace_callback( '/% (s|d)/i', function ($m) { return '"%".strtolower($m[1])'; }, $string ); - $string = preg_replace_callback( '/% (\d+) \$ (s|d)/i', function ($m) { return '"%".$m[1]."\\$".strtolower($m[2])'; }, $string ); - - return $string; - } - - // This function adds the admin settings page to WordPress. - public function admin_menu() { - add_options_page( __( 'GP Machine Translate', 'gp-machine-translate' ), __( 'GP Machine Translate', 'gp-machine-translate' ), 'manage_options', basename( __FILE__ ), array( $this, 'admin_page' ) ); - } - - // This function displays the admin settings page in WordPress. - public function admin_page() { - // If the current user can't manage options, display a message and return immediately. - if( ! current_user_can( 'manage_options' ) ) { _e( 'You do not have permissions to this page!', 'gp-machine-translate' ); return; } - - // If the user has saved the settings, commit them to the database. - if( array_key_exists( 'save_gp_machine_transalate', $_POST ) ) { - // Flush the global key, in case the user is removing the API key. - $this->key = ''; - - // If the API key value is being saved, store it in the global key setting. - if( array_key_exists( 'gp_machine_translate_key', $_POST ) ) { - // Make sure to sanitize the data before saving it. - $this->key = sanitize_text_field( $_POST['gp_machine_translate_key'] ); - } - - // If the client ID value is being saved, store it in the global key setting. - if( array_key_exists( 'gp_machine_translate_client_id', $_POST ) ) { - // Make sure to sanitize the data before saving it. - $this->client_id = sanitize_text_field( $_POST['gp_machine_translate_client_id'] ); - } - - $provider = $_POST['gp_machine_translate_provider']; - - if( $provider != __( '*Select*', 'gp-machine-translate' ) && in_array( $provider, $this->providers ) ) { - update_option( 'gp_machine_translate_provider', $provider ); - $this->provider = $provider; - } - - // Update the option in the database. - update_option( 'gp_machine_translate_key', $this->key ); - - // Update the client ID> - update_option( 'gp_machine_translate_client_id', $this->client_id ); - } - - ?> -
-

- -
- - - - - - - - - - - - - -
- -

-
- -

-
- -

-
- - - -
- -
-query( "UPDATE {$wpdb->usermeta} SET `meta_key`='gp_machine_translate_key' WHERE `meta_key`='gp_google_translate_key';" ); - - // Update the version option to the current version so we don't run the upgrade process again. - update_option( 'gp_machine_translate_version', $this->version ); - - // If trasltr is enabled, disable it as it is no longer supported. - if( get_option( 'gp_machine_translate_provider' ) == 'transltr.org' ) { - update_option( 'gp_machine_translate_provider', "" ); - } - } + // Rename the per use translation key name. We can't do this in the "if" above as the global key + // may be set to blank but user keys may still exist, so we have to do this on each upgrade. + // To be removed in a future version once we're well past version 0.7. + $wpdb->query("UPDATE {$wpdb->usermeta} SET `meta_key`='gp_machine_translate_key' WHERE `meta_key`='gp_google_translate_key';"); + + // Update the version option to the current version so we don't run the upgrade process again. + update_option('gp_machine_translate_version', $this->version); + + // If trasltr is enabled, disable it as it is no longer supported. + if (get_option('gp_machine_translate_provider') == 'transltr.org') { + update_option('gp_machine_translate_provider', ''); + } + } } -// Add an action to WordPress's init hook to setup the plugin. Don't just setup the plugin here as the GlotPress plugin may not have loaded yet. -add_action( 'gp_init', 'gp_machine_translate_init' ); +// Add an action to WordPress's init hook to setup the plugin. Don't just setup the plugin here as the GlotPress plugin may not have loaded yet. +add_action('gp_init', 'gp_machine_translate_init'); + +require_once rtrim(plugin_dir_path(__FILE__), '/') . '/vendor/autoload.php'; -include_once( 'ajax.php' ); +require_once rtrim(plugin_dir_path(__FILE__), '/') . '/ajax.php'; // This function creates the plugin. -function gp_machine_translate_init() { - GLOBAL $gp_machine_translate; +function gp_machine_translate_init() +{ + global $gp_machine_translate; - $gp_machine_translate = new GP_Machine_Translate; -} \ No newline at end of file + $gp_machine_translate = new GP_Machine_Translate(); + $gp_machine_translate->register(); +} diff --git a/microsoft.locales.php b/microsoft.locales.php deleted file mode 100644 index dd3b756..0000000 --- a/microsoft.locales.php +++ /dev/null @@ -1,53 +0,0 @@ - 'af', - 'ar' => 'ar', - 'bg' => 'bg', - 'bs' => 'bs-Latn', - 'ca' => 'ca', - 'cs' => 'cs', - 'cy' => 'cy', - 'da' => 'da', - 'de' => 'de', - 'el' => 'el', - 'es' => 'es', - 'et' => 'et', - 'fa' => 'fa', - 'fi' => 'fi', - 'fr' => 'fr', - 'he' => 'iw', - 'hi' => 'hi', - 'hr' => 'hr', - 'ht' => 'ht', - 'hu' => 'hu', - 'id' => 'id', - 'it' => 'it', - 'ja' => 'ja', - 'ko' => 'ko', - 'lt' => 'lt', - 'lv' => 'lv', - 'ms' => 'ms', - 'mt' => 'mt', - 'mww' => 'mww', - 'nl' => 'nl', - 'no' => 'no', - 'otq' => 'otq', - 'pl' => 'pl', - 'pt' => 'pt-PT', - 'ro' => 'ro', - 'ru' => 'ru', - 'sk' => 'sk', - 'sl' => 'sl', - 'sr' => 'sr-Cyrl', - 'sv' => 'sv', - 'sw' => 'sw', - 'th' => 'th', - 'tlh' => 'tlh', - 'tr' => 'tr', - 'uk' => 'uk', - 'ur' => 'ur', - 'vi' => 'vi', - 'yua' => 'yua', - 'zh_cn' => 'zh-CHS', -); \ No newline at end of file diff --git a/readme.md b/readme.md index 7d259eb..9d7cd20 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,7 @@ **Stable tag:** 1.2 **License:** GPLv2 **License URI:** http://www.gnu.org/licenses/gpl-2.0.html +**Requires PHP:** 7.4 A machine translate plugin for GlotPress as a WordPress plugin. diff --git a/readme.txt b/readme.txt index 4ec8650..49afb47 100644 --- a/readme.txt +++ b/readme.txt @@ -9,6 +9,7 @@ Tested up to: 6.4 Stable tag: 1.2 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html +Requires PHP: 7.4 A machine translate plugin for GlotPress as a WordPress plugin. diff --git a/release/generate.supported.languages.php b/release/generate.supported.languages.php index a0bf428..322a8d3 100644 --- a/release/generate.supported.languages.php +++ b/release/generate.supported.languages.php @@ -1,90 +1,92 @@ 'DeepL', 'Google Translate' => 'Google Translate', 'Microsoft Translator' => 'Microsoft Translator', 'Yandex.Translate' => 'Powered by Yandex.Translate' ); - $includes = array( 'DeepL' => 'deepl.locales.php', 'Yandex.Translate' => 'yandex.locales.php', 'Microsoft Translator' => 'microsoft.locales.php', 'Google Translate' => 'google.locales.php' ); - - // This should be the path to a GlotPress locales file. - include( '../../../glotpress/GlotPress/locales/locales.php' ); - - $gp_locales = new GP_Locales; - $locales = $gp_locales->locales; - $storage = array(); - $length = 0; - $width = 0; - - foreach( $providers as $provider ) { - if( mb_strlen( $provider ) > $width ) { $width = mb_strlen( $provider ); } - - $gp_machine_translate_locales = array(); - - include( '../' . $includes[$provider] ); - - foreach( $gp_machine_translate_locales as $mt_locale => $p_locale ) { - if( array_key_exists( $mt_locale, $locales ) ) { - if( mb_strlen( $locales[$mt_locale]->english_name ) > $width ) { $width = mb_strlen( $locales[$mt_locale]->english_name ); } - - $storage[$provider][] = $locales[$mt_locale]->english_name; - - if( count( $storage[$provider] ) > $length ) { $length = count( $storage[$provider] ); } - } - } - } - - $output = '| '; - $markdown = '| '; - $wp_md = "\t"; - - foreach( $providers as $provider ) { - sort( $storage[$provider] ); - - $padding = str_repeat( ' ', $width - mb_strlen( $provider ) ); - - $output .= $provider . $padding . ' '; - $markdown .= $provider . $padding . ' | '; - $wp_md .= $provider . $padding . ' '; - } - - $output = rtrim( $output ) . PHP_EOL; - $markdown = rtrim( $markdown ) . PHP_EOL; - $wp_md = rtrim( $wp_md ) . PHP_EOL; - - $output .= '|'; - $markdown .= '|'; - $wp_md .= "\t"; - - foreach( $providers as $provider ) { - $padding = str_repeat( '-', $width ); - - $output .= $padding . ' '; - $markdown .= $padding . '--|'; - $wp_md .= $padding . ' '; - } - - $output = rtrim( $output ) . PHP_EOL; - $markdown = rtrim( $markdown ) . PHP_EOL; - $wp_md = rtrim( $wp_md ) . PHP_EOL; - - for( $i = 0; $i < $length; $i++ ) { - $line = ''; - $mline = '| '; - - foreach( $providers as $provider ) { - if( array_key_exists( $i, $storage[$provider] ) ) { - $locale = $storage[$provider][$i]; - $line .= $locale . str_repeat( ' ', $width - mb_strlen( $locale ) ) . ' '; - $mline .= $locale . str_repeat( ' ', $width - mb_strlen( $locale ) ) . ' | '; - } else { - $line .= str_repeat( ' ', $width ) . ' '; - $mline .= str_repeat( ' ', $width ) . ' | '; - } - } - - $output .= rtrim( $line ) . PHP_EOL; - $markdown .= rtrim( $mline ) . PHP_EOL; - $wp_md .= "\t" . rtrim( $line ) . PHP_EOL; - } - - file_put_contents( 'provider-chart.ascii.txt', $output ); - file_put_contents( 'provider-chart.markdown.txt', $markdown ); - file_put_contents( 'provider-chart.wordpress.txt', $wp_md ); +use GpMachineTranslate\Providers\ProviderManager; + +require_once '../vendor/autoload.php'; + +$providerManager = new ProviderManager(null, null); +$providers = $providerManager->getProviderIdentifiers(); +$banners = $providerManager->getProvidersDisplayName(); + +// This should be the path to a GlotPress locales file. +include( '../../../glotpress/GlotPress/locales/locales.php' ); + +$gp_locales = new GP_Locales; +$locales = $gp_locales->locales; +$storage = array(); +$length = 0; +$width = 0; + +foreach( $providers as $provider ) { + if( mb_strlen( $provider ) > $width ) { $width = mb_strlen( $provider ); } + + $gp_machine_translate_locales = $providerManager->getOrCreateProviderInstance($provider)->getLocales(); + + foreach( $gp_machine_translate_locales as $mt_locale => $p_locale ) { + if( array_key_exists( $mt_locale, $locales ) ) { + if( mb_strlen( $locales[$mt_locale]->english_name ) > $width ) { $width = mb_strlen( $locales[$mt_locale]->english_name ); } + + $storage[$provider][] = $locales[$mt_locale]->english_name; + + if( count( $storage[$provider] ) > $length ) { $length = count( $storage[$provider] ); } + } + } +} + +$output = '| '; +$markdown = '| '; +$wp_md = "\t"; + +foreach( $providers as $provider ) { + sort( $storage[$provider] ); + + $padding = str_repeat( ' ', $width - mb_strlen( $provider ) ); + + $output .= $provider . $padding . ' '; + $markdown .= $provider . $padding . ' | '; + $wp_md .= $provider . $padding . ' '; +} + +$output = rtrim( $output ) . PHP_EOL; +$markdown = rtrim( $markdown ) . PHP_EOL; +$wp_md = rtrim( $wp_md ) . PHP_EOL; + +$output .= '|'; +$markdown .= '|'; +$wp_md .= "\t"; + +foreach( $providers as $provider ) { + $padding = str_repeat( '-', $width ); + + $output .= $padding . ' '; + $markdown .= $padding . '--|'; + $wp_md .= $padding . ' '; +} + +$output = rtrim( $output ) . PHP_EOL; +$markdown = rtrim( $markdown ) . PHP_EOL; +$wp_md = rtrim( $wp_md ) . PHP_EOL; + +for( $i = 0; $i < $length; $i++ ) { + $line = ''; + $mline = '| '; + + foreach( $providers as $provider ) { + if( array_key_exists( $i, $storage[$provider] ) ) { + $locale = $storage[$provider][$i]; + $line .= $locale . str_repeat( ' ', $width - mb_strlen( $locale ) ) . ' '; + $mline .= $locale . str_repeat( ' ', $width - mb_strlen( $locale ) ) . ' | '; + } else { + $line .= str_repeat( ' ', $width ) . ' '; + $mline .= str_repeat( ' ', $width ) . ' | '; + } + } + + $output .= rtrim( $line ) . PHP_EOL; + $markdown .= rtrim( $mline ) . PHP_EOL; + $wp_md .= "\t" . rtrim( $line ) . PHP_EOL; +} + +file_put_contents( 'provider-chart.ascii.txt', $output ); +file_put_contents( 'provider-chart.markdown.txt', $markdown ); +file_put_contents( 'provider-chart.wordpress.txt', $wp_md ); \ No newline at end of file diff --git a/templates/admin-settings.php b/templates/admin-settings.php new file mode 100644 index 0000000..8f6a2a0 --- /dev/null +++ b/templates/admin-settings.php @@ -0,0 +1,58 @@ + +
+

+
+ + + + + + + + + + + + + +
+ +

+
+ +

+
+ +

+
+ +
+
\ No newline at end of file diff --git a/templates/profile-settings.php b/templates/profile-settings.php new file mode 100644 index 0000000..dd52dad --- /dev/null +++ b/templates/profile-settings.php @@ -0,0 +1,32 @@ + +

+ + + + + + + + +
+ +

+
+ +

+
\ No newline at end of file diff --git a/vendor/autoload.php b/vendor/autoload.php index d3b305e..afd7848 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -2,6 +2,6 @@ // autoload.php @generated by Composer -require_once __DIR__ . '/composer' . '/autoload_real.php'; +require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit67fbca1eb8a3013cbbce9e0402cb3140::getLoader(); diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php index ff6ecfb..247294d 100644 --- a/vendor/composer/ClassLoader.php +++ b/vendor/composer/ClassLoader.php @@ -37,11 +37,13 @@ * * @author Fabien Potencier * @author Jordi Boggiano - * @see http://www.php-fig.org/psr/psr-0/ - * @see http://www.php-fig.org/psr/psr-4/ + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { + private $vendorDir; + // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); @@ -53,13 +55,21 @@ class ClassLoader private $useIncludePath = false; private $classMap = array(); - private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + private static $registeredLoaders = array(); + + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } public function getPrefixes() { if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', $this->prefixesPsr0); + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); @@ -271,6 +281,26 @@ public function isClassMapAuthoritative() return $this->classMapAuthoritative; } + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + /** * Registers this instance as an autoloader. * @@ -279,6 +309,17 @@ public function isClassMapAuthoritative() public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } } /** @@ -287,6 +328,10 @@ public function register($prepend = false) public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } } /** @@ -313,34 +358,49 @@ public function loadClass($class) */ public function findFile($class) { - // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 - if ('\\' == $class[0]) { - $class = substr($class, 1); - } - // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } - if ($this->classMapAuthoritative) { + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM - if ($file === null && defined('HHVM_VERSION')) { + if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } - if ($file === null) { + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { // Remember that this class does not exist. - return $this->classMap[$class] = false; + $this->missingClasses[$class] = true; } return $file; } + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + private function findFileWithExtension($class, $ext) { // PSR-4 lookup @@ -348,10 +408,14 @@ private function findFileWithExtension($class, $ext) $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { - foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { - if (0 === strpos($class, $prefix)) { - foreach ($this->prefixDirsPsr4[$prefix] as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { return $file; } } @@ -399,6 +463,8 @@ private function findFileWithExtension($class, $ext) if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } + + return false; } } diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE index 1a28124..f27399a 100644 --- a/vendor/composer/LICENSE +++ b/vendor/composer/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2016 Nils Adermann, Jordi Boggiano +Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 7a91153..b26f1b1 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -6,4 +6,5 @@ $baseDir = dirname($vendorDir); return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index b265c64..d37e2bf 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -6,4 +6,5 @@ $baseDir = dirname($vendorDir); return array( + 'GpMachineTranslate\\' => array($baseDir . '/classes'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index ed883a2..aa7668e 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -13,19 +13,24 @@ public static function loadClassLoader($class) } } + /** + * @return \Composer\Autoload\ClassLoader + */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInit67fbca1eb8a3013cbbce9e0402cb3140', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); spl_autoload_unregister(array('ComposerAutoloaderInit67fbca1eb8a3013cbbce9e0402cb3140', 'loadClassLoader')); - $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION'); + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140::getInitializer($loader)); } else { diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 10d01dd..caee8e2 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -6,6 +6,20 @@ class ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140 { + public static $prefixLengthsPsr4 = array ( + 'G' => + array ( + 'GpMachineTranslate\\' => 19, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'GpMachineTranslate\\' => + array ( + 0 => __DIR__ . '/../..' . '/classes', + ), + ); + public static $prefixesPsr0 = array ( 'M' => array ( @@ -16,10 +30,17 @@ class ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140 ), ); + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140::$prefixesPsr0; + $loader->classMap = ComposerStaticInit67fbca1eb8a3013cbbce9e0402cb3140::$classMap; }, null, ClassLoader::class); } diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..fabf8f6 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 50000)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 5.0.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/yandex.locales.php b/yandex.locales.php deleted file mode 100644 index 124d272..0000000 --- a/yandex.locales.php +++ /dev/null @@ -1,28 +0,0 @@ - 'be', - 'ca' => 'ca', - 'cs' => 'cs', - 'da' => 'da', - 'de' => 'de', - 'el' => 'el', - 'es' => 'es', - 'et' => 'et', - 'fi' => 'fi', - 'fr' => 'fr', - 'hu' => 'hu', - 'it' => 'it', - 'lt' => 'lt', - 'lv' => 'lv', - 'mk' => 'mk', - 'nl' => 'nl', - 'no' => 'no', - 'pt' => 'pt', - 'ru' => 'ru', - 'sk' => 'sk', - 'sl' => 'sl', - 'sv' => 'sv', - 'tr' => 'tr', - 'uk' => 'uk', -); \ No newline at end of file