sitepress = $sitepress; $this->acfWorker = $acfWorker; } /** * Adds filters for displaying options page value and updating this value. */ public function register_hooks() { add_filter( 'acf/pre_render_fields', array( $this, 'fields_on_translated_options_page' ), 10, 2 ); add_filter( 'acf/update_value', array( $this, 'overwrite_option_value' ), 10, 4 ); add_filter( 'acf/validate_post_id', [ $this, 'append_language_code_for_option_pages' ] ); } /** * @return bool Tells if currently displayed page is ACF options page within wp-admin. */ public function is_acf_options_page() { $is = is_admin() && isset( $_GET['page'] ) && stristr( $_GET['page'], 'acf-options-' ) !== false; return $is; } /** * Updates fields values on display at ACF options page. * * @param array $fields All fields to be displayed on AF options page. * @param mixed $post_id Post id or string identifying current option page. * * @return array */ public function fields_on_translated_options_page( $fields, $post_id = 0 ) { foreach ( $fields as $key => $field ) { $fields[ $key ] = $this->get_field_options( $field, $post_id ); } return $fields; } /** * @param array $field * @param mixed $post_id * * @return array */ private function get_field_options( array $field, $post_id ) { if ( isset( $field['wpml_cf_preferences'] ) ) { switch ( $field['wpml_cf_preferences'] ) { case WPML_COPY_CUSTOM_FIELD: if ( $this->is_field_on_translated_options_page( $post_id ) ) { if ( $this->is_repeater_field( $field ) ) { $field['value'] = acf_get_value( self::ORIGINAL_ID, $field ); $field = $this->updateTranslatedSubfieldValue( $field ); } else { $field['value'] = $this->convert_relationship_field( acf_get_value( self::ORIGINAL_ID, $field ), $field ); $instructions = ''; if ( isset( $field['instructions'] ) ) { $instructions = $field['instructions'] . '
'; } $instructions .= __( 'This value will be always replaced with the original, because its translation preferences are set to copy.', 'acfml' ); $field['instructions'] = $instructions; } } break; case WPML_COPY_ONCE_CUSTOM_FIELD: if ( null === $field['value'] ) { // acf_get_value returns value or empty string if value not set yet. $field['value'] = $this->convert_relationship_field( acf_get_value( $post_id, $field ), $field ); } if ( empty( $field['value'] ) && $this->is_field_on_translated_options_page( $post_id ) ) { // so if value not set yet, get value from original language. $field['value'] = $this->convert_relationship_field( acf_get_value( self::ORIGINAL_ID, $field ), $field ); } break; } } return $field; } /** * @param array $field * * @return array */ private function updateTranslatedSubfieldValue( $field ) { if ( is_array( $field['value'] ) ) { foreach ( $field['value'] as $row => $pair ) { foreach ( $pair as $fieldHash => $fieldRealValue ) { $subfieldName = $this->getMatchingTranslatableSubFieldName( $field, $fieldHash ); if ( $subfieldName ) { $field['value'][ $row ][ $fieldHash ] = get_option( $this->getTranslatedSubfieldName( $field['name'], $row, $subfieldName ) ); } } } } return $field; } /** * @param array $field * @param string $fieldHash * * @return string|null */ private function getMatchingTranslatableSubFieldName( $field, $fieldHash ) { // $matchesHash :: array -> bool $matchesHash = Relation::propEq( 'key', $fieldHash ); // $isTranslatable :: array -> bool $isTranslatable = Relation::propEq( 'wpml_cf_preferences', WPML_TRANSLATE_CUSTOM_FIELD ); // $getName :: array -> string|null $getName = Obj::prop( 'name' ); return wpml_collect( Obj::propOr( [], 'sub_fields', $field ) ) ->filter( $matchesHash ) ->filter( $isTranslatable ) ->map( $getName ) ->first(); } /** * @param string $parentFieldName * @param int $row * @param string $subFieldName * * @return string */ private function getTranslatedSubfieldName( $parentFieldName, $row, $subFieldName ) { return sprintf( '%s_%s_%d_%s', sprintf( self::TRANSLATED_ID_FORMAT, $this->sitepress->get_current_language() ), $parentFieldName, $row, $subFieldName ); } /** * Updates options page's value during save to assure when field is set to Copy, it always stores default value. * * @param mixed $value The field's value. * @param mixed $post_id Post id or string identifying current option page. * @param array $field Field metadata. * @param string $original_value The field's value (again, the same as in first param). * * @return mixed */ public function overwrite_option_value( $value, $post_id = 0, $field = array(), $original_value = '' ) { if ( ! $this->is_repeater_field( $field ) && isset( $field['wpml_cf_preferences'] ) && WPML_COPY_CUSTOM_FIELD === $field['wpml_cf_preferences'] && $this->is_field_on_translated_options_page( $post_id ) ) { $value = $this->convert_relationship_field( acf_get_value( self::ORIGINAL_ID, $field ), $field ); } return $value; } /** * @param mixed $post_id Post id or string identifying current option page. * * @return bool */ private function is_field_on_translated_options_page( $post_id ) { $on_translated_page = false; if ( ! is_numeric( $post_id ) ) { $current_language = $this->sitepress->get_current_language(); $default_language = $this->sitepress->get_default_language(); $expected_post_id = sprintf( self::TRANSLATED_ID_FORMAT, $current_language ); $on_translated_page = $current_language !== $default_language && $expected_post_id === $post_id; } return $on_translated_page; } /** * If field has numeric relationship value, replace with translated version. * * @param mixed $value Current field value. * @param array $field ACF field. * * @return mixed */ private function convert_relationship_field( $value, $field ) { if ( is_numeric( $value ) ) { $current_language = $this->sitepress->get_current_language(); $data = $this->acfWorker->prepare_metadata( $value, $field['name'], self::ORIGINAL_ID, sprintf( self::TRANSLATED_ID_FORMAT, $current_language ) ); $value = $this->acfWorker->duplicate_post_meta( $value, $current_language, $data ); } return $value; } /** * Checks if given field has repeater field type. * * @param array $field ACF field data. * * @return bool */ private function is_repeater_field( $field ) { return isset( $field['type'] ) && 'repeater' === $field['type']; } /** * @param mixed $post_id * * @return mixed|string */ public function append_language_code_for_option_pages( $post_id ) { if ( is_string( $post_id ) && ! is_numeric( $post_id ) && ! $this->is_id_a_restricted_word( $post_id ) && ! $this->id_starts_with_options( $post_id ) && ! $this->is_term_id( $post_id ) && ! $this->is_taxonomy_name( $post_id ) && ! $this->is_block_id( $post_id ) && ! $this->id_starts_with_user( $post_id ) ) { $cl = acf_get_setting('current_language'); $dl = acf_get_setting('default_language'); if( ! $this->id_ends_with_language_code( $post_id, $cl ) && $cl !== $dl ) { $post_id .= '_' . $cl; } } return $post_id; } /** * @param mixed $post_id * * @return bool */ private function id_starts_with_options( $post_id ) {; return 'options' === substr( $post_id, 0, 7 ); } /** * @param string $post_id * * @return bool */ private function is_block_id( $post_id ) { return 'block_' === substr( $post_id, 0, 6 ); } /** * @param mixed $post_id * * @return bool */ private function id_starts_with_user( $post_id ) { return (bool) Str::startsWith( 'user_', $post_id ); } /** * @param mixed $post_id * @param string $language_code * * @return bool */ private function id_ends_with_language_code( $post_id, $language_code ) { return '_' . $language_code === substr( $post_id, -3 ); } /** * @param mixed $post_id * * @return bool */ private function is_term_id( $post_id ) { return 'term_' === substr( $post_id, 0, 5 ); } /** * @param string $post_id * * @return bool */ private function is_taxonomy_name( $post_id ) { $taxonomies = get_taxonomies( [], 'names' ); foreach ( $taxonomies as $taxonomy_name ) { if ( (bool) Str::startsWith( sprintf( '%s_', $taxonomy_name ), $post_id ) ) { return true; } } return false; } /** * @param string $post_id * * @return bool */ private function is_id_a_restricted_word( $post_id ) { // I am using here array values only to describe why a key is a restricted word. It is kind of a comment // plus it is faster to lookup array keys than values. $restricted = [ 'new_post' => 'The post id is new_post so it is fake id used in acf_form function when creating new post with ACF fields.' ]; return array_key_exists( $post_id, $restricted ); } }