🌍 Language Selector Design
As the Mushroom Hub grows globally, users need an intuitive way to switch languages. A poor language selector is hard to find or confusing (e.g., using flags that don't represent languages).
1. The Flag Problem
If you do use flags (as a visual aid alongside text labels), consider that:
- Many screen readers announce emoji flags by their technical name ("flag for United States"), not as a language indicator.
- Flags carry political sensitivity in some contexts — the Ukraine/Russia flag distinction matters greatly to Psilobase's audience.
- If you use a flag, pair it with the language's native name. Example:
🇺🇦 Українська— not just🇺🇦.
2. Header Dropdown (Primary Pattern)
A compact dropdown in the sticky header is the most space-efficient language switcher. It must be keyboard-accessible and work without JavaScript for basic functionality.
Accessible HTML Pattern
<!-- ARIA-compliant language dropdown -->
<div class="lang-switcher">
<button
id="lang-btn"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="lang-list"
aria-label="Select language: English"
>
🌐 English ▼
</button>
<ul
id="lang-list"
role="listbox"
aria-label="Available languages"
hidden
>
<li role="option" aria-selected="true">
<a href="/en/" lang="en" hreflang="en">English</a>
</li>
<li role="option" aria-selected="false">
<a href="/uk/" lang="uk" hreflang="uk">Українська</a>
</li>
<li role="option" aria-selected="false">
<a href="/es/" lang="es" hreflang="es">Español</a>
</li>
</ul>
</div>
<script>
const btn = document.getElementById('lang-btn');
const list = document.getElementById('lang-list');
btn.addEventListener('click', () => {
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', !expanded);
list.hidden = expanded;
});
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !list.hidden) {
list.hidden = true;
btn.setAttribute('aria-expanded', 'false');
btn.focus();
}
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!btn.contains(e.target) && !list.contains(e.target)) {
list.hidden = true;
btn.setAttribute('aria-expanded', 'false');
}
});
</script>
3. Footer List (Secondary Pattern)
A flat list of all languages in the footer serves two purposes: it helps users who scroll to the bottom looking for region settings, and it provides crawlable hreflang links for search engines to discover alternate language versions of each page.
SEO: hreflang Links
Every page must include hreflang tags in the <head> for all language variants. This tells Google which language each version targets and prevents cannibalisation between translations:
<!-- In <head> of the English page -->
<link rel="alternate" hreflang="en" href="https://psilobase.com/en/safety/" >
<link rel="alternate" hreflang="uk" href="https://psilobase.com/uk/safety/" >
<link rel="alternate" hreflang="es" href="https://psilobase.com/es/safety/" >
<link rel="alternate" hreflang="x-default" href="https://psilobase.com/safety/" >
The x-default tag indicates the fallback page for users whose language is not specifically supported.
4. Language Auto-Detection
Automatic language detection can reduce friction for first-time visitors. Use the browser's navigator.language value, but always allow manual override.
Implementation
// Detect preferred language on first visit
function detectPreferredLanguage() {
// 1. Check if the user has a stored preference
const stored = localStorage.getItem('psilobase-lang');
if (stored) return stored;
// 2. Check browser language
const browserLang = navigator.language || navigator.userLanguage;
const langCode = browserLang.split('-')[0]; // 'uk-UA' → 'uk'
// 3. Only auto-redirect if we support the language
const supported = ['en', 'uk', 'es', 'de', 'fr', 'pt'];
if (supported.includes(langCode)) return langCode;
// 4. Default to English
return 'en';
}
// On first visit only — don't redirect returning users
const isFirstVisit = !sessionStorage.getItem('visited');
if (isFirstVisit) {
sessionStorage.setItem('visited', '1');
const lang = detectPreferredLanguage();
const currentLang = document.documentElement.lang;
if (lang !== currentLang) {
// Show a "Switch to Ukrainian?" banner instead of auto-redirecting
showLanguageSuggestion(lang);
}
}
The Suggestion Banner Pattern
Rather than automatically redirecting users (which can be disorienting), show a dismissible suggestion banner: "We noticed your browser is set to Ukrainian. Would you like to switch?" Store the user's choice (accept or dismiss) in localStorage so the banner never shows twice.
This approach respects user agency and avoids the frustrating experience of being redirected to a language you didn't choose — especially important for editors, researchers, or bilingual users who deliberately access the English version.
5. Preserving Language Preference on Navigation
Once a user selects a language, every subsequent link must stay in that language. This means:
- All internal links must include the language prefix:
/uk/microdosing/not/microdosing/. - The language selection must persist across sessions via
localStorage. - Search functionality must search within the selected language — mixing languages in search results is confusing.
- The 404 page must be language-aware: A Ukrainian user who follows a broken link should see the 404 message in Ukrainian, not English.
6. Content Completeness Indicators
If some pages have not yet been translated, communicate this transparently rather than showing partial or machine-translated content without labelling it. Options:
- Language coverage badge: Each species page shows "Available in: EN | UK | ES". Missing languages are greyed out.
- Translation notice: A banner at the top of untranslated pages: "This page is not yet available in Ukrainian. Showing English version."
- Machine translation disclaimer: If using auto-translated content, label it prominently. Medical and harm-reduction content must never be presented as professionally reviewed when it is machine-translated.