This article documents how WPResidence Translate attaches languages to WordPress nav menus, stores per-language theme-location assignments, tracks menu translation groups in the database, 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-locations.php |
Storage + sanitizer for the language-aware menu location map. |
includes/nav-menu-locations-cleanup.php |
Removes deleted menus from the stored assignments. |
includes/nav-menu-locations-frontend.php |
Front-end fallback logic and language switcher injection into menus. |
includes/nav-menu-translation-sync.php |
Reads/writes translation records for nav menus in the translations table. |
includes/admin/nav-menus.php |
Admin toolbar & language filter on the nav-menus screen. |
includes/admin/nav-menus-helpers.php |
Language overview, meta key, and filter helpers shared by admin code. |
includes/admin/nav-menus-language.php |
Per-menu language resolution and selection adjustment. |
includes/admin/views/menu-synchronization.php |
Admin view for the Menu Synchronization page. |
Menu Language Metadata
The language of a nav menu is stored as term meta on the nav_menu taxonomy term. The meta key is returned by wpr_translation_get_nav_menu_language_meta_key(). Effective resolution:
function wpr_translation_get_nav_menu_language_code( $menu_id ) {
// Stored language in term meta, otherwise the plugin default.
}
wpr_translation_store_nav_menu_language( $menu_id ) writes the language code on menu create/update. Deletion is handled by the cleanup layer.
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_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 uses the current filter (from the wpr_language query arg) to hide menus belonging to other languages when a language chip is active on the admin screen. Capability gate: edit_theme_options.
Language-Aware Location Map
Per-language location assignments are stored in a dedicated option, not in theme_mods_*. Option name:
wpr_translate_nav_menu_locations
Accessed via wpr_translation_get_nav_menu_locations_option_name(). Shape:
array(
'primary' => array( 'en' => 12, 'fr' => 34 ),
'footer' => array( 'en' => 13, 'fr' => 35 ),
)
Normalization is done by wpr_translation_normalize_nav_menu_locations_map() — every location key goes through sanitize_key(), every language code through strtolower( sanitize_key() ), and every menu ID through absint(). Entries with invalid parts are dropped.
Storage API
| Function | Role |
|---|---|
wpr_translation_get_stored_nav_menu_locations() |
Reads the option and returns a normalized map. |
wpr_translation_update_nav_menu_locations_storage( $locations ) |
Writes the normalized map back to the option. |
wpr_translation_capture_nav_menu_locations( $locations ) |
Captures assignments coming out of the nav menu save flow. |
wpr_translation_store_submitted_nav_menu_locations( $menu_id, $menu_data ) |
Stores submitted per-menu location assignments. |
wpr_translation_filter_theme_mod_nav_menu_locations( $value ) |
Intercepts theme_mod_nav_menu_locations so WordPress core reads the language-appropriate menu. |
wpr_translation_cleanup_nav_menu_locations_on_delete( $menu_id ) |
Hook target on menu deletion that strips the menu from every language entry. |
Front-End Fallback
wpr_translation_filter_nav_menu_locations_option() is registered on the dynamic filter:
add_filter(
'option_' . wpr_translation_get_nav_menu_locations_option_name(),
'wpr_translation_filter_nav_menu_locations_option'
);
On non-admin requests, for each location that has a default-language assignment but no entry for the requested language, the default-language menu ID is mirrored into the active language slot. This guarantees the frontend always resolves to a menu even when a translator has not assigned one per language.
Current Language Detection
wpr_translation_get_requested_nav_menu_language_code()— front-end detection of the active language.wpr_translation_get_current_nav_menu_filter()— admin-side reading of thewpr_languagequery var.wpr_translation_adjust_selected_nav_menu()— keeps the selected menu consistent with the active language filter on the admin screen.wpr_translation_enforce_nav_menu_language_context()— prevents cross-language menu selection drift.
Menu Translation Records
Nav menus also participate in the main translations table using element type tax_nav_menu. Helpers in nav-menu-translation-sync.php:
wpr_translation_get_nav_menu_translation_trid( $menu_id )
wpr_translation_upsert_nav_menu_translation( $menu_id, $language_code, $trid, $source_language, $is_original )
wpr_translation_ensure_nav_menu_translation_record( $menu_id, $language_code )
wpr_translation_sync_nav_menu_translations_from_locations( $locations )
Table: {$wpdb->prefix}wpestate_translation_translations. Relevant columns: element_id, element_type = 'tax_nav_menu', trid, language_code, source_language_code, post_status, translator_id, original.
Records are upserted: existing rows are updated via $wpdb->update(); missing rows are inserted. source_language_code is set to null for originals and for rows whose source language is empty.
Language Switcher Injection
wpr_translate_get_language_switcher_menu_markup( $location ) generates the HTML for injecting the language switcher directly into a menu slot, provided wpr_translate_language_switcher_widget_display() is available. Returns an empty string if the switcher function is missing.
Menu Synchronization
The admin view lives in includes/admin/views/menu-synchronization.php. It receives a $data array with:
default_menus,other_menus— menu dropdown options per language.default_menu_items,other_menu_items— flattened menu item lists withtitle,url,depth.selected_default_menu,selected_other_menu.fallback_link— used when a translated target is missing.page_slug— admin page slug, defaults towpr-translate-menu-sync.
The sync workflow copies structure, re-resolves post and term targets to their translated equivalents (via the translation group helpers), and keeps custom links verbatim. Missing translations preserve the source title instead of leaving a blank.
Cleanup on Deletion
wpr_translation_cleanup_nav_menu_locations_on_delete( $menu_id ) is hooked on menu deletion. It loads the option, removes every occurrence of the deleted ID from the language sub-array, drops empty location entries, and writes the map back only when it changed.
Extension Points
- Per-location defaults — extend
wpr_translation_filter_nav_menu_locations_option()via a custom filter to implement custom fallback rules per location. - Extra element types — if you add custom menu-like structures, reuse the
tax_nav_menuelement-type pattern to reuse the translations table. - Sync customization — override the synchronization routine by shadowing its helper functions under
function_exists()guards in a child theme include.
Gotchas
option_{name}is a read filter. Writes go through the storage helpers directly — do not rely on the option filter to persist fallbacks.- Menu IDs are term IDs. Term meta (not post meta) stores the language code.
- The cleanup hook is vital. Without it, deleted menus leave dangling IDs that render as empty locations.
- Translation records for nav menus use
element_type = 'tax_nav_menu'— querying by post type alone will miss them. - Language codes are always lowercased via
strtolower( sanitize_key() ). Comparisons in your own code should match.
Further Reading
- Translation Linking (trid system) — how the
tridcolumn works. - Database Schema — full schema of the six plugin tables.
- Managing Languages — the language registry that the menu map keys on.
See also the main multi-language real estate website page for the product-level view.