507 lines
16 KiB
PHP
507 lines
16 KiB
PHP
<?php
|
|
|
|
use \WPML\FP\Fns;
|
|
use \WPML\TM\Jobs\FieldId;
|
|
use \WPML\FP\Logic;
|
|
use \WPML\FP\Lst;
|
|
use \WPML\FP\Either;
|
|
use \WPML\LIB\WP\Post;
|
|
use function \WPML\FP\curryN;
|
|
use function \WPML\FP\pipe;
|
|
use function \WPML\FP\invoke;
|
|
use WPML\TM\Jobs\Utils;
|
|
use WPML\FP\Relation;
|
|
|
|
/**
|
|
* Class WPML_Element_Translation_Package
|
|
*
|
|
* @package wpml-core
|
|
*/
|
|
class WPML_Element_Translation_Package extends WPML_Translation_Job_Helper {
|
|
|
|
/** @var WPML_WP_API $wp_api */
|
|
private $wp_api;
|
|
|
|
/**
|
|
* The constructor.
|
|
*
|
|
* @param WPML_WP_API $wp_api An instance of the WP API.
|
|
*/
|
|
public function __construct( WPML_WP_API $wp_api = null ) {
|
|
global $sitepress;
|
|
if ( $wp_api ) {
|
|
$this->wp_api = $wp_api;
|
|
} else {
|
|
$this->wp_api = $sitepress->get_wp_api();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create translation package
|
|
*
|
|
* @param \WPML_Package|\WP_Post|int $post
|
|
*
|
|
* @return array<string,string|array<string,string>>
|
|
*/
|
|
public function create_translation_package( $post ) {
|
|
|
|
$package = array();
|
|
$post = is_numeric( $post ) ? get_post( $post ) : $post;
|
|
if ( apply_filters( 'wpml_is_external', false, $post ) ) {
|
|
/** @var stdClass $post */
|
|
$post_contents = (array) $post->string_data;
|
|
$original_id = isset( $post->post_id ) ? $post->post_id : $post->ID;
|
|
$type = 'external';
|
|
|
|
if ( isset( $post->title ) ) {
|
|
$package['title'] = apply_filters( 'wpml_tm_external_translation_job_title', $post->title, $original_id );
|
|
}
|
|
|
|
if ( empty( $package['title'] ) ) {
|
|
$package['title'] = sprintf(
|
|
/* translators: The placeholder will be replaced with a number (an ID) */
|
|
__( 'External package ID: %d', 'wpml-translation-management' ),
|
|
$original_id
|
|
);
|
|
}
|
|
} else {
|
|
$home_url = get_home_url();
|
|
$package['url'] = htmlentities( $home_url . '?' . ( 'page' === $post->post_type ? 'page_id' : 'p' ) . '=' . ( $post->ID ) );
|
|
$package['title'] = $post->post_title;
|
|
|
|
$post_contents = array(
|
|
'title' => $post->post_title,
|
|
'body' => $post->post_content,
|
|
'excerpt' => $post->post_excerpt,
|
|
);
|
|
|
|
if ( wpml_get_setting_filter( false, 'translated_document_page_url' ) === 'translate' ) {
|
|
$post_contents['URL'] = $post->post_name;
|
|
}
|
|
|
|
$original_id = $post->ID;
|
|
|
|
$custom_fields_to_translate = \WPML\TM\Settings\Repository::getCustomFieldsToTranslate();
|
|
|
|
if ( ! empty( $custom_fields_to_translate ) ) {
|
|
$package = $this->add_custom_field_contents(
|
|
$package,
|
|
$post,
|
|
$custom_fields_to_translate,
|
|
$this->get_tm_setting( array( 'custom_fields_encoding' ) )
|
|
);
|
|
}
|
|
|
|
$post_contents = array_merge( $post_contents, $this->get_taxonomy_fields( $post ) );
|
|
$type = 'post';
|
|
}
|
|
$package['contents']['original_id'] = array(
|
|
'translate' => 0,
|
|
'data' => $original_id,
|
|
);
|
|
$package['type'] = $type;
|
|
|
|
$package['contents'] = $this->buildEntries( $package['contents'], $post_contents );
|
|
|
|
return apply_filters( 'wpml_tm_translation_job_data', $package, $post );
|
|
}
|
|
|
|
private function buildEntries( $contents, $entries, $parentKey = '' ) {
|
|
foreach ( $entries as $key => $entry ) {
|
|
$fullKey = $parentKey ? $parentKey . '_' . $key : $key;
|
|
|
|
if ( is_array( $entry ) ) {
|
|
$contents = $this->buildEntries( $contents, $entry, $fullKey );
|
|
} else {
|
|
$contents[ $fullKey ] = [
|
|
'translate' => 1,
|
|
'data' => base64_encode( $entry ),
|
|
'format' => 'base64',
|
|
];
|
|
}
|
|
}
|
|
|
|
return $contents;
|
|
}
|
|
|
|
/**
|
|
* @param array $translation_package
|
|
* @param int $job_id
|
|
* @param array $prev_translation
|
|
*/
|
|
public function save_package_to_job( array $translation_package, $job_id, $prev_translation ) {
|
|
global $wpdb;
|
|
|
|
$show = $wpdb->hide_errors();
|
|
|
|
foreach ( $translation_package['contents'] as $field => $value ) {
|
|
$job_translate = array(
|
|
'job_id' => $job_id,
|
|
'content_id' => 0,
|
|
'field_type' => $field,
|
|
'field_wrap_tag' => isset( $value['wrap_tag'] ) ? $value['wrap_tag'] : '',
|
|
'field_format' => isset( $value['format'] ) ? $value['format'] : '',
|
|
'field_translate' => $value['translate'],
|
|
'field_data' => $value['data'],
|
|
'field_data_translated' => '',
|
|
'field_finished' => 0,
|
|
);
|
|
|
|
if ( array_key_exists( $field, $prev_translation ) ) {
|
|
$job_translate['field_data_translated'] = $prev_translation[ $field ]->get_translation();
|
|
$job_translate['field_finished'] = $prev_translation[ $field ]->is_finished( $value['data'] );
|
|
}
|
|
|
|
$job_translate = $this->filter_non_translatable_fields( $job_translate );
|
|
|
|
$wpdb->insert( $wpdb->prefix . 'icl_translate', $job_translate );
|
|
}
|
|
|
|
$wpdb->show_errors( $show );
|
|
}
|
|
|
|
/**
|
|
* @param array $job_translate
|
|
*
|
|
* @return mixed|void
|
|
*/
|
|
private function filter_non_translatable_fields( $job_translate ) {
|
|
|
|
if ( $job_translate['field_translate'] ) {
|
|
$data = $job_translate['field_data'];
|
|
if ( 'base64' === $job_translate['field_format'] ) {
|
|
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
|
$data = base64_decode( $data );
|
|
}
|
|
$is_translatable = ! WPML_String_Functions::is_not_translatable( $data ) && apply_filters( 'wpml_translation_job_post_meta_value_translated', 1, $job_translate['field_type'] );
|
|
$is_translatable = (bool) apply_filters( 'wpml_tm_job_field_is_translatable', $is_translatable, $job_translate );
|
|
if ( ! $is_translatable ) {
|
|
$job_translate['field_translate'] = 0;
|
|
$job_translate['field_data_translated'] = $job_translate['field_data'];
|
|
$job_translate['field_finished'] = 1;
|
|
}
|
|
}
|
|
|
|
return $job_translate;
|
|
}
|
|
|
|
/**
|
|
* @param object $job
|
|
* @param int $post_id
|
|
* @param array $fields
|
|
*/
|
|
public function save_job_custom_fields( $job, $post_id, $fields ) {
|
|
$decode_translation = function ( $translation ) {
|
|
// always decode html entities eg decode & to &.
|
|
return html_entity_decode( str_replace( '�A;', "\n", $translation ) );
|
|
};
|
|
|
|
$get_field_id = function( $field_name, $el_data ) {
|
|
if (
|
|
strpos( $el_data->field_data, (string) $field_name ) === 0
|
|
&& 1 === preg_match( '/field-(.*?)-name/U', $el_data->field_type, $match )
|
|
&& 1 === preg_match( '/field-' . $field_name . '-[0-9].*?-name/', $el_data->field_type )
|
|
) {
|
|
return $match[1];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
$field_names = [];
|
|
|
|
foreach ( $fields as $field_name => $val ) {
|
|
if ( '' === (string) $field_name ) {
|
|
continue;
|
|
}
|
|
|
|
// find it in the translation.
|
|
foreach ( $job->elements as $el_data ) {
|
|
$field_id_string = $get_field_id( $field_name, $el_data );
|
|
if ( $field_id_string ) {
|
|
$field_names[ $field_name ] = isset( $field_names[ $field_name ] )
|
|
? $field_names[ $field_name ] : [];
|
|
|
|
$field_translation = false;
|
|
foreach ( $job->elements as $v ) {
|
|
if ( 'field-' . $field_id_string === $v->field_type ) {
|
|
$field_translation = $this->decode_field_data(
|
|
$v->field_data_translated,
|
|
$v->field_format
|
|
);
|
|
}
|
|
if ( 'field-' . $field_id_string . '-type' === $v->field_type ) {
|
|
$field_type = $v->field_data;
|
|
break;
|
|
}
|
|
}
|
|
if ( false !== $field_translation && isset( $field_type ) && 'custom_field' === $field_type ) {
|
|
$field_id_string = $this->remove_field_name_from_start( $field_name, $field_id_string );
|
|
$meta_keys = wpml_collect( explode( '-', $field_id_string ) )
|
|
->map( [ 'WPML_TM_Field_Type_Encoding', 'decode_hyphen' ] )
|
|
->prepend( $field_name )
|
|
->toArray();
|
|
$field_names = Utils::insertUnderKeys( $meta_keys, $field_names, $decode_translation( $field_translation ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->save_custom_field_values( $field_names, $post_id, $job->original_doc_id );
|
|
}
|
|
|
|
/**
|
|
* Remove the field from the start of the string.
|
|
*
|
|
* @param string $field_name The field to remove.
|
|
* @param string $field_id_string The full field identifier.
|
|
* @return string
|
|
*/
|
|
private function remove_field_name_from_start( $field_name, $field_id_string ) {
|
|
return preg_replace( '#' . $field_name . '-?#', '', $field_id_string, 1 );
|
|
}
|
|
|
|
/**
|
|
* @param array $fields_in_job
|
|
* @param int $post_id
|
|
* @param int $original_post_id
|
|
*/
|
|
private function save_custom_field_values( $fields_in_job, $post_id, $original_post_id ) {
|
|
$encodings = $this->get_tm_setting( array( 'custom_fields_encoding' ) );
|
|
foreach ( $fields_in_job as $name => $contents ) {
|
|
$this->wp_api->delete_post_meta( $post_id, $name );
|
|
|
|
$contents = (array) $contents;
|
|
$single = count( $contents ) === 1;
|
|
$encoding = isset( $encodings[ $name ] ) ? $encodings[ $name ] : '';
|
|
|
|
foreach ( $contents as $value ) {
|
|
|
|
$value = self::preserve_numerics( $value, $name, $original_post_id, $single );
|
|
$value = $encoding ? WPML_Encoding::encode( $value, $encoding ) : $value;
|
|
$value = apply_filters( 'wpml_encode_custom_field', $value, $name );
|
|
$value = $this->prevent_strip_slash_on_json( $value, $encoding );
|
|
|
|
$this->wp_api->add_post_meta( $post_id, $name, $value, $single );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The core function `add_post_meta` always performs
|
|
* a `stripslashes_deep` on the value. We need to escape
|
|
* once more before to call the function.
|
|
*
|
|
* @param string $value
|
|
* @param string $encoding
|
|
*
|
|
* @return string
|
|
*/
|
|
private function prevent_strip_slash_on_json( $value, $encoding ) {
|
|
if ( in_array( 'json', explode( ',', $encoding ), true ) ) {
|
|
$value = wp_slash( $value );
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param array $package
|
|
* @param object $post
|
|
* @param array $fields_to_translate
|
|
* @param array $fields_encoding
|
|
*
|
|
* @return array
|
|
*/
|
|
private function add_custom_field_contents( $package, $post, $fields_to_translate, $fields_encoding ) {
|
|
foreach ( $fields_to_translate as $key ) {
|
|
$encoding = isset( $fields_encoding[ $key ] ) ? $fields_encoding[ $key ] : '';
|
|
$custom_fields_values = array_values( array_filter( get_post_meta( $post->ID, $key ) ) );
|
|
foreach ( $custom_fields_values as $index => $custom_field_val ) {
|
|
$custom_field_val = apply_filters( 'wpml_decode_custom_field', $custom_field_val, $key );
|
|
$package = $this->add_single_field_content( $package, $key, array( $index ), $custom_field_val, $encoding );
|
|
}
|
|
}
|
|
|
|
return $package;
|
|
}
|
|
|
|
/**
|
|
* For array valued custom fields cf is given in the form field-{$field_name}-join('-', $indicies)
|
|
*
|
|
* @param array $package
|
|
* @param string $key
|
|
* @param array $custom_field_index
|
|
* @param array|stdClass|string $custom_field_val
|
|
* @param string $encoding
|
|
*
|
|
* @return array
|
|
*/
|
|
private function add_single_field_content( $package, $key, $custom_field_index, $custom_field_val, $encoding ) {
|
|
if ( $encoding && is_scalar( $custom_field_val ) ) {
|
|
$custom_field_val = WPML_Encoding::decode( $custom_field_val, $encoding );
|
|
$encoding = '';
|
|
}
|
|
if ( is_scalar( $custom_field_val ) ) {
|
|
list( $cf, $key_index ) = WPML_TM_Field_Type_Encoding::encode( $key, $custom_field_index );
|
|
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
|
$package['contents'][ $cf ] = array(
|
|
'translate' => 1,
|
|
'data' => base64_encode( $custom_field_val ),
|
|
'format' => 'base64',
|
|
);
|
|
$package['contents'][ $cf . '-name' ] = array(
|
|
'translate' => 0,
|
|
'data' => $key_index,
|
|
);
|
|
$package['contents'][ $cf . '-type' ] = array(
|
|
'translate' => 0,
|
|
'data' => 'custom_field',
|
|
);
|
|
|
|
} else {
|
|
foreach ( (array) $custom_field_val as $ind => $value ) {
|
|
$package = $this->add_single_field_content(
|
|
$package,
|
|
$key,
|
|
array_merge( $custom_field_index, array( $ind ) ),
|
|
$value,
|
|
$encoding
|
|
);
|
|
}
|
|
}
|
|
|
|
return $package;
|
|
}
|
|
|
|
/**
|
|
* Ensure that any numerics are preserved in the given value. eg any string like '10'
|
|
* will be converted to an integer if the corresponding original value was an integer.
|
|
*
|
|
* @param mixed $value
|
|
* @param string $name
|
|
* @param string|int $original_post_id
|
|
* @param bool $single
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public static function preserve_numerics( $value, $name, $original_post_id, $single ) {
|
|
$get_original = function () use ( $original_post_id, $name, $single ) {
|
|
$meta = get_post_meta( $original_post_id, $name, $single );
|
|
return apply_filters( 'wpml_decode_custom_field', $meta, $name );
|
|
};
|
|
if ( is_numeric( $value ) && is_int( $get_original() ) ) {
|
|
$value = intval( $value );
|
|
} elseif ( is_array( $value ) ) {
|
|
$value = self::preserve_numerics_recursive( $get_original(), $value );
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Ensure that any numerics are preserved in the given value. eg any string like '10'
|
|
* will be converted to an integer if the corresponding original value was an integer.
|
|
*
|
|
* @param mixed $original
|
|
* @param mixed $value
|
|
*
|
|
* @return mixed
|
|
*/
|
|
private static function preserve_numerics_recursive( $original, $value ) {
|
|
if ( is_array( $original ) ) {
|
|
foreach ( $original as $key => $data ) {
|
|
if ( isset( $value[ $key ] ) ) {
|
|
if ( is_array( $data ) ) {
|
|
$value[ $key ] = self::preserve_numerics_recursive( $data, $value[ $key ] );
|
|
} elseif ( is_int( $data ) && is_numeric( $value[ $key ] ) ) {
|
|
$value[ $key ] = intval( $value[ $key ] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
private function get_taxonomy_fields( $post ) {
|
|
global $sitepress;
|
|
|
|
$termMetaKeysToTranslate = self::getTermMetaKeysToTranslate();
|
|
|
|
// $getTermFields :: WP_Term → [[fieldId, fieldVal]]
|
|
$getTermFields = function ( $term ) {
|
|
return [
|
|
[ FieldId::forTerm( $term->term_taxonomy_id ), $term->name ],
|
|
[ FieldId::forTermDescription( $term->term_taxonomy_id ), $term->description ],
|
|
];
|
|
};
|
|
|
|
// $getTermMetaFields :: [metakeys] → WP_Term → [[fieldId, fieldVal]]
|
|
$getTermMetaFields = curryN(
|
|
2,
|
|
function ( $termMetaKeysToTranslate, $term ) {
|
|
|
|
// $getMeta :: int → string → object
|
|
$getMeta = curryN(
|
|
2,
|
|
function ( $termId, $key ) {
|
|
return (object) [
|
|
'id' => $termId,
|
|
'key' => $key,
|
|
'meta' => get_term_meta( $termId, $key ),
|
|
];
|
|
}
|
|
);
|
|
|
|
// $hasMeta :: object → bool
|
|
$hasMeta = function ( $termData ) {
|
|
return isset( $termData->meta[0] );
|
|
};
|
|
|
|
// $makeField :: object → [fieldId, $fieldVal]
|
|
$makeField = function ( $termData ) {
|
|
return [ FieldId::forTermMeta( $termData->id, $termData->key ), $termData->meta[0] ];
|
|
};
|
|
|
|
// $get :: [metakeys] → [[fieldId, $fieldVal]]
|
|
$get = pipe(
|
|
Fns::map( $getMeta( $term->term_taxonomy_id ) ),
|
|
Fns::filter( $hasMeta ),
|
|
Fns::map( $makeField )
|
|
);
|
|
|
|
return $get( $termMetaKeysToTranslate );
|
|
}
|
|
);
|
|
|
|
// $getAll :: [WP_Term] → [[fieldId, fieldVal]]
|
|
$getAll = Fns::converge( Lst::concat(), [ $getTermFields, $getTermMetaFields( $termMetaKeysToTranslate ) ] );
|
|
|
|
return wpml_collect( $sitepress->get_translatable_taxonomies( false, $post->post_type ) ) // [taxonomies]
|
|
->map( Post::getTerms( $post->ID ) ) // [Either false|WP_Error [WP_Term]]
|
|
->filter( Fns::isRight() ) // [Right[WP_Term]]
|
|
->map( invoke( 'get' ) ) // [[WP_Term]]
|
|
->flatten() // [WP_Term]
|
|
->map( $getAll ) // [[fieldId, fieldVal]]
|
|
->mapWithKeys( Lst::fromPairs() ) // [fieldId => fieldVal]
|
|
->toArray();
|
|
}
|
|
|
|
public static function getTermMetaKeysToTranslate() {
|
|
$fieldTranslation = new WPML_Custom_Field_Setting_Factory( self::get_core_translation_management() );
|
|
|
|
$settingsFactory = self::get_core_translation_management()->settings_factory();
|
|
|
|
$translatableMetaKeys = pipe(
|
|
[ $settingsFactory, 'term_meta_setting' ],
|
|
invoke( 'status' ),
|
|
Relation::equals( WPML_TRANSLATE_CUSTOM_FIELD )
|
|
);
|
|
|
|
return wpml_collect( $fieldTranslation->get_term_meta_keys() )
|
|
->filter( $translatableMetaKeys )
|
|
->values()
|
|
->toArray();
|
|
}
|
|
}
|