diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2945c..67e29eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Check for empty transient value when fetching term language. - Constant `UBB_SETTINGS_READONLY` for read only settings in the backoffice settings page. - Internal option `ubb_settings_manual_changes` for keeping track if manual changes have been made to the settings. +- Setting Advanced Custom Fields fields as translatable automatically when they are registered. ### Changed - Improved get post language handling of empty values. - Options in options page are now modifiable from the values set in the `ubb_options` filter. - API rest url when Unbabble is set to directory routing no longer has the language directory applied. +- Registering Advanced Custom Fields integration hooks immediately on Unbabble register instead of waiting for `admin_init`. ### Fixed diff --git a/lib/Integrations/AdvancedCustomFieldsPro.php b/lib/Integrations/AdvancedCustomFieldsPro.php index 0428c77..3b38e55 100644 --- a/lib/Integrations/AdvancedCustomFieldsPro.php +++ b/lib/Integrations/AdvancedCustomFieldsPro.php @@ -2,11 +2,12 @@ namespace TwentySixB\WP\Plugin\Unbabble\Integrations; -use TwentySixB\WP\Plugin\Unbabble\Options; +use TwentySixB\WP\Plugin\Unbabble\LangInterface; class AdvancedCustomFieldsPro { public function register() { add_filter( 'option_acf_pro_license', [ $this, 'change_home_url' ] ); + add_filter( 'acf/prepare_fields_for_import', [ $this, 'set_fields_as_translatable' ], PHP_INT_MAX - 10, 1 ); } /** @@ -28,4 +29,215 @@ public function change_home_url( $pro_license ) { $decoded['url'] = get_home_url(); return base64_encode( maybe_serialize( $decoded ) ); } + + /** + * Sets ACF fields as translatable according to their type. + * + * @since Unreleased + * + * @param array $fields + * @param string $prefix + * + * @return array + */ + public function set_fields_as_translatable( array $fields, string $prefix = '' ) : array { + $field_keys = []; + + /** + * Store layouts for flexible content fields. Some layout types are not stored inside + * the flexible content field, but in a separate entry. This is the case for the block + * layout type. + */ + $layouts_to_look_for = []; + + // Loop all the fields being imported. + foreach ( $fields as $field ) { + $prefix_value = $prefix; + + /** + * Ignore sub fields since we deal with top fields only (aggregators included), + * unless they are a flexible content layout. + */ + if ( + empty( $prefix ) + && ( + ! isset( $field['parent_layout'] ) + || ! isset( $layouts_to_look_for[ $field['parent_layout'] ] ) + ) + && ( + ! isset( $field['parent' ] ) + || str_starts_with( $field['parent'], 'field_' ) + ) + ) { + continue; + } + + // If the field is a layout, fetch the prefix value. + if ( isset( $field['parent_layout'] ) ) { + $prefix_value = $layouts_to_look_for[ $field['parent_layout'] ]; + } + + // If the field is a repeater, check it subfields and add the prefix value. + if ( $field['type'] === 'repeater' ) { + $this->set_fields_as_translatable( $field['sub_fields'] ?? [], $prefix_value . $field['name'] . '_%_' ); + continue; + } + + // If the field is a block or a group, check it subfields and add the prefix value. + if ( $field['type'] === 'block' || $field['type'] === 'group' ) { + $this->set_fields_as_translatable( $field['sub_fields'] ?? [], $prefix_value . $field['name'] . '_' ); + continue; + } + + // If the field is a flexible content, add its layouts to the variable with the correct prefix value to be checked later. + if ( $field['type'] === 'flexible_content' ) { + foreach ( $field['layouts'] ?? [] as $layout ) { + $layouts_to_look_for[ $layout['key'] ] = $prefix_value . $field['name'] . '_%_'; + } + continue; + } + + // Check for field types that have post/term identifiers, and if they are translatable. + $object_type = null; + switch ( $field['type'] ) { + // case 'page_link': TODO: can contain ids but also archive urls so can break unbabble. + case 'relationship': + $object_type = $this->check_relationship( $field ); + break; + case 'post_object': + $object_type = $this->check_post_object( $field ); + break; + case 'image': + $object_type = $this->check_image( $field ); + break; + case 'file': + $object_type = $this->check_file( $field ); + break; + case 'gallery': + $object_type = $this->check_gallery( $field ); + break; + case 'taxonomy': + $object_type = $this->check_taxonomy( $field ); + break; + } + + // If the field is not translatable, skip it. + if ( empty( $object_type ) ) { + continue; + } + + // Add to the field keys array. + $field_keys[ $prefix_value . $field['name'] ] = $object_type; + } + + // Register the field keys to be translated. + if ( ! empty( $field_keys ) ) { + add_filter( 'ubb_change_language_post_meta_translate_keys', fn( $meta_keys ) => array_merge( $meta_keys, $field_keys ) ); + add_filter( 'ubb_yoast_duplicate_post_meta_translate_keys', fn( $meta_keys ) => array_merge( $meta_keys, $field_keys ) ); + } + + return $fields; + } + + /** + * Check if a relationship field is translatable. + * + * @since Unreleased + * + * @param array $field + * + * @return string|null + */ + private function check_relationship( array $field ) : ?string { + // TODO: How does ACF handle when only some of these are translatable? + foreach ( $field['post_type'] as $post_types ) { + if ( LangInterface::is_post_type_translatable( $post_types ) ) { + return 'post'; + } + } + return null; + } + + /** + * Check if a post object field is translatable. + * + * @since Unreleased + * + * @param array $field + * + * @return string|null + */ + private function check_post_object( array $field ) : ?string { + // TODO: How does ACF handle when only some of these are translatable? + foreach ( $field['post_type'] as $post_type ) { + if ( LangInterface::is_post_type_translatable( $post_type ) ) { + return 'post'; + } + } + return null; + } + + /** + * Check if an image field is translatable. + * + * @since Unreleased + * + * @param array $field + * + * @return string|null + */ + private function check_image( array $field ) : ?string { + if ( LangInterface::is_post_type_translatable( 'attachment' ) ) { + return 'post'; + } + return null; + } + + /** + * Check if a file field is translatable. + * + * @since Unreleased + * + * @param array $field + * + * @return string|null + */ + private function check_file( array $field ) : ?string { + if ( LangInterface::is_post_type_translatable( 'attachment' ) ) { + return 'post'; + } + return null; + } + + /** + * Check if a gallery field is translatable. + * + * @since Unreleased + * + * @param array $field + * + * @return string|null + */ + private function check_gallery( array $field ) : ?string { + if ( LangInterface::is_post_type_translatable( 'attachment' ) ) { + return 'post'; + } + return null; + } + + /** + * Check if a taxonomy field is translatable. + * + * @since Unreleased + * + * @param array $field + * + * @return string|null + */ + private function check_taxonomy( array $field ) : ?string { + if ( LangInterface::is_taxonomy_translatable( $field['taxonomy'] ) ) { + return 'term'; + } + return null; + } } diff --git a/lib/Plugin.php b/lib/Plugin.php index d29bb0c..87e9fcc 100644 --- a/lib/Plugin.php +++ b/lib/Plugin.php @@ -230,11 +230,21 @@ private function define_commands() : void { } ); } + /** + * Define the integrations with other plugins. + * + * @since Unreleased Register ACF integration immediatly for field registration. + * @since 0.0.1 + * + * @return void + */ private function define_integrations() : void { $this->define_integration_migrators(); + $immediate_integrations = [ + AdvancedCustomFieldsPro::class => 'advanced-custom-fields-pro/acf.php', + ]; $admin_integrations = [ YoastDuplicatePost::class => 'duplicate-post/duplicate-post.php', - AdvancedCustomFieldsPro::class => 'advanced-custom-fields-pro/acf.php', ]; $integrations = [ Relevanssi::class => 'relevanssi/relevanssi.php', @@ -243,6 +253,12 @@ private function define_integrations() : void { SearchWP::class => 'searchwp/index.php', ]; + foreach ( $immediate_integrations as $integration_class => $plugin_name ) { + if ( \is_plugin_active( $plugin_name ) ) { + ( new $integration_class() )->register(); + } + } + \add_action( 'admin_init', function() use ( $admin_integrations ) { foreach ( $admin_integrations as $integration_class => $plugin_name ) { if ( \is_plugin_active( $plugin_name ) ) {