Spanish Toggle Implementation β€” Solano (May 7, 2026)

What landed

A working Spanish ↔ English toggle on the local Solano site at the right end of the main nav (#grouped-nav). Visible as two small flag + 2-letter-code stacks (πŸ‡ΊπŸ‡Έ EN / πŸ‡²πŸ‡½ ES), opacity-only highlight on the active language. Header bar + alert bar were also widened to full-viewport with 20px side padding to give the nav more breathing room.

Status: Implemented locally. Verified translating both directions against Google Translate's element.js via the existing GTranslate plugin. Not yet pushed to production.

Files

Path What it is
~/Local Sites/solano-californiaforever/app/public/wp-content/mu-plugins/gtranslate-nav-placement.php Stop-gap dev mu-plugin. All the work β€” server-side injection, CSS, JS hydration. NOT git-tracked (mu-plugins/ is a Flywheel convention dir).
~/Local Sites/solano-californiaforever/app/public/wp-content/plugins/gtranslate/ GTranslate WordPress plugin (free version, latest stable from wordpress.org). Activated.

Key decisions / playbook for re-implementing on main CF

1. Markup is server-side injected, not JS-built

The toggle markup is added to the response HTML via template_redirect + ob_start regex injection β€” finds #grouped-nav's opening tag and inserts the toggle <div> before </nav>. This means the toggle is in the initial paint, no layout shift after JS hydrates. JS only wires up click handlers + active-state classes.

When porting to main CF: the Tailwind theme uses different nav markup. Need to find the equivalent insertion anchor. The principle (server-side injection, JS hydrates only) carries over.

2. The plugin's default widgets are hidden

GTranslate plugin auto-renders into any WP menu item with the menu-item-gtranslate custom class. On Solano, those WP menu items exist on the utility nav + (cloned into) mmenu drawer. We hide both via body.gt-nav-mounted li.menu-item-gtranslate { display: none !important; }.

When porting: check whether main CF has any menu-item-gtranslate items in its WP menus and hide them the same way.

3. Google Translate's element.js is lazy-loaded β€” we have to handle it manually

The plugin's dropdown.js only loads https://translate.google.com/translate_a/element.js on pointerenter of the original wrapper. Since we hide the wrapper, the lazy-load trigger never fires, so doGTranslate errors with "Cannot read properties of undefined (reading 'length')" because goog-te-combo doesn't exist.

Fix: mu-plugin manually loads element.js on: - First pointerenter / focusin of our flag toggle (warms it up) - Click of either flag button (in case user clicks without hovering) - Page load if cookie says non-English (auto-load when in Spanish β€” see "Mitigations" below)

4. Translation triggers via window.doGTranslate('en|<lang>')

Plugin exposes window.doGTranslate as a global. Our flag click handler:

window.doGTranslate( 'en|' + btn.dataset.lang );  // 'en|es' or 'en|en'

The function has built-in 500ms retry logic while Google's combo loads, so even cold-clicks self-recover.

5. Visual treatment β€” opacity-only

After iterating through border + background-color highlights, landed on opacity-only differentiation: - Active: opacity: 1 - Inactive: opacity: 0.4 - Hover/focus on inactive: opacity: 1 - 0.15s ease transition

No border, no background tint, no outline. Cleaner against the theme's existing nav.

Each button stacks <span class="gt-flag">πŸ‡ΊπŸ‡Έ</span> over <span class="gt-code">EN</span> β€” flag emoji on top (18px), 2-letter ISO code below (10px, weight 600, navy #0A2240, letter-spacing 0.5px, uppercase).

6. Placement β€” right end of #grouped-nav, sibling of nav-groups

Toggle is a sibling of the existing .nav-group divs (Suisun Expansion, Shipyard), NOT inside any of them. #grouped-nav is display: flex; justify-content: flex-end; gap: 24px; so all children right-align with consistent spacing.

When porting to main CF: visual placement may differ β€” Eric said "we might visually put it somewhere different" on the main CF site. Same toggle component, different home.

7. Header + alert bar padding override

The .container class in the theme caps at max-width: 1440px with auto margins. With the new toggle, the nav was getting squished. Override at the header + alert bar level only:

#desktop-header > .container,
#desktop-header .container,
#global-alert > .container,
#global-alert .container {
  max-width: none !important;
  width: auto !important;
  margin-left: 0 !important;
  margin-right: 0 !important;
  padding-left: 20px !important;
  padding-right: 20px !important;
}

This is Solano-specific. Main CF has different markup; assess separately.

8. Latency mitigations (the cheap stuff)

GTranslate is fundamentally client-side β€” there will always be a brief English-then-Spanish flash on each page navigation when in Spanish mode. Bottleneck is the round trip to translate.google.com/translate_a/element.js. Two mitigations in place:

a. Preconnect + dns-prefetch hints in <head>:

<link rel="preconnect" href="https://translate.google.com" crossorigin>
<link rel="preconnect" href="https://translate.googleapis.com" crossorigin>
<link rel="dns-prefetch" href="//translate.google.com">
<link rel="dns-prefetch" href="//translate.googleapis.com">

b. Auto-load element.js at script-execution time when the cookie says non-English, instead of waiting for hover/click. Mirror the plugin's own load_tlib behavior.

Combined: reduces the visible flash from ~500ms to ~150ms on production. Hard floor exists because Google Translate has no server-rendered Spanish option. The "real" fix is Track B (server-rendered translations via Polylang/WPML/custom workflow) which is parked pending Anders.

A future option (not implemented): hide <body> content with opacity:0 + a brief loader on pages where cookie says non-English, until Google's first translation pass completes. Fade in. Need a timeout fallback so a failed Google fetch doesn't leave the page blank. Eric considered this but chose to ship the cheaper preconnect + auto-load and live with the small flash.

Production push

Not yet performed. Workflow when ready: 1. SCP gtranslate-nav-placement.php to wp-content/mu-plugins/ on the Flywheel host - SSH alias: californiaforever+local-solano-cf-site@ssh.getflywheel.com 2. Activate gtranslate plugin on production via WP admin or WP-CLI over SSH 3. Verify both https://solano.californiaforever.com/ and a few key pages load + translate 4. Cache purge (Cloudflare / Flywheel)

Per CLAUDE.md hard rule: each of these prod actions needs explicit per-action approval before running.

Open items / next steps