File: /var/www/indoadvisory/wp/wp-content/plugins/polylang-pro/integrations/acf/acf-auto-translate.php
<?php
/**
* @package Polylang-Pro
*/
/**
* This class is part of the ACF compatibility.
* Manages the automatic translation of posts and terms in custom fields.
*
* @since 2.7
*/
class PLL_ACF_Auto_Translate {
/**
* ACF fields storage, used to remember which fields are currently handled.
*
* @var array
*/
private $fields;
/**
* Constructor.
* Setups actions and filters.
*
* @since 2.7
*/
public function __construct() {
add_filter( 'acf/update_value', array( $this, 'store_updated_field' ), 10, 3 );
add_filter( 'acf/delete_value', array( $this, 'store_updated_field' ), 10, 3 );
add_action( 'pll_save_term', array( $this, 'store_term_fields' ), 5 ); // Before PLL_Sync_Metas.
add_action( 'pll_duplicate_term', array( $this, 'store_term_fields' ), 5 ); // Before PLL_Sync_Metas.
add_filter( 'acf/load_value', array( $this, 'load_value' ), 10, 3 );
add_filter( 'acf/load_value/type=repeater', array( $this, 'load_value' ), 20, 3 );
add_filter( 'acf/load_value/type=flexible_content', array( $this, 'load_value' ), 20, 3 );
add_filter( 'pll_translate_post_meta', array( $this, 'translate_meta' ), 10, 5 );
add_filter( 'pll_translate_term_meta', array( $this, 'translate_meta' ), 10, 4 );
}
/**
* Stores updated or deleted fields for future usage.
*
* @since 2.3
*
* @param mixed $value Not used.
* @param mixed $post_id Not used.
* @param array $field Custom field.
* @return mixed Unmodified custom field value.
*/
public function store_updated_field( $value, $post_id, $field ) {
$this->fields[ $field['name'] ] = $field;
return $value;
}
/**
* Store fields when saving a term or when duplicating a term.
*
* @since 2.3
*
* @param int $term_id Id of the term being saved.
* @return void
*/
public function store_term_fields( $term_id ) {
$this->fields = get_field_objects( 'term_' . $term_id );
}
/**
* Copies and possibly translates custom fields when creating a new term translation.
*
* @since 2.2
*
* @param mixed $value Custom field value of the source term.
* @param string $post_id Expects term_{$term_id} for a term.
* @param array $field Custom field.
* @return mixed
*/
public function load_value( $value, $post_id, $field ) {
if ( 'term_0' === $post_id && isset( $_GET['taxonomy'], $_GET['from_tag'], $_GET['new_lang'] ) && taxonomy_exists( sanitize_key( $_GET['taxonomy'] ) ) && $lang = PLL()->model->get_language( sanitize_key( $_GET['new_lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$from_tag = (int) $_GET['from_tag']; // phpcs:ignore WordPress.Security.NonceVerification
$tr_id = 'term_' . $from_tag; // Converts to ACF internal id.
$fields = get_field_objects( $tr_id );
if ( ! empty( $fields ) ) {
$keys = array_keys( $fields );
/** This filter is documented in /polylang/modules/sync/admin-sync.php */
$keys = array_unique( apply_filters( 'pll_copy_term_metas', $keys, false, $from_tag, 0, $lang->slug ) );
// Second test to load the values of subfields of accepted fields.
if ( in_array( $field['name'], $keys ) || preg_match( '#^(' . implode( '|', $keys ) . ')_(.+)#', $field['name'] ) ) {
$value = acf_get_value( $tr_id, $field ); // Since ACF 5.0.0.
$empty = null; // Parameter 1 is useless in this context.
$value = $this->translate_fields( $empty, $value, $field['name'], $field, $lang->slug );
if ( pll_is_translated_post_type( 'acf-field-group' ) ) {
$references = $this->translate_fields_references( $tr_id, $lang->slug );
$this->translate_references_in_value( $value, $references );
}
}
}
}
return $value;
}
/**
* Translates a custom field before it is copied or synchronized.
*
* @since 2.3
* @since 2.4 Added parameter $to.
*
* @param mixed $value Meta value.
* @param string $key Meta key.
* @param string $lang Language of target.
* @param int $from Id of the object from which we copy informations.
* @param int $to Id of the object to which we copy informations.
* @return mixed
*/
public function translate_meta( $value, $key, $lang, $from, $to = 0 ) {
if ( ! empty( $value ) && $field = isset( $this->fields[ $key ] ) ? $this->fields[ $key ] : $this->get_field_object( $key, $from ) ) {
$create_if_not_exists = false;
// Check if we should create translations if they don't exist.
// $to is not empty only when translating posts.
if ( ! empty( $to ) && ( $post_type = get_post_type( $to ) ) && pll_is_translated_post_type( $post_type ) ) {
$duplicate_options = get_user_meta( get_current_user_id(), 'pll_duplicate_content', true );
$active = ! empty( $duplicate_options ) && ! empty( $duplicate_options[ $post_type ] );
$create_if_not_exists = $active || PLL()->sync_post_model->are_synchronized( $from, $to );
}
$value = $this->translate_field( $value, $lang, $field, $create_if_not_exists );
}
if ( pll_is_translated_post_type( 'acf-field-group' ) && is_string( $value ) && acf_is_field_key( $value ) ) {
$references = $this->translate_fields_references( $from, $lang );
if ( isset( $references[ $value ] ) ) {
$value = $references[ $value ];
}
}
return $value;
}
/**
* Returns an array containing all the field data for a given field name.
* Unlike the original ACF function, it works for clone fields.
*
* @since 2.6.2
*
* @param string $key The field name or key.
* @param string|int $post_id The post_id of which the value is saved against.
* @return array|false
*/
protected function get_field_object( $key, $post_id ) {
$field = get_field_object( $key, $post_id );
if ( $field ) {
return $field;
}
$post_id = acf_get_valid_post_id( $post_id );
$field_key = acf_get_reference( $key, $post_id ); // Since ACF 5.6.5.
if ( ! is_string( $field_key ) ) {
return false;
}
$field_key = substr( $field_key, -19 ); // Keep the last key in field_xxx_field_yyy for clone fields.
if ( ! acf_is_field_key( $field_key ) ) {
return false;
}
$field = acf_get_field( $field_key );
if ( empty( $field ) ) {
return false;
}
$field['value'] = acf_get_value( $post_id, $field );
$field['value'] = acf_format_value( $field['value'], $post_id, $field );
return $field;
}
/**
* Translates a CPT archive link in a page link field.
*
* @since 2.3.6
*
* @param string $link CPT archive link.
* @param string $lang Language slug.
* @return string Modified link.
*/
protected function translate_cpt_archive_link( $link, $lang ) {
$lang = PLL()->model->get_language( $lang );
$link = PLL()->links_model->switch_language_in_link( $link, $lang );
foreach ( array_keys( PLL()->translate_slugs->slugs_model->get_translatable_slugs() ) as $type ) {
// Unfortunately ACF does not pass the post type, so let's try with all post type archives.
if ( 0 === strpos( $type, 'archive_' ) ) {
$link = PLL()->translate_slugs->slugs_model->switch_translated_slug( $link, $lang, $type );
}
}
return $link;
}
/**
* Translates a custom field value.
*
* @since 2.3
* @since 2.4 Added parameter $create_if_not_exists.
*
* @param mixed $value Custom field value.
* @param string $lang Language slug.
* @param array $field Custom field.
* @param bool $create_if_not_exists Should we create the translation if it does not exist.
* @return mixed
*/
protected function translate_field( $value, $lang, $field, $create_if_not_exists = false ) {
$return = $value;
switch ( $field['type'] ) {
case 'image':
case 'file':
if ( PLL()->options['media_support'] ) {
$return = 0;
if ( $tr_id = pll_get_post( $value, $lang ) ) {
$return = $tr_id;
} elseif ( $create_if_not_exists ) {
$return = PLL()->posts->create_media_translation( $value, $lang );
}
}
break;
case 'gallery':
if ( PLL()->options['media_support'] && is_array( $value ) ) {
$return = array();
foreach ( $value as $img ) {
if ( $tr_id = pll_get_post( $img, $lang ) ) {
$return[] = $tr_id;
} elseif ( $create_if_not_exists ) {
$return[] = PLL()->posts->create_media_translation( $img, $lang );
}
}
$return = array_map( 'strval', $return ); // See acf_field_gallery::update_value().
}
break;
case 'post_object':
case 'relationship':
if ( is_numeric( $value ) ) {
$return = 0;
if ( $tr_id = pll_get_post( $value, $lang ) ) {
$return = $tr_id;
}
} elseif ( is_array( $value ) ) {
$return = array();
foreach ( $value as $p ) {
if ( $tr_id = pll_get_post( $p, $lang ) ) {
$return[] = $tr_id;
}
}
$return = array_map( 'strval', $return ); // See the method update_value() for these fields.
}
break;
case 'page_link':
if ( is_numeric( $value ) ) {
// Unique translated post.
$return = 0;
if ( $tr_id = pll_get_post( $value, $lang ) ) {
$return = $tr_id;
}
} elseif ( is_array( $value ) ) {
// Multiple choice.
$return = array();
foreach ( $value as $p ) {
if ( is_numeric( $p ) && $tr_id = pll_get_post( $p, $lang ) ) {
$return[] = $tr_id;
} else {
$return[] = $this->translate_cpt_archive_link( $p, $lang ); // Archive.
}
}
$return = array_map( 'strval', $return ); // See acf_field_page_link::update_value().
} else {
$return = $this->translate_cpt_archive_link( $value, $lang ); // Archive.
}
break;
case 'taxonomy':
if ( pll_is_translated_taxonomy( $field['taxonomy'] ) ) {
if ( is_numeric( $value ) ) {
$return = 0;
if ( $tr_id = pll_get_term( $value, $lang ) ) {
$return = $tr_id;
}
} elseif ( is_array( $value ) ) {
$return = array();
foreach ( $value as $t ) {
if ( $tr_id = pll_get_term( $t, $lang ) ) {
$return[] = $tr_id;
}
}
}
}
break;
}
return $return;
}
/**
* Translates repeater and flexible content sub fields (for recursive translation of this fields).
*
* @since 2.2
*
* @param array $r Reference to a flat list of translated custom fields.
* @param mixed $value Custom field value.
* @param string $name Custom field name.
* @param array $field ACF field or subfield.
* @param string $lang Language slug.
* @return array Hierarchical list of custom fields values.
*/
protected function translate_sub_fields( &$r, $value, $name, $field, $lang ) {
$return = array();
foreach ( $value as $row => $sub_fields ) {
$sub = array();
foreach ( $sub_fields as $id => $sub_value ) {
$field = acf_get_field( substr( $id, -19 ) ); // Keep the last key in field_xxx_field_yyy for clone fields.
if ( $field ) {
$sub[ $id ] = $this->translate_fields( $r, $sub_value, $name . '_' . $row . '_' . $field['name'], $field, $lang );
} else {
$sub[ $id ] = $sub_value;
}
}
$return[] = $sub;
}
return $return;
}
/**
* Recursively translates custom group, repeater and flexible content fields.
*
* @since 2.0
*
* @param array $r Reference to a flat list of translated custom fields.
* @param mixed $value Custom field value.
* @param string $name Custom field name.
* @param array $field ACF field or subfield.
* @param string $lang Language slug.
* @return array Hierarchical list of custom fields values.
*/
protected function translate_fields( &$r, $value, $name, $field, $lang ) {
if ( empty( $value ) ) {
return;
}
$r[ '_' . $name ] = $field['key'];
$return = array();
switch ( $field['type'] ) {
case 'group':
$sub = array();
foreach ( $value as $id => $sub_value ) {
if ( $field = acf_get_field( $id ) ) {
$sub[ $id ] = $this->translate_fields( $r, $sub_value, $name . '_' . $field['name'], $field, $lang );
} else {
$sub[ $id ] = $sub_value;
}
}
$return[] = $sub;
break;
case 'repeater':
case 'flexible_content':
$return = $this->translate_sub_fields( $r, $value, $name, $field, $lang );
break;
default:
$return = $this->translate_field( $value, $lang, $field );
break;
}
return empty( $return ) ? $value : $return;
}
/**
* Translated field groups:
* Recursively translates the references in value for repeaters and flexible content.
*
* @since 2.2
*
* @param array $value Reference to a custom field value.
* @param array $references List of custom fields references with source as key and translation as value.
* @return void
*/
protected function translate_references_in_value( &$value, $references ) {
if ( is_array( $value ) ) {
foreach ( $value as $row => $sub_fields ) {
if ( is_array( $sub_fields ) ) {
foreach ( $sub_fields as $id => $sub_value ) {
if ( is_array( $sub_value ) ) {
$this->translate_references_in_value( $sub_value, $references );
}
if ( isset( $references[ $id ] ) ) {
$value[ $row ][ $references[ $id ] ] = $sub_value;
unset( $value[ $row ][ $id ] );
}
}
}
}
}
}
/**
* Translated field groups:
* Searches for fields having the same name in translated posts.
*
* @since 2.2
*
* @param int|string $from Source post id.
* @param string $lang Target language code.
* @return array
*/
protected function translate_fields_references( $from, $lang ) {
$keys = array();
$fields = get_field_objects( $from );
if ( is_array( $fields ) ) {
foreach ( $fields as $field ) {
if ( $tr_group = pll_get_post( $field['parent'], $lang ) ) {
$tr_fields = acf_get_fields( $tr_group );
$this->translate_field_references( $keys, $field, $tr_fields );
}
}
}
return $keys;
}
/**
* Translated field groups:
* Loops through sub fields in the recursive search for fields
* having the same name among translated fields groups.
*
* @since 2.2
*
* @param array $keys Reference to an array mapping the fields keys of the translated post to the field keys of the currentpost.
* @param array $fields ACF Custom fields of the current post.
* @param array $tr_fields ACF Custom fields of a translation.
* @return void
*/
protected function translate_sub_fields_references( &$keys, $fields, $tr_fields ) {
foreach ( $fields as $field ) {
$this->translate_field_references( $keys, $field, $tr_fields );
}
}
/**
* Translated field groups:
* Recursively searches for fields having the same name among translated fields groups.
*
* @since 2.2
*
* @param array $keys Reference to an array mapping the fields keys of the translated post to the field keys of the currentpost.
* @param array $field ACF Custom fields of the current post.
* @param array $tr_fields ACF Custom fields of a translation.
* @return void
*/
protected function translate_field_references( &$keys, $field, $tr_fields ) {
$k = array_search( $field['name'], wp_list_pluck( $tr_fields, 'name' ) );
if ( false !== $k ) {
$keys[ $field['key'] ] = $tr_fields[ $k ]['key'];
if ( ! empty( $field['sub_fields'] ) ) {
$this->translate_sub_fields_references( $keys, $field['sub_fields'], $tr_fields[ $k ]['sub_fields'] );
}
if ( ! empty( $field['layouts'] ) ) {
foreach ( $field['layouts'] as $row => $layout ) {
if ( ! empty( $layout['sub_fields'] ) ) {
$this->translate_sub_fields_references( $keys, $layout['sub_fields'], $tr_fields[ $k ]['layouts'][ $row ]['sub_fields'] );
}
}
}
}
}
}