Blocklingo > Header and footer picks the menu * location; the languages are added as menu items, so the theme's own walker * and CSS render them like the other links. Nothing is stored in the menu. */ declare( strict_types = 1 ); namespace AddBlack\Lang\Switcher; defined( 'ABSPATH' ) || exit; use AddBlack\Lang\Hookable; use AddBlack\Lang\Hooks; use AddBlack\Lang\Settings\SettingsRepository; use WP_Post; final class MenuSwitcher implements Hookable { /** Class on every added item; the link attributes filter looks for it. */ public const ITEM_CLASS = 'blocklingo-menu-lang'; /** Added items get IDs below this, so they never clash with real menu items. */ private const ID_BASE = -1000; /** @var array item ID => link attributes. */ private array $links = array(); public function __construct( private readonly SettingsRepository $settings, private readonly SwitcherBlock $switcher ) {} public function register(): void { Hooks::filter( 'wp_nav_menu_objects', array( $this, 'add_items' ), 20, 2 ); Hooks::filter( 'nav_menu_link_attributes', array( $this, 'link_attributes' ), 20, 2 ); Hooks::filter( 'nav_menu_item_id', array( $this, 'item_id' ), 20, 2 ); } /** * Nav_menu_item_id: menu-item-blocklingo-uk instead of menu-item--1001. * * @param mixed $id The li id. * @param mixed $item Menu item. * @return mixed */ public function item_id( mixed $id, mixed $item ): mixed { if ( ! is_object( $item ) || ! isset( $item->ID ) || ! isset( $this->links[ (int) $item->ID ] ) ) { return $id; } return 'menu-item-blocklingo-' . $this->links[ (int) $item->ID ]['code']; } /** * Wp_nav_menu_objects: append one item per language to the chosen location. * * @param mixed $items Sorted menu items. * @param mixed $args wp_nav_menu() arguments. * @return mixed */ public function add_items( mixed $items, mixed $args ): mixed { $options = $this->settings->get()->switcher_menu(); if ( ! is_array( $items ) || ! is_object( $args ) || '' === $options['location'] || ( $args->theme_location ?? '' ) !== $options['location'] ) { return $items; } $links = $this->switcher->links( array( 'display' => $options['display'], 'hideCurrent' => $options['hide_current'], 'hideMissing' => $options['hide_missing'], ) ); if ( ! $links ) { return $items; } wp_enqueue_style( 'blocklingo-switcher' ); $order = count( $items ); foreach ( $links as $i => $link ) { $id = self::ID_BASE - $i; $this->links[ $id ] = array( 'code' => $link['code'], 'hreflang' => $link['hreflang'], 'current' => $link['current'], 'aria' => $link['aria'], ); $items[] = self::item( $id, ++$order, $link ); } return $items; } /** * Nav_menu_link_attributes: hreflang/lang, and the same aria labels as the block. * * @param mixed $atts Link attributes. * @param mixed $item Menu item. * @return mixed */ public function link_attributes( mixed $atts, mixed $item ): mixed { if ( ! is_array( $atts ) || ! is_object( $item ) || ! isset( $item->ID ) || ! isset( $this->links[ (int) $item->ID ] ) ) { return $atts; } $link = $this->links[ (int) $item->ID ]; $atts['hreflang'] = $link['hreflang']; $atts['lang'] = $link['hreflang']; if ( $link['current'] ) { $atts['aria-current'] = 'true'; } if ( '' !== $link['aria'] ) { $atts['aria-label'] = $link['aria']; } return $atts; } /** * A menu item that exists only for this request. * * @param array{code: string, url: string, current: bool, html: string} $link */ private static function item( int $id, int $order, array $link ): WP_Post { $classes = array( 'menu-item', 'menu-item-type-custom', 'menu-item-object-custom', self::ITEM_CLASS ); if ( $link['current'] ) { $classes[] = 'is-current'; } $classes[] = self::ITEM_CLASS . '-' . $link['code']; $classes[] = 'wp-exclude-emoji'; return new WP_Post( (object) array( 'ID' => $id, 'db_id' => $id, 'post_type' => 'nav_menu_item', 'post_status' => 'publish', 'post_title' => $link['html'], 'title' => $link['html'], 'url' => $link['url'], 'menu_item_parent' => '0', 'menu_order' => $order, 'object_id' => (string) $id, 'object' => 'custom', 'type' => 'custom', 'type_label' => '', 'target' => '', 'attr_title' => '', 'description' => '', 'classes' => $classes, 'xfn' => '', 'current' => false, 'filter' => 'raw', ) ); } }