Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/translatepress-multilingual/class-translate-press.php
+++ b/translatepress-multilingual/class-translate-press.php
@@ -82,7 +82,7 @@
define( 'TRP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'TRP_PLUGIN_BASE', plugin_basename( __DIR__ . '/index.php' ) );
define( 'TRP_PLUGIN_SLUG', 'translatepress-multilingual' );
- define( 'TRP_PLUGIN_VERSION', '3.2.6' );
+ define( 'TRP_PLUGIN_VERSION', '3.3' );
wp_cache_add_non_persistent_groups(array('trp'));
@@ -382,6 +382,7 @@
$this->loader->add_action( 'admin_menu', $this->upgrade, 'register_menu_page' );
$this->loader->add_action( 'admin_init', $this->upgrade, 'show_admin_error_message' );
+ $this->loader->add_action( 'admin_init', $this->upgrade, 'maybe_start_gettext_tables_optimization_from_notice' );
$this->loader->add_action( 'admin_init', $this->upgrade, 'show_admin_notice' );
$this->loader->add_action( 'admin_init', $this->upgrade, 'show_notification_about_add_ons_removal' );
$this->loader->add_action( 'admin_init', $this->upgrade, 'trp_prepare_options_for_database_optimization' );
--- a/translatepress-multilingual/includes/advanced-settings/remove-duplicates-from-db.php
+++ b/translatepress-multilingual/includes/advanced-settings/remove-duplicates-from-db.php
@@ -5,6 +5,42 @@
add_filter( 'trp_register_advanced_settings', 'trp_register_remove_duplicate_entries_from_db', 530 );
function trp_register_remove_duplicate_entries_from_db( $settings_array ){
+ $gettext_optimization_pending = get_option( 'trp_updated_database_gettext_tables_optimization', 'yes' ) === 'no';
+ $gettext_batch_status = get_option( 'trp_gettext_tables_optimization_330', 'is not set' );
+
+ if ( $gettext_optimization_pending && ! in_array( $gettext_batch_status, array( 'no', 'failed' ), true ) ) {
+ $start_url = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'page' => 'trp_advanced_page',
+ 'tab' => 'troubleshooting',
+ 'trp_start_gettext_tables_optimization' => '1',
+ ),
+ admin_url( 'admin.php' )
+ ),
+ 'trp_start_gettext_tables_optimization'
+ );
+
+ $settings_array[] = array(
+ 'name' => 'pending_gettext_database_optimization',
+ 'type' => 'text',
+ 'label' => esc_html__( 'Pending gettext database optimization', 'translatepress-multilingual' ),
+ 'description' => sprintf(
+ wp_kses(
+ __( 'TranslatePress needs to optimize its gettext database tables. Back up the database, then <a href="%s">start the optimization</a>.', 'translatepress-multilingual' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ ),
+ )
+ ),
+ esc_url( $start_url )
+ ),
+ 'id' => 'debug',
+ 'container' => 'debug',
+ );
+ }
+
$settings_array[] = array(
'name' => 'remove_duplicate_entries_from_db',
'type' => 'text',
--- a/translatepress-multilingual/includes/class-editor-api-gettext-strings.php
+++ b/translatepress-multilingual/includes/class-editor-api-gettext-strings.php
@@ -89,12 +89,15 @@
$update_strings[ $language ] = array();
foreach( $language_strings as $string ) {
if ( isset( $string->id ) && is_numeric( $string->id ) ) {
+ $translated = trp_sanitize_string( $string->translated );
+ $status = ! empty( $translated ) ? TRP_Query::HUMAN_REVIEWED : TRP_Query::NOT_TRANSLATED;
+
array_push($update_strings[ $language ], array(
'id' => (int)$string->id,
'original' => trp_sanitize_string( $string->original, false ),
- 'translated' => trp_sanitize_string( $string->translated ),
+ 'translated' => $translated,
'domain' => sanitize_text_field( $string->domain ),
- 'status' => (int)$string->status,
+ 'status' => $status,
'plural_form' => (int)$string->plural_form,
'context' => $string->context
));
--- a/translatepress-multilingual/includes/class-plugin-notices.php
+++ b/translatepress-multilingual/includes/class-plugin-notices.php
@@ -48,7 +48,7 @@
// Check that the user hasn't already clicked to ignore the message
if ( ! get_user_meta($user_id, $this->notificationId.'_dismiss_notification' ) || $this->force_show ) {//ignore the dismissal if we have force_show
add_filter('safe_style_css', array( $this, 'allow_z_index_in_wp_kses'));
- echo wp_kses( apply_filters($this->notificationId.'_notification_message','<div class="'. $this->notificationClass .'" style="position:relative;' . ((strpos($this->notificationClass, 'trp-narrow')!==false ) ? 'max-width: 825px;' : '') . '" >'.$this->notificationMessage.'</div>', $this->notificationMessage), [ 'div' => [ 'class' => [],'style' => [] ], 'p' => ['style' => [], 'class' => []], 'a' => ['href' => [], 'type'=> [], 'class'=> [], 'style'=>[], 'title'=>[],'target'=>[]], 'span' => ['class'=> []], 'strong' => [], 'img' => [ 'src' => [], 'style' => [] ], 'br' => [] ] );
+ echo wp_kses( apply_filters($this->notificationId.'_notification_message','<div class="'. $this->notificationClass .'" style="position:relative;' . ((strpos($this->notificationClass, 'trp-narrow')!==false ) ? 'max-width: 825px;' : '') . '" >'.$this->notificationMessage.'</div>', $this->notificationMessage), [ 'div' => [ 'class' => [],'style' => [] ], 'p' => ['style' => [], 'class' => []], 'a' => ['href' => [], 'type'=> [], 'class'=> [], 'style'=>[], 'title'=>[],'target'=>[]], 'span' => ['class'=> [], 'style' => []], 'strong' => [], 'img' => [ 'src' => [], 'style' => [] ], 'br' => [] ] );
remove_filter('safe_style_css', array( $this, 'allow_z_index_in_wp_kses'));
}
do_action( $this->notificationId.'_notification_displayed', $current_user, $pagenow );
@@ -59,6 +59,12 @@
function allow_z_index_in_wp_kses( $styles ) {
$styles[] = 'z-index';
$styles[] = 'position';
+ $styles[] = 'display';
+ $styles[] = 'align-items';
+ $styles[] = 'gap';
+ $styles[] = 'float';
+ $styles[] = 'margin';
+ $styles[] = 'visibility';
return $styles;
}
--- a/translatepress-multilingual/includes/class-plugin-optin.php
+++ b/translatepress-multilingual/includes/class-plugin-optin.php
@@ -485,6 +485,7 @@
'trp_updated_database_gettext_original_id_cleanup',
'trp_updated_database_gettext_original_id_insert',
'trp_updated_database_gettext_original_id_update',
+ 'trp_updated_database_gettext_runtime_status_update',
'trp_were_old_slug_tables_found',
'trp_add_ons_settings',
];
--- a/translatepress-multilingual/includes/class-translation-manager.php
+++ b/translatepress-multilingual/includes/class-translation-manager.php
@@ -182,6 +182,7 @@
//human or machine translation tooltips
'human_translation' => esc_html__('Human Translation', 'translatepress-multilingual'),
'machine_translation' => esc_html__('Machine Translation', 'translatepress-multilingual'),
+ 'language_file_translation' => esc_html__('Gettext File Translation', 'translatepress-multilingual'),
'percentage_bar' => array(
'tooltip_text_default' => esc_html__( 'Text on this page is %s% translated into all languages.', 'translatepress-multilingual'),
'tooltip_text_general' => esc_html__( '%1$s% of text on this page is translated into %2$s.', 'translatepress-multilingual'),
@@ -1010,13 +1011,18 @@
$url = add_query_arg( array(
'page' => 'trp_update_database',
), site_url('wp-admin/admin.php') );
+ $database_update_confirmation_message = sprintf(
+ "%sn%s",
+ __( 'IMPORTANT: It is strongly recommended to first backup the database!', 'translatepress-multilingual' ),
+ __( 'Are you sure you want to continue?', 'translatepress-multilingual' )
+ );
// maybe change notice color to blue #28B1FF
$html = "<div class='trp-notice trp-notice-warning'>";
$html .= '<p><strong>' . esc_html__( 'TranslatePress data update', 'translatepress-multilingual' ) . '</strong> – ' . esc_html__( 'We need to update your translations database to the latest version.', 'translatepress-multilingual' ) . '</p>';
$html .= '<p>' . esc_html__( 'Updating will allow editing translations of localized text from plugins and theme. Existing translation will still work as expected.', 'translatepress-multilingual' ) . '</p>';
- $html .= '<p><a class="trp-button-primary" target="_blank" href="' . esc_url( $url ) . '" onclick="return confirm( '' . __( 'IMPORTANT: It is strongly recommended to first backup the database!nAre you sure you want to continue?', 'translatepress-multilingual' ) . '');" class="button-primary">' . esc_html__( 'Run the updater', 'translatepress-multilingual' ) . '</a></p>';
+ $html .= '<p><a class="trp-button-primary" target="_blank" href="' . esc_url( $url ) . '" onclick="return confirm( ' . esc_attr( wp_json_encode( $database_update_confirmation_message ) ) . ' );" class="button-primary">' . esc_html__( 'Run the updater', 'translatepress-multilingual' ) . '</a></p>';
$html .= '</div>';
$trp_editor_notices = $html;
@@ -1030,12 +1036,17 @@
$url = add_query_arg( array(
'page' => 'trp_update_database',
), site_url('wp-admin/admin.php') );
+ $database_update_confirmation_message = sprintf(
+ "%sn%s",
+ __( 'IMPORTANT: It is strongly recommended to first backup the database!', 'translatepress-multilingual' ),
+ __( 'Are you sure you want to continue?', 'translatepress-multilingual' )
+ );
$html = "<div class='trp-notice trp-notice-warning'>";
$html .= '<p><strong>' . esc_html__( 'TranslatePress data update', 'translatepress-multilingual' ) . '</strong> – ' . esc_html__( 'We need to update your translations database to the latest version.', 'translatepress-multilingual' ) . '</p>';
$html .= '<p>' . esc_html__( 'Updating will allow editing translations of slugs. Existing translation will still work as expected.', 'translatepress-multilingual' ) . '</p>';
- $html .= '<p><a class="trp-button-primary" target="_blank" href="' . esc_url( $url ) . '" onclick="return confirm( '' . __( 'IMPORTANT: It is strongly recommended to first backup the database!nAre you sure you want to continue?', 'translatepress-multilingual' ) . '');" class="button-primary">' . esc_html__( 'Run the updater', 'translatepress-multilingual' ) . '</a></p>';
+ $html .= '<p><a class="trp-button-primary" target="_blank" href="' . esc_url( $url ) . '" onclick="return confirm( ' . esc_attr( wp_json_encode( $database_update_confirmation_message ) ) . ' );" class="button-primary">' . esc_html__( 'Run the updater', 'translatepress-multilingual' ) . '</a></p>';
$html .= '</div>';
$trp_editor_notices = $html;
--- a/translatepress-multilingual/includes/class-translation-render.php
+++ b/translatepress-multilingual/includes/class-translation-render.php
@@ -1564,25 +1564,48 @@
/**
* function that removes any unwanted leftover <trp-gettext> tags
+ *
+ * Security ( CU-869eddnvm ): the opening-tag removals below used an unbounded inner match ( .*? ) that
+ * could bridge across html attribute/tag delimiters. TP emits these wrappers as real tags, but the same
+ * marker can also appear ENCODED ( percent-encoded, or html-entity escaped: %23%21trpst%23trp-gettext,
+ * <trp-gettext ) inside an attribute value that survived wp_kses, because the marker text contains no
+ * html-special characters. A .*? there let a start marker inside one attribute ( href ) reach an end
+ * marker inside another ( title ) and collapse everything between them, splicing the two attributes into
+ * e.g. href="javascript:..." from an otherwise kses-clean, unauthenticated comment ( stored XSS ).
+ *
+ * The real-tag form ( <trp-gettext ...> ) is never attacker-reachable: wp_kses strips a literal <trp-*>
+ * tag from user input, so it stays permissive ( bounded only by the real angle brackets ). Only the
+ * encoded/entity form is attacker-reachable, so there the captured span excludes the raw attribute
+ * delimiters "'<> and can no longer leave a single attribute value / text node. Legitimately escaped
+ * wrappers carry "/' rather than raw quotes, so they are still removed.
+ *
* @param $string
* @return string|string[]|null
*/
function remove_trp_html_tags( $string ){
- $string = preg_replace( '/(<|<)trp-gettext (.*?)(>|>)/i', '', $string );
+ // trp-gettext opening tag: the real form ( <...> ) and the entity form ( <...> ) both carry an
+ // unquoted attribute ( data-trpgettextoriginal=123 ), so a single delimiter-constrained match is safe.
+ $string = preg_replace( '/(<|<)trp-gettext ([^"'<>]*?)(>|>)/i', '', $string );
$string = preg_replace( '/(<|<)(\\)*/trp-gettext(>|>)/i', '', $string );
// In case we have a gettext string which was run through rawurlencode(). See more details on iss6563
- $string = preg_replace( '/%23%21trpst%23trp-gettext(.*?)%23%21trpen%23/i', '', $string );
+ $string = preg_replace( '/%23%21trpst%23trp-gettext([^"'<>]*?)%23%21trpen%23/i', '', $string );
$string = preg_replace( '/%23%21trpst%23%2Ftrp-gettext%23%21trpen%23/i', '', $string );
$string = preg_replace( '/%23%21trpst%23%5C%2Ftrp-gettext%23%21trpen%23/i', '', $string );
if (!isset($_REQUEST['trp-edit-translation']) || $_REQUEST['trp-edit-translation'] != 'preview') {
- $string = preg_replace('/(<|<)trp-wrap (.*?)(>|>)/i', '', $string);
+ // Real trp-wrap tag carries a double-quoted attribute ( class="trp-wrap" ), so the real form stays
+ // permissive; only the attacker-reachable entity form is delimiter-constrained.
+ $string = preg_replace('/<trp-wrap [^<>]*?>/i', '', $string);
+ $string = preg_replace('/<trp-wrap ([^"'<>]*?)>/i', '', $string);
$string = preg_replace('/(<|<)(\\)*/trp-wrap(>|>)/i', '', $string);
}
//remove post containers before outputting
- $string = preg_replace( '/(<|<)trp-post-container (.*?)(>|>)/i', '', $string );
+ // Real trp-post-container carries a single-quoted attribute ( data-trp-post-id='123' ), so the real
+ // form stays permissive; only the attacker-reachable entity form is delimiter-constrained.
+ $string = preg_replace( '/<trp-post-container [^<>]*?>/i', '', $string );
+ $string = preg_replace( '/<trp-post-container ([^"'<>]*?)>/i', '', $string );
$string = preg_replace( '/(<|<)(\\)*/trp-post-container(>|>)/i', '', $string );
return $string;
--- a/translatepress-multilingual/includes/class-upgrade.php
+++ b/translatepress-multilingual/includes/class-upgrade.php
@@ -65,6 +65,23 @@
}
}
+ if ( get_option( 'trp_updated_database_gettext_original_lookup_hash', 'is not set' ) === 'is not set' ) {
+ $originals_table = $this->trp_query->get_table_name_for_gettext_original_strings();
+ $lookup_hash_is_ready = (
+ ! $this->trp_query->table_exists( $originals_table ) ||
+ (
+ $this->trp_query->table_column_exists( $originals_table, 'lookup_hash' ) &&
+ $this->trp_query->table_index_exists( $originals_table, 'gettext_lookup_hash_unique' )
+ )
+ );
+
+ update_option( 'trp_updated_database_gettext_original_lookup_hash', $lookup_hash_is_ready ? 'yes' : 'no' );
+ }
+
+ if ( get_option( 'trp_updated_database_gettext_tables_optimization', 'is not set' ) === 'is not set' ) {
+ update_option( 'trp_updated_database_gettext_tables_optimization', $this->gettext_tables_optimization_is_ready() ? 'yes' : 'no' );
+ }
+
// Updates that can be done right way. They should take very little time.
if ( version_compare( $stored_database_version, '1.3.0', '<=' ) ) {
$this->trp_query->check_for_block_type_column();
@@ -223,6 +240,7 @@
'gettext_original_id_insert' => __('Inserting gettext original strings for language %s...', 'translatepress-multilingual' ),
'gettext_original_id_cleanup' => __('Cleaning gettext original strings table for language %s...', 'translatepress-multilingual' ),
'gettext_original_id_update' => __('Updating gettext original string ids for language %s...', 'translatepress-multilingual' ),
+ 'gettext_tables_optimization' => __('Starting gettext database optimization in the background...', 'translatepress-multilingual' ),
'migrate_old_slugs_to_the_new_translate_table_structure_post_type_and_tax_284' => __( 'Migrating taxonomy and post type base slugs to new table structure...', 'translatepress-multilingual' ),
'migrate_old_slugs_to_the_new_translate_table_structure_post_meta_284' => __( 'Migrating post slugs to new table structure for language %s...', 'translatepress-multilingual' ),
'migrate_old_slugs_to_the_new_translate_table_structure_term_meta_284' => __( 'Migrating term slugs to new table structure for language %s...', 'translatepress-multilingual' ),
@@ -338,6 +356,14 @@
'batch_size' => 5000,
'message_initial' => '',
),
+ 'gettext_tables_optimization' => array(
+ 'version' => '3.3',
+ 'option_name' => 'trp_updated_database_gettext_tables_optimization',
+ 'callback' => array( $this,'trp_start_gettext_tables_optimization'),
+ 'batch_size' => 1,
+ 'message_initial' => '',
+ 'execute_only_once' => true,
+ ),
'migrate_old_slugs_to_the_new_translate_table_structure_post_type_and_tax_284' => array(
'version' => '0',
'option_name' => 'trp_migrate_old_slug_to_new_parent_and_translate_slug_table_post_type_and_tax_284',
@@ -387,9 +413,12 @@
}
$updates_needed = $this->get_updates_details();
$option_db_error_message = get_option( $updates_needed['show_error_db_message']['option_name'] );
- foreach ( $updates_needed as $update ) {
+ foreach ( $updates_needed as $update_key => $update ) {
$option = get_option( $update['option_name'], 'is not set' );
if ( $option === 'no' && $option_db_error_message !== 'no' ) {
+ if ( $update_key === 'gettext_tables_optimization' && $this->gettext_tables_optimization_batch_has_started() ) {
+ continue;
+ }
add_action( 'admin_notices', array( $this, 'admin_notice_update_database' ) );
break;
}
@@ -401,6 +430,26 @@
* Print admin notice message
*/
public function admin_notice_update_database() {
+ $database_update_confirmation_message = sprintf(
+ "%sn%s",
+ __( 'IMPORTANT: It is strongly recommended to first backup the database!', 'translatepress-multilingual' ),
+ __( 'Are you sure you want to continue?', 'translatepress-multilingual' )
+ );
+
+ if ( $this->only_gettext_tables_optimization_is_pending() ) {
+ $url = wp_nonce_url(
+ add_query_arg( array( 'trp_start_gettext_tables_optimization' => '1' ) ),
+ 'trp_start_gettext_tables_optimization'
+ );
+
+ $html = '<div id="message" class="notice notice-warning">';
+ $html .= '<p><strong>' . esc_html__( 'TranslatePress data update', 'translatepress-multilingual' ) . '</strong> – ' . esc_html__( 'TranslatePress needs to optimize gettext database tables. Translations continue to work while this runs in the background.', 'translatepress-multilingual' ) . '</p>';
+ $html .= '<p>' . esc_html__( 'Before starting, we strongly recommend creating a database backup.', 'translatepress-multilingual' ) . '</p>';
+ $html .= '<p class="submit"><a href="' . esc_url( $url ) . '" onclick="return confirm( ' . esc_attr( wp_json_encode( $database_update_confirmation_message ) ) . ' );" class="button-primary">' . esc_html__( 'Start optimization', 'translatepress-multilingual' ) . '</a></p>';
+ $html .= '</div>';
+ echo $html;//phpcs:ignore
+ return;
+ }
$url = add_query_arg( array(
'page' => 'trp_update_database',
@@ -409,17 +458,128 @@
// maybe change notice color to blue #28B1FF
$html = '<div id="message" class="updated">';
$html .= '<p><strong>' . esc_html__( 'TranslatePress data update', 'translatepress-multilingual' ) . '</strong> – ' . esc_html__( 'We need to update your translations database to the latest version.', 'translatepress-multilingual' ) . '</p>';
- $html .= '<p class="submit"><a href="' . esc_url( $url ) . '" onclick="return confirm( '' . __( 'IMPORTANT: It is strongly recommended to first backup the database!nAre you sure you want to continue?', 'translatepress-multilingual' ) . '');" class="button-primary">' . esc_html__( 'Run the updater', 'translatepress-multilingual' ) . '</a></p>';
+ $html .= '<p class="submit"><a href="' . esc_url( $url ) . '" onclick="return confirm( ' . esc_attr( wp_json_encode( $database_update_confirmation_message ) ) . ' );" class="button-primary">' . esc_html__( 'Run the updater', 'translatepress-multilingual' ) . '</a></p>';
$html .= '</div>';
echo $html;//phpcs:ignore
}
+ /**
+ * Start gettext table optimization directly from the admin notice.
+ */
+ public function maybe_start_gettext_tables_optimization_from_notice() {
+ if ( empty( $_GET['trp_start_gettext_tables_optimization'] ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( apply_filters( 'trp_update_database_capability', 'manage_options' ) ) ) {
+ return;
+ }
+
+ $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
+ if ( ! wp_verify_nonce( $nonce, 'trp_start_gettext_tables_optimization' ) ) {
+ return;
+ }
+
+ $this->trp_start_gettext_tables_optimization();
+
+ wp_safe_redirect( remove_query_arg( array( 'trp_start_gettext_tables_optimization', '_wpnonce' ) ) );
+ exit;
+ }
+
+ /**
+ * Return whether gettext table optimization is the only pending DB updater item.
+ *
+ * @return bool
+ */
+ protected function only_gettext_tables_optimization_is_pending() {
+ $updates_needed = $this->get_updates_details();
+ $pending_keys = array();
+
+ foreach ( $updates_needed as $key => $update ) {
+ if ( get_option( $update['option_name'], 'is not set' ) === 'no' ) {
+ $pending_keys[] = $key;
+ }
+ }
+
+ return count( $pending_keys ) === 1 && $pending_keys[0] === 'gettext_tables_optimization';
+ }
+
+ /**
+ * Return whether the background gettext optimizer task has already started.
+ *
+ * @return bool
+ */
+ protected function gettext_tables_optimization_batch_has_started() {
+ return in_array( get_option( 'trp_gettext_tables_optimization_330', 'is not set' ), array( 'no', 'failed' ), true );
+ }
+
public function trp_successfully_run_database_optimization($language_code= null, $inferior_size = null, $batch_size = null){
delete_option('trp_show_error_db_message');
return true;
}
+ /**
+ * Schedule gettext database optimization in the background.
+ *
+ * @return bool
+ */
+ public function trp_start_gettext_tables_optimization() {
+ $trp = TRP_Translate_Press::get_trp_instance();
+ $batch_processor = $trp->get_component( 'batch_processor' );
+
+ if ( $batch_processor && $batch_processor->start_task( 'trp_gettext_tables_optimization_330', true ) ) {
+ return true;
+ }
+
+ $this->stop_and_print_error( __( 'Update aborted! Could not schedule gettext database optimization.', 'translatepress-multilingual' ) );
+ }
+
+ /**
+ * Check if gettext tables already have the final optimized schema.
+ *
+ * @return bool
+ */
+ protected function gettext_tables_optimization_is_ready() {
+ if ( ! $this->trp_query ) {
+ $trp = TRP_Translate_Press::get_trp_instance();
+ /* @var TRP_Query */
+ $this->trp_query = $trp->get_component( 'query' );
+ }
+
+ $originals_table = $this->trp_query->get_table_name_for_gettext_original_strings();
+
+ if ( ! $this->trp_query->table_exists( $originals_table ) ) {
+ return true;
+ }
+
+ if (
+ ! $this->trp_query->table_column_exists( $originals_table, 'lookup_hash' ) ||
+ ! $this->trp_query->table_index_exists( $originals_table, 'gettext_lookup_hash_unique' ) ||
+ ! $this->trp_query->table_index_exists( $originals_table, 'gettext_lookup_original_domain_context' )
+ ) {
+ return false;
+ }
+
+ foreach ( $this->trp_query->get_all_gettext_table_names() as $table_name ) {
+ $table_name = sanitize_text_field( $table_name );
+
+ if ( ! $this->trp_query->table_exists( $table_name ) ) {
+ continue;
+ }
+
+ if ( ! $this->trp_query->table_column_exists( $table_name, 'original_id' ) || ! $this->trp_query->table_column_exists( $table_name, 'plural_form' ) ) {
+ return false;
+ }
+
+ if ( ! $this->trp_query->table_index_exists( $table_name, 'gettext_original_plural_unique' ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public function show_admin_error_message(){
if ( ( isset( $_GET[ 'page'] ) && $_GET['page'] == 'trp_update_database' ) ){
@@ -1043,6 +1203,206 @@
}
}
+ /**
+ * Run the gettext originals lookup hash migration through the dedicated migration class.
+ *
+ * @param string $language_code Unused. Kept for updater callback signature.
+ * @param int $inferior_limit Unused. This migration resumes by DB state.
+ * @param int $batch_size Number of rows/groups/map entries per batch.
+ * @param array $extra_params Migration phase state.
+ *
+ * @return array|bool
+ * @throws Exception
+ */
+ public function trp_updated_database_gettext_original_lookup_hash( $language_code, $inferior_limit, $batch_size, $extra_params = array() ) {
+ if ( ! $this->trp_query ) {
+ $trp = TRP_Translate_Press::get_trp_instance();
+ /* @var TRP_Query */
+ $this->trp_query = $trp->get_component( 'query' );
+ }
+
+ require_once TRP_PLUGIN_DIR . 'includes/upgrade/gettext-optimization/class-gettext-originals-lookup-hash-migration.php';
+
+ $migration = new TRP_Gettext_Originals_Lookup_Hash_Migration( $this->trp_query );
+
+ return $migration->run( $language_code, $inferior_limit, $batch_size, $extra_params );
+ }
+
+ /**
+ * Classify gettext rows into language-file or runtime gettext statuses.
+ *
+ * Rows imported from .po/.mo files use status 4 when their stored
+ * translation matches the active language-file translation. Rows that must
+ * override runtime gettext output use the human-reviewed status 2. Machine
+ * translated rows keep their existing status semantics.
+ *
+ * @param string $language_code
+ * @param int $inferior_limit
+ * @param int $batch_size
+ * @param string $table_name Optional discovered gettext table name.
+ *
+ * @return bool
+ */
+ public function trp_updated_database_gettext_runtime_status_update( $language_code, $inferior_limit, $batch_size, $table_name = '' ) {
+ if ( ! $this->trp_query ) {
+ $trp = TRP_Translate_Press::get_trp_instance();
+ /* @var TRP_Query */
+ $this->trp_query = $trp->get_component( 'query' );
+ }
+
+ $table_name = is_string( $table_name ) && $table_name !== '' ? sanitize_text_field( $table_name ) : sanitize_text_field( $this->trp_query->get_gettext_table_name( $language_code ) );
+ $originals_table = sanitize_text_field( $this->trp_query->get_table_name_for_gettext_original_strings() );
+
+ if ( ! $this->trp_query->table_exists( $table_name ) ) {
+ return true;
+ }
+
+ $rows = $this->db->get_results(
+ $this->db->prepare(
+ "SELECT tt.id, tt.translated, tt.status, tt.original AS tt_original, tt.domain AS tt_domain, tt.plural_form, ot.original, ot.domain, ot.context, ot.original_plural
+ FROM `$table_name` AS tt
+ LEFT JOIN `$originals_table` AS ot ON tt.original_id = ot.id
+ ORDER BY tt.id
+ LIMIT %d, %d",
+ $inferior_limit,
+ $batch_size
+ ),
+ ARRAY_A
+ );
+
+ if ( empty( $rows ) ) {
+ return true;
+ }
+
+ $current_locale = determine_locale();
+ $switched = switch_to_locale( $language_code );
+ $language_file_available = $switched || $current_locale === $language_code;
+
+ $ids_to_human_reviewed_status = array();
+ $ids_to_language_file_status = array();
+
+ foreach ( $rows as $row ) {
+ $new_status = $this->get_migrated_gettext_status( $row, $language_file_available );
+
+ if ( $new_status === TRP_Query::HUMAN_REVIEWED ) {
+ $ids_to_human_reviewed_status[] = (int) $row['id'];
+ } elseif ( $new_status === TRP_Query::GETTEXT_TRANSLATED_IN_LANGUAGE_FILE ) {
+ $ids_to_language_file_status[] = (int) $row['id'];
+ }
+ }
+
+ if ( $switched ) {
+ restore_previous_locale();
+ }
+
+ $this->update_gettext_statuses( $table_name, $ids_to_human_reviewed_status, TRP_Query::HUMAN_REVIEWED );
+ $this->update_gettext_statuses( $table_name, $ids_to_language_file_status, TRP_Query::GETTEXT_TRANSLATED_IN_LANGUAGE_FILE );
+
+ $is_finished = count( $rows ) < $batch_size;
+
+ return $is_finished;
+ }
+
+ /**
+ * Decide the migrated gettext status for an existing DB row.
+ *
+ * Empty/untranslated rows and machine translated rows keep their existing
+ * status semantics. Human/file-based rows use status 4 when they match the
+ * active language-file output and status 2 when they must override it.
+ *
+ * @param array $row Gettext row joined with the original gettext table.
+ * @param bool $language_file_available Whether WordPress loaded language files for the target locale.
+ *
+ * @return int|null
+ */
+ protected function get_migrated_gettext_status( $row, $language_file_available = true ) {
+ if ( empty( $row['translated'] ) || (int) $row['status'] === TRP_Query::NOT_TRANSLATED ) {
+ return null;
+ }
+
+ if ( (int) $row['status'] === TRP_Query::MACHINE_TRANSLATED ) {
+ return TRP_Query::MACHINE_TRANSLATED;
+ }
+
+ $original = ! empty( $row['original'] ) ? $row['original'] : $row['tt_original'];
+ $domain = ! empty( $row['domain'] ) ? $row['domain'] : $row['tt_domain'];
+ $context = ! empty( $row['context'] ) ? $row['context'] : 'trp_context';
+ $plural_form = isset( $row['plural_form'] ) ? (int) $row['plural_form'] : 0;
+ $original_plural = ! empty( $row['original_plural'] ) ? $row['original_plural'] : null;
+
+ if ( empty( $original ) || empty( $domain ) ) {
+ return TRP_Query::HUMAN_REVIEWED;
+ }
+
+ if ( ! $language_file_available ) {
+ $source_translation = ( ! empty( $original_plural ) && (int) $plural_form > 0 ) ? $original_plural : $original;
+
+ return ( $row['translated'] !== $source_translation ) ? TRP_Query::HUMAN_REVIEWED : TRP_Query::GETTEXT_TRANSLATED_IN_LANGUAGE_FILE;
+ }
+
+ $mo_translation = $this->get_gettext_language_file_translation( $original, $domain, $context, $plural_form, $original_plural );
+
+ return ( $row['translated'] !== $mo_translation ) ? TRP_Query::HUMAN_REVIEWED : TRP_Query::GETTEXT_TRANSLATED_IN_LANGUAGE_FILE;
+ }
+
+ /**
+ * Return the translation WordPress would provide from loaded language files.
+ *
+ * The caller is responsible for switching to the target locale before calling
+ * this method. If no matching entry exists in the language files, WordPress
+ * returns the source string.
+ *
+ * @param string $original Original gettext string.
+ * @param string $domain Text domain.
+ * @param string $context Gettext context or trp_context placeholder.
+ * @param int $plural_form Plural form index stored by TranslatePress.
+ * @param string|null $original_plural Original plural string, when available.
+ *
+ * @return string
+ */
+ protected function get_gettext_language_file_translation( $original, $domain, $context, $plural_form, $original_plural ) {
+ $translations = get_translations_for_domain( $domain );
+ $context = ( $context === 'trp_context' ) ? null : $context;
+
+ if ( ! empty( $original_plural ) ) {
+ $plural_forms = new TRP_Plural_Forms( $this->settings );
+
+ if ( method_exists( $translations, 'translate_entry' ) ) {
+ return $plural_forms->translate_plural( $original, $original_plural, $plural_form, $context, $translations );
+ }
+
+ return (int) $plural_form === 0 ? $original : $original_plural;
+ }
+
+ return $translations->translate( $original, $context );
+ }
+
+ /**
+ * Bulk update gettext status for a list of gettext row ids.
+ *
+ * @param string $table_name Gettext table name.
+ * @param array $ids Row ids to update.
+ * @param int $status New gettext status value.
+ *
+ * @return void
+ */
+ protected function update_gettext_statuses( $table_name, $ids, $status ) {
+ if ( empty( $ids ) ) {
+ return;
+ }
+
+ $ids = array_map( 'intval', $ids );
+ $ids = array_filter( $ids );
+
+ if ( empty( $ids ) ) {
+ return;
+ }
+
+ $this->db->query(
+ "UPDATE `$table_name` SET status = " . (int) $status . " WHERE id IN (" . implode( ',', $ids ) . ")"
+ );
+ }
+
/**
*
* Hooked to admin_init
--- a/translatepress-multilingual/includes/compatibility-functions.php
+++ b/translatepress-multilingual/includes/compatibility-functions.php
@@ -42,6 +42,57 @@
add_filter( 'trp_allow_tp_to_run', 'trp_missing_mbstrings_library' );
/**
+ * robots.txt should never be handled by TranslatePress.
+ *
+ * Without this, the robots.txt file is processed by TranslatePress on secondary
+ * languages: it becomes accessible (and translated) on language URLs such as
+ * /es/robots.txt, and when "Use subdirectory for default language" is enabled the
+ * default robots.txt gets redirected to the language slug URL. Since the resulting
+ * file no longer matches the canonical one, this causes indexing issues.
+ *
+ * The check is based on the request URI (instead of is_robots()) because these
+ * filters run on 'plugins_loaded', before the query is parsed and conditional tags
+ * are available.
+ *
+ * @see https://app.clickup.com/t/qtc0c2
+ *
+ * @param string $url Optional URL to check. Defaults to the current request URI.
+ * @return bool Whether the current request targets a robots.txt file.
+ */
+function trp_is_robots_txt_request( $url = '' ){
+ if ( empty( $url ) ) {
+ $url = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore
+ }
+ if ( empty( $url ) || ! is_string( $url ) ) {
+ return false;
+ }
+ // Only look at the path, ignoring any query string or fragment.
+ $path = wp_parse_url( $url, PHP_URL_PATH );
+ if ( empty( $path ) ) {
+ return false;
+ }
+ return (bool) preg_match( '#(^|/)robots.txt$#i', $path );
+}
+
+// Don't run TranslatePress (no translation, no output buffering) on robots.txt requests.
+function trp_stop_running_on_robots_txt( $allow_to_run ){
+ if ( trp_is_robots_txt_request() ) {
+ return false;
+ }
+ return $allow_to_run;
+}
+add_filter( 'trp_allow_tp_to_run', 'trp_stop_running_on_robots_txt' );
+
+// Don't redirect robots.txt to a language URL (e.g. /robots.txt -> /en/robots.txt).
+function trp_stop_redirect_on_robots_txt( $allow_redirect, $needed_language, $current_page_url ){
+ if ( trp_is_robots_txt_request( $current_page_url ) || trp_is_robots_txt_request() ) {
+ return false;
+ }
+ return $allow_redirect;
+}
+add_filter( 'trp_allow_language_redirect', 'trp_stop_redirect_on_robots_txt', 10, 3 );
+
+/**
* Don't have html inside menu title tags. Some themes just put in the title the content of the link without striping HTML
*/
add_filter( 'nav_menu_link_attributes', 'trp_remove_html_from_menu_title', 10, 3);
@@ -829,7 +880,10 @@
function trp_woo_fix_product_remove_from_cart_notice($message, $cart_item){
$product = wc_get_product( $cart_item['product_id'] );
if ($product){
- $message = sprintf( _x( '“ %s ”', 'Item name in quotes', 'woocommerce' ), $product->get_name() ); //phpcs:ignore
+ $trp = TRP_Translate_Press::get_trp_instance();
+ $translation_render = $trp->get_component( 'translation_render' );
+ $product_name = $translation_render->translate_page( $product->get_name() );
+ $message = sprintf( _x( '“ %s ”', 'Item name in quotes', 'woocommerce' ), $product_name ); //phpcs:ignore
}
return $message;
}
@@ -2202,6 +2256,50 @@
}
+/**
+ * Compatibility with Elementor when editing the static front page while "Use a subdirectory for the
+ * default language" is enabled.
+ *
+ * Elementor builds the preview URL from get_permalink(). For the static front page that permalink is
+ * the bare home URL (no ?page_id=), and TP does not add the language subdirectory on admin requests,
+ * so the preview URL ends up as e.g. https://example.com/?elementor-preview=ID . On the front end that
+ * bare-home request no longer resolves to the front page (it now lives under /<default-language>/), so
+ * it returns a 404 and the Elementor editor hangs on the loading screen.
+ *
+ * Regular pages are not affected because their preview URL carries ?page_id=ID, which resolves fine.
+ *
+ * We fix it at the source by adding the default-language subdirectory to the front-page preview URL
+ * (e.g. https://example.com/<default-language>/?elementor-preview=ID), which resolves correctly (200)
+ * and lets the editor finish loading.
+ */
+add_filter( 'elementor/document/urls/preview', 'trp_elementor_front_page_preview_url_subdirectory', 10, 2 );
+function trp_elementor_front_page_preview_url_subdirectory( $url, $document ) {
+
+ $trp = TRP_Translate_Press::get_trp_instance();
+ $settings = $trp->get_component( 'settings' )->get_settings();
+
+ // Only when the default language uses a subdirectory.
+ if ( ( isset( $settings['add-subdirectory-to-default-language'] ) ? $settings['add-subdirectory-to-default-language'] : 'no' ) !== 'yes' ) {
+ return $url;
+ }
+
+ // Only for the configured static front page.
+ if ( get_option( 'show_on_front' ) !== 'page' ) {
+ return $url;
+ }
+
+ $front_page_id = (int) get_option( 'page_on_front' );
+ if ( $front_page_id === 0 || ! is_object( $document ) || (int) $document->get_main_id() !== $front_page_id ) {
+ return $url;
+ }
+
+ $url_converter = $trp->get_component( 'url_converter' );
+
+ // Pass an empty processed marker so the URL is not suffixed with #TRPLINKPROCESSED.
+ return $url_converter->get_url_for_language( $settings['default-language'], $url, '' );
+}
+
+
/**
* Compatibility with Give WP plugin.
*
--- a/translatepress-multilingual/includes/gettext/class-gettext-manager.php
+++ b/translatepress-multilingual/includes/gettext/class-gettext-manager.php
@@ -56,7 +56,11 @@
$this->trp_query = $trp->get_component( 'query' );
}
- $strings = $this->trp_query->get_all_gettext_strings( $language );
+ if ( $this->is_translation_editor_preview() || ! $this->gettext_runtime_status_migration_is_complete() ) {
+ $strings = $this->trp_query->get_all_gettext_strings( $language );
+ } else {
+ $strings = $this->trp_query->get_runtime_gettext_strings( $language );
+ }
if ( ! empty( $strings ) ) {
$trp_translated_gettext_texts = $strings;
$trp_strings = array();
@@ -115,6 +119,24 @@
$this->call_gettext_filters( 'woocommerce_' );
}
+ protected function is_translation_editor_preview() {
+ return isset( $_REQUEST['trp-edit-translation'] ) && $_REQUEST['trp-edit-translation'] === 'preview';
+ }
+
+ /**
+ * Return whether the gettext runtime status migration has completed.
+ *
+ * Upgraded sites temporarily fall back to loading all gettext rows until the
+ * full gettext optimization task has finished, including runtime-status
+ * classification.
+ *
+ * @return bool
+ */
+ protected function gettext_runtime_status_migration_is_complete() {
+ return get_option( 'trp_updated_database_gettext_tables_optimization', 'yes' ) === 'yes' &&
+ get_option( 'trp_updated_database_gettext_runtime_status_update', 'yes' ) !== 'no';
+ }
+
public function processing_gettext_is_needed() {
global $pagenow;
@@ -306,88 +328,285 @@
* function that machine translates gettext strings
*/
public function machine_translate_gettext() {
+ $this->flush_deferred_gettext_storage_and_mt();
+
/* @todo set the original language to detect and also decide if we automatically translate for the default language */
global $TRP_LANGUAGE, $trp_gettext_strings_for_machine_translation;
if ( ! empty( $trp_gettext_strings_for_machine_translation ) ) {
- if ( ! $this->machine_translator ) {
- $trp = TRP_Translate_Press::get_trp_instance();
- $this->machine_translator = $trp->get_component( 'machine_translator' );
+ $this->machine_translate_gettext_queue( $trp_gettext_strings_for_machine_translation, $TRP_LANGUAGE );
+ $trp_gettext_strings_for_machine_translation = array();
+ }
+ }
+
+ /**
+ * Resolve all gettext rows observed during the request and process deferred MT.
+ *
+ * Runtime misses are collected in memory and flushed in batches at shutdown
+ * to avoid one DB query per gettext string.
+ *
+ * @return void
+ */
+ protected function flush_deferred_gettext_storage_and_mt() {
+ $pending_storage = $this->process_gettext->get_pending_gettext_storage();
+ $pending_mt_candidates = $this->process_gettext->get_pending_gettext_mt_candidates();
+
+ if ( empty( $pending_storage ) && empty( $pending_mt_candidates ) ) {
+ return;
+ }
+
+ if ( ! $this->trp_query ) {
+ $trp = TRP_Translate_Press::get_trp_instance();
+ $this->trp_query = $trp->get_component( 'query' );
+ }
+
+ $chunk_size = max( 1, (int) apply_filters( 'trp_gettext_pending_storage_chunk_size', 250 ) );
+ $gettext_insert_update = $this->trp_query->get_query_component( 'gettext_insert_update' );
+
+ foreach ( $pending_storage as $language => $items ) {
+ if ( empty( $items ) || ! in_array( $language, $this->settings['translation-languages'] ) ) {
+ continue;
+ }
+
+ $resolved_rows = $this->resolve_pending_gettext_rows( $language, $items, $chunk_size );
+ $missing = array_diff_key( $items, $resolved_rows );
+
+ if ( ! empty( $missing ) ) {
+ foreach ( array_chunk( $missing, $chunk_size, true ) as $missing_chunk ) {
+ $gettext_insert_update->insert_gettext_strings( array_values( $missing_chunk ), $language );
+ }
+
+ $inserted_rows = $this->resolve_pending_gettext_rows( $language, $missing, $chunk_size );
+ $resolved_rows = array_replace( $resolved_rows, $inserted_rows );
+ }
+
+ $this->update_existing_gettext_rows_from_observed_translations( $language, $items, $resolved_rows );
+ $language_mt_candidates = isset( $pending_mt_candidates[ $language ] ) ? $pending_mt_candidates[ $language ] : array();
+ $mt_queue = $this->build_deferred_gettext_mt_queue( $language, $language_mt_candidates, $resolved_rows );
+
+ if ( ! empty( $mt_queue ) ) {
+ $this->machine_translate_gettext_queue( $mt_queue, $language );
}
+ }
- // Gettext strings are considered by default to be in the English language
- $source_language = apply_filters( 'trp_gettext_source_language', 'en_US', $TRP_LANGUAGE, array(), $trp_gettext_strings_for_machine_translation );
- // machine translate new strings
- if ( $this->machine_translator->is_available( array( $source_language, $TRP_LANGUAGE ) ) ) {
+ $this->process_gettext->clear_pending_gettext_buffers();
+ }
- /* Transform associative array into ordered numeric array. We need to keep keys numeric and ordered because $new_strings and $machine_strings depend on it.
- * Array was constructed as associative with db ids as keys to avoid duplication.
+ /**
+ * Resolve observed gettext items to existing DB rows in bounded chunks.
+ *
+ * @param string $language Target language code.
+ * @param array $items Observed gettext items keyed by storage key.
+ * @param int $chunk_size Number of items to resolve per DB query.
+ *
+ * @return array
*/
- $trp_gettext_strings_for_machine_translation = array_values( $trp_gettext_strings_for_machine_translation );
+ protected function resolve_pending_gettext_rows( $language, $items, $chunk_size ) {
+ $resolved_rows = array();
+
+ foreach ( array_chunk( $items, $chunk_size, true ) as $items_chunk ) {
+ $rows = $this->trp_query->get_gettext_rows_by_composite_keys( $language, array_values( $items_chunk ) );
+
+ foreach ( $rows as $row ) {
+ $key = $this->get_gettext_row_storage_key( $language, $row );
+
+ if ( $key ) {
+ if ( isset( $resolved_rows[ $key ] ) && ! empty( $resolved_rows[ $key ]['translated'] ) ) {
+ continue;
+ }
+
+ $resolved_rows[ $key ] = $row;
+ }
+ }
+ }
+
+ return $resolved_rows;
+ }
+
+ /**
+ * Build the request-local storage key for a gettext DB row.
+ *
+ * @param string $language Target language code.
+ * @param array $row Gettext row joined with the original gettext table.
+ *
+ * @return string
+ */
+ protected function get_gettext_row_storage_key( $language, $row ) {
+ $original = ! empty( $row['original'] ) ? $row['original'] : $row['tt_original'];
+ $domain = ! empty( $row['domain'] ) ? $row['domain'] : $row['tt_domain'];
+ $context = ! empty( $row['context'] ) ? $row['context'] : 'trp_context';
+ $plural_form = isset( $row['plural_form'] ) ? (int) $row['plural_form'] : 0;
+
+ if ( empty( $original ) || empty( $domain ) ) {
+ return '';
+ }
+
+ return $this->process_gettext->get_gettext_storage_key( $language, $context, $plural_form, $domain, $original );
+ }
+
+ /**
+ * Persist observed language-file translations into existing untranslated rows.
+ *
+ * This updates storage/status only. Language-file translations stay on the
+ * non-runtime gettext status because they should not override themselves.
+ *
+ * @param string $language Target language code.
+ * @param array $items Observed gettext items keyed by storage key.
+ * @param array $resolved_rows Existing DB rows keyed by storage key.
+ *
+ * @return void
+ */
+ protected function update_existing_gettext_rows_from_observed_translations( $language, $items, &$resolved_rows ) {
+ $updates = array();
+
+ foreach ( $items as $key => $item ) {
+ if ( empty( $resolved_rows[ $key ]['id'] ) || empty( $item['translated'] ) || ! empty( $resolved_rows[ $key ]['translated'] ) ) {
+ continue;
+ }
+
+ $updates[] = array(
+ 'id' => (int) $resolved_rows[ $key ]['id'],
+ 'translated' => $item['translated'],
+ 'status' => $this->trp_query->get_constant_gettext_translated_in_language_file(),
+ );
+
+ $resolved_rows[ $key ]['translated'] = $item['translated'];
+ $resolved_rows[ $key ]['status'] = $this->trp_query->get_constant_gettext_translated_in_language_file();
+ }
+
+ if ( ! empty( $updates ) ) {
+ $gettext_insert_update = $this->trp_query->get_query_component( 'gettext_insert_update' );
+ $gettext_insert_update->update_gettext_strings( $updates, $language, array( 'id', 'translated', 'status' ) );
+ }
+ }
+
+ /**
+ * Build an id-backed gettext machine translation queue after storage resolution.
+ *
+ * @param string $language Target language code.
+ * @param array $mt_candidates Observed MT candidates keyed by storage key.
+ * @param array $resolved_rows Existing DB rows keyed by storage key.
+ *
+ * @return array
+ */
+ protected function build_deferred_gettext_mt_queue( $language, $mt_candidates, $resolved_rows ) {
+ if ( empty( $mt_candidates ) ) {
+ return array();
+ }
+
+ $mt_queue = array();
+
+ foreach ( $mt_candidates as $key => $candidate ) {
+ if ( empty( $resolved_rows[ $key ]['id'] ) || ! empty( $resolved_rows[ $key ]['translated'] ) ) {
+ continue;
+ }
+ $db_id = (int) $resolved_rows[ $key ]['id'];
+ if ( isset( $mt_queue[ $db_id ] ) ) {
+ continue;
+ }
+
+ $mt_queue[ $db_id ] = array(
+ 'id' => $db_id,
+ 'original' => $candidate['original'],
+ 'translated' => '',
+ 'domain' => $candidate['domain'],
+ 'status' => $this->trp_query->get_constant_machine_translated(),
+ 'context' => $candidate['context'],
+ 'plural_form' => $candidate['plural_form'],
+ 'original_plural' => $candidate['original_plural'],
+ );
+ }
+
+ return $mt_queue;
+ }
+
+ /**
+ * Machine translate gettext rows and persist successful MT translations.
+ *
+ * @param array $gettext_queue Queue of gettext rows keyed by DB id or numeric index.
+ * @param string $language Target language code.
+ *
+ * @return void
+ */
+ protected function machine_translate_gettext_queue( $gettext_queue, $language ) {
+ if ( empty( $gettext_queue ) || empty( $language ) ) {
+ return;
+ }
+
+ if ( ! $this->machine_translator ) {
+ $trp = TRP_Translate_Press::get_trp_instance();
+ $this->machine_translator = $trp->get_component( 'machine_translator' );
+ }
+
+ // Gettext strings are considered by default to be in the English language.
+ $source_language = apply_filters( 'trp_gettext_source_language', 'en_US', $language, array(), $gettext_queue );
+ if ( ! $this->machine_translator->is_available( array( $source_language, $language ) ) ) {
+ return;
+ }
+
+ $gettext_queue = array_values( $gettext_queue );
$new_strings = array();
- foreach ( $trp_gettext_strings_for_machine_translation as $trp_gettext_string_for_machine_translation ) {
- $new_strings[] = ( $trp_gettext_string_for_machine_translation['original_plural'] && (int)$trp_gettext_string_for_machine_translation['plural_form'] > 0 ) ? $trp_gettext_string_for_machine_translation['original_plural'] : $trp_gettext_string_for_machine_translation['original'];
+ foreach ( $gettext_queue as $gettext_string ) {
+ $new_strings[] = ( $gettext_string['original_plural'] && (int)$gettext_string['plural_form'] > 0 ) ? $gettext_string['original_plural'] : $gettext_string['original'];
}
if ( ! $this->trp_query ) {
$trp = TRP_Translate_Press::get_trp_instance();
$this->trp_query = $trp->get_component( 'query' );
}
- $gettext_insert_update = $this->trp_query->get_query_component( 'gettext_insert_update' );
- if ( apply_filters( 'trp_gettext_allow_machine_translation', true, $source_language, $TRP_LANGUAGE, $new_strings, $trp_gettext_strings_for_machine_translation ) ) {
+ $gettext_insert_update = $this->trp_query->get_query_component( 'gettext_insert_update' );
- // Translate in chunks and save each chunk to the gettext table before requesting the
- // next one, so an aborted or overlapping page load never re-sends (and re-bills) a
- // chunk that was already translated and saved.
- //
- // Request-wide time budget shared with the regular/DOM path through the same global:
- // regular strings (translated while the page renders) use up the budget before
- // gettext strings (translated here, on shutdown), so regular strings are prioritised.
- // Whatever is left is translated on future page loads.
+ if ( apply_filters( 'trp_gettext_allow_machine_translation', true, $source_language, $language, $new_strings, $gettext_queue ) ) {
global $trp_machine_translation_deadline;
if ( ! isset( $trp_machine_translation_deadline ) ) {
$trp_machine_translation_deadline = microtime( true ) + apply_filters( 'trp_machine_translation_time_budget', 10 );
}
- // Deduplicate before chunking so a repeated string is sent to the engine, and billed,
- // only once.
- foreach ( array_chunk( array_unique( $new_strings ), $this->machine_translator->get_chunk_size(), true ) as $strings_chunk ) {
+ $unique_strings = array_values( array_unique( $new_strings ) );
+ foreach ( array_chunk( $unique_strings, $this->machine_translator->get_chunk_size() ) as $strings_chunk ) {
if ( microtime( true ) > $trp_machine_translation_deadline ) {
break;
}
- $chunk_machine_strings = $this->machine_translator->translate( $strings_chunk, $TRP_LANGUAGE, $source_language );
- if ( empty( $chunk_machine_strings ) ) {
+ $machine_strings = $this->machine_translator->translate( $strings_chunk, $language, $source_language );
+ if ( empty( $machine_strings ) ) {
continue;
}
- // map this chunk's translations back onto the gettext strings and save them
$strings_to_save = array();
- foreach ( $strings_chunk as $key => $new_string ) {
- if ( isset( $chunk_machine_strings[ $new_string ] ) && isset( $trp_gettext_strings_for_machine_translation[ $key ] ) ) {
- $trp_gettext_strings_for_machine_translation[ $key ]['translated'] = $chunk_machine_strings[ $new_string ];
- $strings_to_save[] = $trp_gettext_strings_for_machine_translation[ $key ];
+ foreach ( $new_strings as $key => $new_string ) {
+ if ( isset( $machine_strings[ $new_string ] ) ) {
+ $gettext_queue[ $key ]['translated'] = $machine_strings[ $new_string ];
+ $strings_to_save[] = $gettext_queue[ $key ];
}
}
+
if ( ! empty( $strings_to_save ) ) {
- $gettext_insert_update->update_gettext_strings( $strings_to_save, $TRP_LANGUAGE );
+ $gettext_insert_update->update_gettext_strings( $strings_to_save, $language, array( 'id', 'original', 'translated', 'domain', 'status', 'plural_form' ) );
}
}
- } else {
- // Custom engine via filter: not chunked here, so save once.
- $machine_strings = apply_filters( 'trp_gettext_machine_translate_strings', array(), $new_strings, $TRP_LANGUAGE, $trp_gettext_strings_for_machine_translation );
- if ( ! empty( $machine_strings ) ) {
- foreach ( $new_strings as $key => $new_string ) {
- if ( isset( $machine_strings[ $new_string ] ) && isset( $trp_gettext_strings_for_machine_translation[ $key ] ) ) {
- $trp_gettext_strings_for_machine_translation[ $key ]['translated'] = $machine_strings[ $new_string ];
- }
+ return;
}
- $gettext_insert_update->update_gettext_strings( $trp_gettext_strings_for_machine_translation, $TRP_LANGUAGE );
+
+ $machine_strings = apply_filters( 'trp_gettext_machine_translate_strings', array(), $new_strings, $language, $gettext_queue );
+ if ( empty( $machine_strings ) ) {
+ return;
}
+
+ foreach ( $new_strings as $key => $new_string ) {
+ if ( isset( $machine_strings[ $new_string ] ) ) {
+ $gettext_queue[ $key ]['translated'] = $machine_strings[ $new_string ];
}
}
+
+ $gettext_queue = array_filter( $gettext_queue, function( $gettext_string ) {
+ return ! empty( $gettext_string['translated'] );
+ } );
+
+ if ( ! empty( $gettext_queue ) ) {
+ $gettext_insert_update->update_gettext_strings( $gettext_queue, $language, array( 'id', 'original', 'translated', 'domain', 'status', 'plural_form' ) );
}
}
@@ -601,10 +820,10 @@
$translated = $trp_plural_forms->translate_plural( $current_string['original'], $current_string['original_plural'], $plural_form_i, $context, $translations );
if ( $translated && $translated != $current_string['original'] && $translated != $current_string['original_plural'] ) {
- $status = 2;
+ $status = $this->trp_query->get_constant_gettext_translated_in_language_file();
}else {
$translated = '';
- $status = 0;
+ $status = $this->trp_query->get_constant_not_translated();
}
if ( $plural_form_id_translation_table ) {
if ( $translated ) {
@@ -635,15 +854,15 @@
}
} else {
if ( $current_string['status'] == 0 && empty( $current_string['translated'] ) ) {
- $translated = $translations->translate( $current_string['original'] );
+ $translated = $translations->translate( $current_string['original'], $context );
}
}
if ( $current_string['status'] == 0 && empty( $current_string['translated'] ) ) {
if ( $translated && $translated != $current_string['original'] && $translated != $current_string['original_plural'] ) {
- $status = 2;
+ $status = $this->trp_query->get_constant_gettext_translated_in_language_file();
} else {
$translated = '';
- $status = 0;
+ $status = $this->trp_query->get_constant_not_translated();
}
if ( $current_string['id'] ) {
@@ -651,7 +870,7 @@
$update_gettext_strings[] = array(
'id' => $current_string['id'],
'translated' => $translated,
- 'status' => 2
+ 'status' => $this->trp_query->get_constant_gettext_translated_in_language_file()
);
}