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
- Production push for Solano (above) β pending Eric's go.
- Main CF (californiaforever.com) β same toggle component, possibly different visual placement. Tailwind theme, different nav markup; will need separate insertion anchor + scoping pass. Header + alert bar padding override is Solano-specific; main CF has its own layout.
- Track B (custom AI translation workflow) β still parked pending Anders' decision per
WAITING-ON-CLIENT.md.