Making WordPress.org


Ignore:
Timestamp:
03/29/2023 02:12:17 PM (14 months ago)
Author:
amieiro
Message:

Translate: Add suggestions from OpenAI and DeepL

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-suggestions/inc/routes/class-translation-memory.php

    r8749 r12510  
    44
    55use GP;
     6use GP_Locales;
    67use GP_Route;
    78use WordPressdotorg\GlotPress\TranslationSuggestions\Translation_Memory_Client;
     
    1112
    1213    public function get_suggestions( $project_path, $locale_slug, $set_slug ) {
    13         $original_id = gp_get( 'original' );
    14         $nonce       = gp_get( 'nonce' );
     14        $original_id                           = gp_get( 'original' );
     15        $translation_id                        = gp_get( 'translation', 0 );
     16        $nonce                                 = gp_get( 'nonce' );
     17        $gp_default_sort                       = get_user_option( 'gp_default_sort' );
     18        $external_services_exclude_some_status = gp_array_get( $gp_default_sort, 'external_services_exclude_some_status', 0 );
     19        $translation                           = null;
     20        $openai_suggestions                    = array();
     21        $deepl_suggestions                     = array();
    1522
    1623        if ( ! wp_verify_nonce( $nonce, 'translation-memory-suggestions-' . $original_id ) ) {
     
    3239        }
    3340
    34         $suggestions = Translation_Memory_Client::query( $original->singular, $locale );
     41        $suggestions                     = Translation_Memory_Client::query( $original->singular, $locale );
     42        $current_set_slug                = 'default';
     43        $locale_glossary_translation_set = GP::$translation_set->by_project_id_slug_and_locale( 0, $current_set_slug, $locale_slug );
     44        $locale_glossary                 = GP::$glossary->by_set_id( $locale_glossary_translation_set->id );
     45
     46        if ( $external_services_exclude_some_status ) {
     47            if ( $translation_id > 0 ) {
     48                $translation = GP::$translation->get( $translation_id );
     49            }
     50            if ( ! $translation || ( 'current' != $translation->status && 'rejected' != $translation->status && 'old' != $translation->status ) ) {
     51                $openai_suggestions = $this->get_openai_suggestion( $original->singular, $locale_slug, $locale_glossary );
     52                $deepl_suggestions  = $this->get_deepl_suggestion( $original->singular, $locale_slug, $set_slug );
     53            }
     54        } else {
     55            $openai_suggestions = $this->get_openai_suggestion( $original->singular, $locale_slug, $locale_glossary );
     56            $deepl_suggestions  = $this->get_deepl_suggestion( $original->singular, $locale_slug, $set_slug );
     57        }
    3558
    3659        if ( is_wp_error( $suggestions ) ) {
     
    3861        }
    3962
    40         wp_send_json_success( gp_tmpl_get_output( 'translation-memory-suggestions', [ 'suggestions' => $suggestions ], PLUGIN_DIR . '/templates/' ) );
     63        wp_send_json_success( gp_tmpl_get_output( 'translation-memory-suggestions', compact( 'suggestions', 'openai_suggestions', 'deepl_suggestions' ), PLUGIN_DIR . '/templates/' ) );
     64    }
     65
     66    /**
     67     * Get suggestions from OpenAI (ChatGPT).
     68     *
     69     * @param string       $original_singular The singular from the original string.
     70     * @param string       $locale            The locale.
     71     * @param \GP_Glossary $locale_glossary   The glossary for the locale.
     72     *
     73     * @return array
     74     */
     75    private function get_openai_suggestion( $original_singular, $locale, $locale_glossary ): array {
     76        $openai_query    = '';
     77        $glossary_query  = '';
     78        $gp_default_sort = get_user_option( 'gp_default_sort' );
     79        $openai_key      = gp_array_get( $gp_default_sort, 'openai_api_key' );
     80        if ( empty( trim( $openai_key ) ) ) {
     81            return array();
     82        }
     83        $openai_prompt      = gp_array_get( $gp_default_sort, 'openai_custom_prompt' );
     84        $openai_temperature = gp_array_get( $gp_default_sort, 'openai_temperature', 0 );
     85        if ( ! is_float( $openai_temperature ) || $openai_temperature < 0 || $openai_temperature > 2 ) {
     86            $openai_temperature = 0;
     87        }
     88
     89        $glossary_entries = array();
     90        foreach ( $locale_glossary->get_entries() as $gp_glossary_entry ) {
     91            if ( strpos( strtolower( $original_singular ), strtolower( $gp_glossary_entry->term ) ) !== false ) {
     92                // Use the translation as key, because we could have multiple translations with the same term.
     93                $glossary_entries[ $gp_glossary_entry->translation ] = $gp_glossary_entry->term;
     94            }
     95        }
     96        if ( ! empty( $glossary_entries ) ) {
     97            $glossary_query = ' The following terms are translated as follows: ';
     98            foreach ( $glossary_entries as $translation => $term ) {
     99                $glossary_query .= '"' . $term . '" is translated as "' . $translation . '"';
     100                if ( array_key_last( $glossary_entries ) != $translation ) {
     101                    $glossary_query .= ', ';
     102                }
     103            }
     104            $glossary_query .= '.';
     105        }
     106
     107        $gp_locale     = GP_Locales::by_field( 'slug', $locale );
     108        $openai_query .= ' Translate the following text to ' . $gp_locale->english_name . ": \n";
     109        $openai_query .= '"' . $original_singular . '"';
     110
     111        $messages = array(
     112            array(
     113                'role'    => 'system',
     114                'content' => $openai_prompt . $glossary_query,
     115            ),
     116            array(
     117                'role'    => 'user',
     118                'content' => $openai_query,
     119            ),
     120        );
     121
     122        $openai_response = wp_remote_post(
     123            'https://api.openai.com/v1/chat/completions',
     124            array(
     125                'timeout' => 20,
     126                'headers' => array(
     127                    'Content-Type'  => 'application/json',
     128                    'Authorization' => 'Bearer ' . $openai_key,
     129                ),
     130                'body'    => wp_json_encode(
     131                    array(
     132                        'model'       => 'gpt-3.5-turbo',
     133                        'max_tokens'  => 1000,
     134                        'n'           => 1,
     135                        'messages'    => $messages,
     136                        'temperature' => $openai_temperature,
     137                    )
     138                ),
     139            )
     140        );
     141        if ( is_wp_error( $openai_response ) ) {
     142            return array();
     143        }
     144        $response_status = wp_remote_retrieve_response_code( $openai_response );
     145        if ( 200 !== $response_status ) {
     146            return array();
     147        }
     148        $output = json_decode( wp_remote_retrieve_body( $openai_response ), true );
     149        $this->update_openai_tokens_used( $output['usage']['total_tokens'] );
     150
     151        $message                           = $output['choices'][0]['message'];
     152        $response['openai']['translation'] = trim( trim( $message['content'] ), '"' );
     153        $response['openai']['diff']        = '';
     154
     155        return $response;
     156    }
     157
     158    /**
     159     * Updates the number of tokens used by OpenAI.
     160     *
     161     * @param int $tokens_used The number of tokens used.
     162     */
     163    private function update_openai_tokens_used( int $tokens_used ) {
     164        $gp_external_translations = get_user_option( 'gp_external_translations' );
     165        $openai_tokens_used       = gp_array_get( $gp_external_translations, 'openai_tokens_used' );
     166        if ( ! is_int( $openai_tokens_used ) || $openai_tokens_used < 0 ) {
     167            $openai_tokens_used = 0;
     168        }
     169        $openai_tokens_used                            += $tokens_used;
     170        $gp_external_translations['openai_tokens_used'] = $openai_tokens_used;
     171        update_user_option( get_current_user_id(), 'gp_external_translations', $gp_external_translations );
     172    }
     173
     174    /**
     175     * Gets a translation suggestion from DeepL.
     176     *
     177     * @param string $original_singular The singular from the original string.
     178     * @param string $locale            The locale.
     179     * @param string $set_slug          The set slug.
     180     *
     181     * @return array
     182     */
     183    private function get_deepl_suggestion( string $original_singular, string $locale, string $set_slug ): array {
     184        $free_url        = 'https://api-free.deepl.com/v2/translate';
     185        $gp_default_sort = get_user_option( 'gp_default_sort' );
     186        $deepl_api_key   = gp_array_get( $gp_default_sort, 'deepl_api_key' );
     187        if ( empty( trim( $deepl_api_key ) ) ) {
     188            return array();
     189        }
     190        $target_lang = $this->get_deepl_locale( $locale );
     191        if ( empty( $target_lang ) ) {
     192            return array();
     193        }
     194        $deepl_response = wp_remote_post(
     195            $free_url,
     196            array(
     197                'timeout' => 20,
     198                'body'    => array(
     199                    'auth_key'    => $deepl_api_key,
     200                    'text'        => $original_singular,
     201                    'source_lang' => 'EN',
     202                    'target_lang' => $target_lang,
     203                    'formality'   => $this->get_language_formality( $target_lang, $set_slug ),
     204                ),
     205            ),
     206        );
     207        if ( is_wp_error( $deepl_response ) ) {
     208            return array();
     209        } else {
     210            $body                             = wp_remote_retrieve_body( $deepl_response );
     211            $response['deepl']['translation'] = json_decode( $body )->translations[0]->text;
     212            $response['deepl']['diff']        = '';
     213            $this->update_deepl_chars_used( $original_singular );
     214            return $response;
     215        }
     216    }
     217
     218    /**
     219     * Updates the number of characters used by DeepL.
     220     *
     221     * @param string $original_singular The singular from the original string.
     222     */
     223    private function update_deepl_chars_used( string $original_singular ) {
     224        $gp_external_translations = get_user_option( 'gp_external_translations' );
     225        $deepl_chars_used         = gp_array_get( $gp_external_translations, 'deepl_chars_used', 0 );
     226        if ( ! is_int( $deepl_chars_used ) || $deepl_chars_used < 0 ) {
     227            $deepl_chars_used = 0;
     228        }
     229        $deepl_chars_used                            += mb_strlen( $original_singular );
     230        $gp_external_translations['deepl_chars_used'] = $deepl_chars_used;
     231        update_user_option( get_current_user_id(), 'gp_external_translations', $gp_external_translations );
     232    }
     233
     234    /**
     235     * Gets the Deepl locale.
     236     *
     237     * @param string $locale The WordPress locale.
     238     *
     239     * @return string
     240     */
     241    private function get_deepl_locale( string $locale ): string {
     242        $available_locales = array(
     243            'bg'    => 'BG',
     244            'cs'    => 'CS',
     245            'da'    => 'DA',
     246            'de'    => 'DE',
     247            'el'    => 'EL',
     248            'en-gb' => 'EN-GB',
     249            'es'    => 'ES',
     250            'et'    => 'ET',
     251            'fi'    => 'FI',
     252            'fr'    => 'FR',
     253            'hu'    => 'HU',
     254            'id'    => 'ID',
     255            'it'    => 'IT',
     256            'ja'    => 'JA',
     257            'ko'    => 'KO',
     258            'lt'    => 'LT',
     259            'lv'    => 'LV',
     260            'nb'    => 'NB',
     261            'nl'    => 'NL',
     262            'pl'    => 'PL',
     263            'pt'    => 'PT-PT',
     264            'pt-br' => 'PT-BR',
     265            'ro'    => 'RO',
     266            'ru'    => 'RU',
     267            'sk'    => 'SK',
     268            'sl'    => 'SL',
     269            'sv'    => 'SV',
     270            'tr'    => 'TR',
     271            'uk'    => 'UK',
     272            'zh-cn' => 'ZH',
     273        );
     274        if ( array_key_exists( $locale, $available_locales ) ) {
     275            return $available_locales[ $locale ];
     276        }
     277        return '';
     278    }
     279
     280    /**
     281     * Gets the formality of the language.
     282     *
     283     * @param string $locale   The locale.
     284     * @param string $set_slug The set slug.
     285     *
     286     * @return string
     287     */
     288    private function get_language_formality( string $locale, string $set_slug ): string {
     289        $lang_informality = array(
     290            'BG'    => 'prefer_more',
     291            'CS'    => 'prefer_less',
     292            'DA'    => 'prefer_less',
     293            'DE'    => 'prefer_less',
     294            'EL'    => 'prefer_more',
     295            'EN-GB' => 'prefer_less',
     296            'ES'    => 'prefer_less',
     297            'ET'    => 'prefer_less',
     298            'FI'    => 'prefer_less',
     299            'FR'    => 'prefer_more',
     300            'HU'    => 'prefer_more',
     301            'ID'    => 'prefer_more',
     302            'IT'    => 'prefer_less',
     303            'JA'    => 'prefer_more',
     304            'KO'    => 'prefer_less',
     305            'LT'    => 'prefer_more',
     306            'LV'    => 'prefer_less',
     307            'NB'    => 'prefer_less',
     308            'NL'    => 'prefer_less',
     309            'PL'    => 'prefer_less',
     310            'PT-BR' => 'prefer_less',
     311            'PT-PT' => 'prefer_more',
     312            'RO'    => 'prefer_less',
     313            'RU'    => 'prefer_more',
     314            'SK'    => 'prefer_less',
     315            'SL'    => 'prefer_less',
     316            'SV'    => 'prefer_less',
     317            'TR'    => 'prefer_less',
     318            'UK'    => 'prefer_more',
     319            'ZH'    => 'prefer_more',
     320        );
     321
     322        if ( ( 'DE' == $locale || 'NL' == $locale ) && 'formal' == $set_slug ) {
     323            return 'prefer_more';
     324        }
     325        if ( array_key_exists( $locale, $lang_informality ) ) {
     326            return $lang_informality[ $locale ];
     327        }
     328
     329        return 'default';
     330    }
     331
     332    /**
     333     * Updates the external translations used by each user.
     334     *
     335     * @return void
     336     */
     337    public function update_external_translations() {
     338        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wporg-editor-settings' ) ) {
     339            wp_send_json_error( array( 'message' => esc_html__( 'Invalid nonce.', 'glotpress' ) ), 403 );
     340        }
     341        if ( ! isset( $_POST['translation'] ) ) {
     342            wp_send_json_error( array( 'message' => esc_html__( 'Translation parameter is not present.', 'glotpress' ) ), 400 );
     343        }
     344        if ( ! isset( $_POST['openAITranslationsUsed'] ) && ! isset( $_POST['deeplTranslationsUsed'] ) ) {
     345            wp_send_json_error( array( 'message' => esc_html__( 'Translation suggested parameter is not present.', 'glotpress' ) ), 400 );
     346        }
     347        if ( isset( $_POST['openAITranslationsUsed'] ) ) {
     348            $this->update_one_external_translation(
     349                $_POST['translation'],
     350                $_POST['openAITranslationsUsed'],
     351                'openai_translations_used',
     352                'openai_same_translations_used'
     353            );
     354        }
     355        if ( isset( $_POST['deeplTranslationsUsed'] ) ) {
     356            $this->update_one_external_translation(
     357                $_POST['translation'],
     358                $_POST['deeplTranslationsUsed'],
     359                'deepl_translations_used',
     360                'deepl_same_translations_used'
     361            );
     362        }
     363        wp_send_json_success();
     364    }
     365
     366    /**
     367     * Updates an external translation used by each user.
     368     *
     369     * @param string $translation                     The translation.
     370     * @param string $suggestion                      The suggestion.
     371     * @param string $external_translations_used      The external translations used.
     372     * @param string $external_same_translations_used The external same translations used.
     373     *
     374     * @return void
     375     */
     376    private function update_one_external_translation( string $translation, string $suggestion, string $external_translations_used, string $external_same_translations_used ) {
     377        $sameTranslationUsed      = $translation == $suggestion;
     378        $gp_external_translations = get_user_option( 'gp_external_translations' );
     379        $translations_used        = gp_array_get( $gp_external_translations, $external_translations_used, 0 );
     380        $same_translations_used   = gp_array_get( $gp_external_translations, $external_same_translations_used, 0 );
     381        if ( ! is_int( $translations_used ) || $translations_used < 0 ) {
     382            $translations_used = 0;
     383        }
     384        $translations_used++;
     385        $gp_external_translations[ $external_translations_used ] = $translations_used;
     386        if ( $sameTranslationUsed ) {
     387            if ( ! is_int( $same_translations_used ) || $same_translations_used < 0 ) {
     388                $same_translations_used = 0;
     389            }
     390            $same_translations_used++;
     391            $gp_external_translations[ $external_same_translations_used ] = $same_translations_used;
     392        }
     393        update_user_option( get_current_user_id(), 'gp_external_translations', $gp_external_translations );
    41394    }
    42395}
Note: See TracChangeset for help on using the changeset viewer.