This article documents how WPEstate Translate 1.0.9 attaches languages to WordPress nav menus, stores per-language theme-location assignments and synchronizes menu structure. It is the implementation-level companion to the user article. Product context is on our multi-language real estate website page.
File Map
| File | Role |
|---|---|
| includes/nav-menu-assignments-storage.php | Theme-scoped storage envelope, normalizer, read/write of the assignment map. |
| includes/nav-menu-assignments-read.php | Side-effect-free resolution of one location + language slot. |
| includes/nav-menu-assignments-commands.php | Validated, atomic save commands and the core nav_menu_locations projection. |
| includes/nav-menu-assignments-language.php | Changing a menu’s language and moving the assignments that reference it. |
| includes/nav-menu-assignments-default-language.php | Reactions to default language changes and removed languages. |
| includes/nav-menu-assignments-all-themes.php | Atomic writes across every stored theme map. |
| includes/nav-menu-locations.php | Public wrappers (kept for compatibility) and the core hooks. |
| includes/nav-menu-locations-cleanup.php | Removes deleted menus from stored assignments. |
| includes/nav-menu-locations-frontend.php | Front-end fallback and language switcher injection into menus. |
| includes/nav-menu-language-request.php | Requested language for front-end menu reads. |
| includes/nav-menu-translation-sync.php | Retired trid helpers, kept callable. |
| includes/admin/nav-menus.php, nav-menus-helpers.php, nav-menus-language.php, nav-menus-language-save.php, nav-menus-assignment-notices.php | Admin toolbar, language filter, per-menu language save and admin notices on the nav-menus screen. |
| includes/admin/menu.php + includes/admin/views/menu-synchronization.php | Menu Synchronization page handler and view. |
Menu Language Metadata
A nav menu’s language is term meta wpr_menu_language on the nav_menu term (key from wpr_translation_get_nav_menu_language_meta_key()). wpr_translation_get_nav_menu_language_code( $menu_id ) returns the stored code, otherwise the current default language. A menu with no meta therefore follows the default language.
wpr_translation_store_nav_menu_language( $menu_id, $menu_data ) runs on wp_create_nav_menu and wp_update_nav_menu (priority 10). It reads $_POST['wpr_menu_language'] (nonce update-nav_menu), requires edit_theme_options and calls wpr_translate_menu_assignments_change_menu_language(). When the posted code equals the default language and the menu has no meta yet, nothing is written, so the menu keeps following the default.
Admin Integration
wpr_translation_setup_nav_menu_integration() is hooked on load-nav-menus.php and registers:
add_action( 'admin_notices', 'wpr_translation_render_nav_menu_toolbar' ); add_action( 'admin_notices', 'wpr_translate_menu_assignments_render_notices', 5 ); add_action( 'admin_enqueue_scripts', 'wpr_translation_enqueue_nav_menu_toolbar_assets' ); add_filter( 'wp_get_nav_menus', 'wpr_translation_filter_nav_menus_by_language', 10, 2 );
The wp_get_nav_menus filter hides menus of other languages while a language chip (wpr_language query arg) is active. Failed assignment writes are queued as admin notices instead of failing silently.
Language-Aware Location Map
Assignments are stored in the option wpr_translate_nav_menu_locations (wpr_translation_get_nav_menu_locations_option_name()) as a versioned envelope scoped by theme stylesheet:
array(
'version' => 1,
'themes' => array(
'wpresidence-child' => array(
'primary' => array( 'en' => 12, 'fr' => 34 ),
'footer' => array( 'en' => 13, 'fr' => 35 ),
),
),
)
Only the active theme’s map (get_stylesheet()) is read and written; maps of other themes are preserved. Location keys go through sanitize_key(), language codes through strtolower( sanitize_key() ), menu IDs through absint(). A site without the envelope reads core nav_menu_locations as default language assignments.
Storage API
| Function | Role |
|---|---|
| wpr_translation_get_stored_nav_menu_locations() | Wrapper for wpr_translate_menu_assignments_read_map(): the active theme’s normalized map. |
| wpr_translation_update_nav_menu_locations_storage( $locations ) | Wrapper for wpr_translate_menu_assignments_save_map(): validates the complete map, writes it atomically and returns true or WP_Error. |
| wpr_translate_menu_assignments_save_slot( $location, $language, $menu_id ) | Saves or clears ($menu_id = 0) one slot. |
| wpr_translate_menu_assignments_resolve_slot( $location, $language, $frontend ) | Returns the menu ID for a slot; on the front end falls back to the default language slot. Returns WP_Error for a missing menu or a language mismatch. |
| wpr_translation_capture_nav_menu_locations( $locations ) | On pre_set_theme_mod_nav_menu_locations. |
| wpr_translation_store_submitted_nav_menu_locations( $menu_id, $menu_data ) | On wp_create_nav_menu / wp_update_nav_menu (priority 20). |
| wpr_translation_filter_theme_mod_nav_menu_locations( $value ) | On theme_mod_nav_menu_locations, so core reads the menu for the current language. |
| wpr_translation_cleanup_nav_menu_locations_on_delete( $menu_id ) | On wp_delete_nav_menu; removes the menu from every stored theme map. |
A save is validated before it is written. Rows carried over unchanged from the previous map are accepted even if stale, so old data never blocks an unrelated write.
Front-End Fallback
wpr_translation_filter_nav_menu_locations_option() is registered on 'option_' . wpr_translation_get_nav_menu_locations_option_name(). It unwraps the envelope to the active theme’s map and, on non-admin requests, copies the default language menu ID into any location that has no entry for the requested language.
The front-end get_terms language filter skips the nav_menu taxonomy, so wp_get_nav_menus() returns all menus on secondary languages (Studio / Elementor navigation widgets rely on this).
Default Language and Language List Changes
The default language and the language list are both saved in WPEstate Translate > Languages (option wpr_translate_languages). Two hooks react when that option changes:
Hook on update_option_wpr_translate_languages |
Behaviour |
|---|---|
| wpr_translate_menu_assignments_handle_default_language_change (10) | A metadata-free menu assigned to the old default is pinned to the old default (wpr_menu_language written), so it stays in its language and the new default keeps its own menu. |
| wpr_translate_menu_assignments_prune_removed_languages (20) | Drops assignment rows whose language was removed from the Languages Manager. |
Current Language Detection
wpr_translation_get_requested_nav_menu_language_code()– front-end active language.wpr_translation_get_current_nav_menu_filter()– adminwpr_languagequery var.wpr_translation_adjust_selected_nav_menu()– onnav_menu_selected_id, keeps the selected menu in the active language.wpr_translation_enforce_nav_menu_language_context()– onload-nav-menus.php(priority 5), prevents cross-language selection drift.
Menu Translation Records (retired)
Nav menus no longer write rows to {$wpdb->prefix}wpestate_translation_translations. Menu language is owned by the wpr_menu_language meta and the assignment map. The helpers remain callable for compatibility:
| Function | 1.0.9 behaviour |
|---|---|
| wpr_translation_get_nav_menu_translation_trid( $menu_id ) | Still reads a legacy tax_nav_menu trid if one exists. |
| wpr_translation_upsert_nav_menu_translation(), wpr_translation_ensure_nav_menu_translation_record() | Return false; no write. |
| wpr_translation_sync_nav_menu_translations_from_locations( $locations ) | Returns the normalized map; no family membership is created. |
Language Switcher Injection
wpr_translate_inject_language_switcher_into_menu() on wp_nav_menu_items reads wpr_translate_settings['menu_language_switcher'][ $location ] (before or after, set in WPEstate Translate > Settings) and adds the markup from wpr_translate_get_language_switcher_menu_markup( $location ). Menus printed without theme_location are matched to a location by menu term ID. See the Language Switcher Widget developer reference.
Menu Synchronization
wpr_translate_admin_handle_menu_sync_submission() runs on admin_init when wpr_menu_sync_action=sync is posted (capability manage_options, nonce wpr_menu_sync) and calls wpr_translate_admin_sync_menu_structures( $source_menu_id, $target_menu_id, $fallback_url ). It redirects to page=wpr-translate-menu-sync&settings-updated=true so success and error notices show.
- The target menu must have
wpr_menu_languagemeta, otherwisewpr_menu_sync_languageerror. - Every item payload is built first; if none can be built the target menu is left untouched (
wpr_menu_sync_no_items). - Only then are target items deleted and rebuilt, keeping the parent/child structure.
| Source item type | Result in the target menu |
|---|---|
| post_type | Linked to the translated post (wpr_translate_relationship_resolve_post()) with its title; if no translation, a custom link to the fallback URL (or #). |
| taxonomy | Linked to the translated term with a language-localized URL (wpr_translate_relationship_resolve_term()); if no translation, a custom link to the fallback URL. |
| custom | Same URL. |
Titles of custom and taxonomy items are looked up in the string catalog under the active theme’s text domain (wpr_translate_admin_get_theme_domain( wp_get_theme() )).
Extension Points
- Writes – use
wpr_translation_update_nav_menu_locations_storage()orwpr_translate_menu_assignments_save_slot()and check forWP_Error. Do notupdate_option()the envelope directly. - Reads – use
wpr_translate_menu_assignments_resolve_slot()for one slot with fallback. - Overrides – most public helpers are wrapped in
function_exists()guards and can be shadowed from a child theme include loaded earlier.
Gotchas
option_{name}is a read filter. It returns the active theme’s map, not the raw envelope.- Menu IDs are term IDs; the language is term meta, not post meta.
- A menu with no
wpr_menu_languagefollows the default language. Menu Synchronization still requires the target menu to have explicit meta. - Switching themes gives an empty map for the new stylesheet; the previous theme’s assignments are kept.
- Language codes are always lowercased via
strtolower( sanitize_key() ).
Further Reading
- Translation Linking (trid system) – how posts and terms are linked.
- Database Schema – the plugin tables.
- Managing Languages – the language registry the menu map keys on.
See also the main multi-language real estate website page for the product-level view.