Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ACF - Set fields as translatable #113

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
214 changes: 213 additions & 1 deletion lib/Integrations/AdvancedCustomFieldsPro.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

/**
Expand All @@ -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;
}
}
18 changes: 17 additions & 1 deletion lib/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know if it's an overkill for now, but it seems that we might need other situations for this and we could structure things a bit better in an array that indicates when to run each integration.

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',
Expand All @@ -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 ) ) {
Expand Down