This article covers the meta synchronization pipeline in WPEstate Translate 1.0.9 (text domain wpr-translate): how rules are loaded and normalized, which hooks drive synchronization and which safeguards prevent loops and stale writes. It is the companion to the user-facing Custom Field Rules article and part of the wider multi-language real estate website architecture.
Files Involved
| File | Role |
|---|---|
| includes/meta-sync.php | Runtime synchronization on save_post, meta change, delete and untrash hooks. |
| includes/custom-field-rules.php | Rule loader, first-run import, override storage, reload, state reporting. |
| includes/custom-field-rules-storage.php | Normalization, grouping and legacy rule conversion. |
| includes/custom-fields-sync.php | JSON config path, hash tracking, wpr_cf_preferences cache. |
| includes/admin/post-list-meta.php | Meta cloning when a translation is created (wpr_translation_clone_meta_fields()), copy-once markers. |
| includes/admin/views/settings-custom-fields.php | Custom Field Rules admin view. |
Behavior Keywords
Four keywords are accepted, normalized in wpr_translate_normalize_meta_field_behavior_value():
| Keyword | Effect |
|---|---|
| copy | Copied when a translation is created and kept in sync on every save / meta change of any family member. |
| copy-once | Copied only when the translation is created; tracked with the _wpr_cf_copied_once marker. |
| translate | Never propagated; an empty placeholder is seeded on the new translation. |
| ignore | Never touched. |
The legacy alias copy_once is rewritten to copy-once. Unknown values are dropped.
Rule Source of Truth
Defaults come from wpr/custom-fields-config.json, located by wpr_translate_locate_theme_wpr_file(): the child theme copy first, then the parent theme. wpr_translate_get_custom_field_rules_file_path() passes the result through the wpr_translate_custom_field_rules_file filter. wpr_translate_read_custom_field_rules_file() accepts a top-level array or an object with a custom_fields key. Each rule can carry:
{ "id": "property_price", "label": "Price", "description": "Property asking price", "action": "copy", "post": "estate_property" }
The key value is accepted as an alias for action.
First Load on Fresh Installs
When no defaults are stored yet, wpr_translate_get_custom_field_rules() reads the JSON file and persists it through wpr_translate_import_custom_field_rules(). Plugin activation calls the same getter, so a new site has the theme rules without pressing Reload from JSON File. The automatic import keeps existing overrides; only the Reload button resets them. If the file is missing nothing is written.
Normalization & Grouping
wpr_translate_normalize_custom_field_rules() sanitizes every entry, builds a unique uid per post type via wpr_translate_build_unique_rule_id(), and defaults the action to copy when missing. wpr_translate_group_custom_field_rules_by_post() groups rules by post type with estate_property first.
Options Used
| Option | Contents |
|---|---|
| wpr_cf_rules_defaults | Normalized default rules + file hash, path and last-loaded timestamp. |
| wpr_cf_rule_overrides | Admin-changed behaviors keyed by rule uid. |
| wpr_cf_file_state | Status / error payload for the JSON loader shown on the admin page. |
| wpr_cf_preferences | Secondary cache maintained by custom-fields-sync.php (hash + raw rules). |
Overrides are merged on top of defaults in wpr_translate_get_custom_field_rules(). Each merged rule carries source (file or override) and the original file_action.
Runtime Hooks
All registered in plugin-bootstrap.php:
| Hook | Callback | Purpose |
|---|---|---|
| save_post (20) | wpr_translate_handle_save_post_meta_sync() | Full sweep of every copy key from the saved post to its family. |
| added_post_meta / updated_post_meta / deleted_post_meta (20) | wpr_translate_handle_post_meta_change_sync() | Single-key sync (wpr_sync_single_meta_field()) for code that writes meta without saving the post. |
| before_delete_post (1) / deleted_post (999) | wpr_translate_meta_sync_mark_post_deleting() / _unmark_post_deleting() | Meta rows removed by wp_delete_post() are not treated as a sync source, so deleting one translation cannot empty copy fields on the others. |
| untrash_post (1) / untrashed_post (20) | wpr_translate_meta_sync_mark_post_untrashing() / wpr_translate_handle_untrashed_post_meta_sync() | The save_post fired by wp_untrash_post() is skipped; after restore, the post is synced from an available sibling so stale values are not pushed to the family. |
Both sync callbacks skip autosaves, revisions, auto-drafts and unregistered post types. The single-key handler also skips _wpr_language and _wpr_translations and only acts on copy keys. Both respect the opt-out filter:
apply_filters( 'wpr_translate_should_sync_post_meta', true, $post_id, $post_type, $post );
Sibling Resolution
wpr_translate_resolve_meta_sync_translation_post_ids( $post_id, $post_type ) returns the members of wpr_translate_relationship_get_post_family( $post_id ) (the shared relationship layer, cached per request). On a WP_Error it returns only the current post, so nothing is synced. Do not query siblings through a language-filtered WP_Query; use the family helper.
Full-Sweep Sync: wpr_sync_meta_fields()
$behaviors = wpr_translate_get_meta_field_behaviors();
$copy_keys = keys where behavior === 'copy';
$siblings = wpr_translate_resolve_meta_sync_translation_post_ids( $post_id, $post_type );
// stop if fewer than 2 members or the source is not available
foreach ( $siblings as $target ) { // skip source and unavailable targets
foreach ( $copy_keys as $meta_key ) {
$source = get_post_meta( $source_post_id, $meta_key, false );
$current = get_post_meta( $target, $meta_key, false );
if ( array_map( 'maybe_serialize', $source ) !== array_map( 'maybe_serialize', $current ) ) {
wpr_translate_meta_sync_enter_internal_write();
delete_post_meta( $target, $meta_key );
foreach ( $source as $v ) add_post_meta( $target, $meta_key, wp_slash( $v ) );
wpr_translate_meta_sync_exit_internal_write();
}
}
}
Values from get_post_meta() are already decoded and are not passed through maybe_unserialize() again, so a string that only looks serialized stays a string.
Meta Cloning on Translation Create
wpr_translation_clone_meta_fields( $source_post_id, $new_post_id, $excluded_meta_keys ) applies the rules when a translation is created:
- Always skipped:
_edit_lock,_edit_last,_wp_old_slug,_wp_desired_post_slug,_wp_trash_meta_*,_wpr_cf_copied_once,_wpr_language,_wpr_translations. Copying old-slug meta used to letwp_old_slug_redirect()send a default-language URL to the translation. - Builder payloads (
_elementor_data,_elementor_edit_mode,_elementor_version,_elementor_template_type, WPBakery keys) are always copied. - Elementor caches (
_elementor_css,_elementor_page_assets,_elementor_element_cache) are deleted on the new post so Elementor regenerates them. - Attachment fields are skipped when Media Synchronization is off.
Re-Entry Guards
- Per-request static guard keyed by
post_type:post_idinwpr_sync_meta_fields()andwpr_sync_single_meta_field(). - Internal-write depth counter via
wpr_translate_meta_sync_enter_internal_write()/_exit_internal_write(); the single-key handler bails whenwpr_translate_meta_sync_is_internal_write()is true. - Delete / untrash flags in
$GLOBALS(see Runtime Hooks). - Availability check via
wpr_translation_is_translation_post_available()– unavailable translations are skipped.
Extension Points
apply_filters( 'wpr_translate_should_sync_post_meta', $should, $post_id, $post_type, $post )– veto sync per post or post type.apply_filters( 'wpr_translate_meta_field_behaviors', $map )– last chance to change the behavior map.apply_filters( 'wpr_translate_custom_field_rules_file', $path )– override the JSON defaults file path.
Admin Page Wiring
The view receives $data['custom_field_rules'] (merged map) and $data['custom_field_state'] (reload status). The Reload button posts wpr_cf_action=reload with nonce wpr_cf_reload_nonce (action wpr_cf_reload); the handler calls wpr_translate_reload_custom_field_rules_from_file(), which deletes wpr_cf_rule_overrides and re-imports the file.
Dropdown changes are saved through wpr_translate_save_custom_field_overrides(); actions are validated against copy, translate, copy-once, ignore.
Legacy Import
wpr_translate_convert_legacy_custom_fields_to_rules() converts the WPResidence wp_estate_custom_fields theme option (indexed arrays, [0] name, [1] label, [2] type, [4] choices) into rules. wpr_translate_import_custom_field_rules() merges them in on every import, so theme-defined custom fields get defaults too.
Related Reading
- Translation Linking (trid system) – how sibling posts are linked.
- WP_Query Language Filtering – why sibling lookups must not go through language-filtered queries.
- Database Schema – the translation table used for legacy content.
For product context, visit the multi-language real estate website landing page.