12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 15-locale-detection-routing | 02 | execute | 2 |
|
|
true |
|
Purpose: Enable users to switch languages via visible dropdown in header, with proper SEO markup for search engines. Output: Language switcher partial, updated header, hreflang integration in all layouts
<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/15-locale-detection-routing/15-CONTEXT.md @.planning/phases/15-locale-detection-routing/15-RESEARCH.md@themes/quotify/partials/header.htm @themes/quotify/layouts/default.htm @themes/quotify/layouts/dashboard.htm @themes/quotify/assets/css/app.css @plugins/golem15/translate/components/LocalePicker.php @plugins/golem15/translate/components/AlternateHrefLangElements.php
Task 1: Create language switcher partial and CSS themes/quotify/partials/language-switcher.htm themes/quotify/assets/css/components/language-switcher.css themes/quotify/assets/css/app.css Create `themes/quotify/partials/language-switcher.htm`:{% set locales = localePicker.locales %}
{% set activeLocale = localePicker.activeLocale %}
{% set activeLocaleName = localePicker.activeLocaleName %}
<div class="language-switcher" data-language-switcher>
<button type="button" class="language-switcher-toggle" aria-expanded="false" aria-haspopup="listbox" aria-label="{{ 'Select language'|_ }}">
<span class="language-switcher-label">{{ activeLocaleName }}</span>
<svg class="language-switcher-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
<ul class="language-switcher-list" role="listbox" aria-label="{{ 'Available languages'|_ }}">
{% for code, name in locales %}
<li role="option" {% if code == activeLocale %}aria-selected="true"{% endif %}>
<a href="#"
class="language-switcher-option {% if code == activeLocale %}active{% endif %}"
data-request="localePicker::onSwitchLocale"
data-request-data="locale: '{{ code }}'">
{{ name }}
</a>
</li>
{% endfor %}
</ul>
</div>
Create themes/quotify/assets/css/components/language-switcher.css:
/* Language Switcher */
.language-switcher {
position: relative;
}
.language-switcher-toggle {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: transparent;
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-700);
cursor: pointer;
transition: all var(--transition-fast);
}
.language-switcher-toggle:hover {
border-color: var(--color-gray-300);
background: var(--color-gray-50);
}
.language-switcher-toggle:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.language-switcher-icon {
width: 16px;
height: 16px;
transition: transform var(--transition-fast);
}
.language-switcher.open .language-switcher-icon {
transform: rotate(180deg);
}
.language-switcher-list {
position: absolute;
top: 100%;
right: 0;
z-index: 50;
min-width: 140px;
margin-top: var(--space-1);
padding: var(--space-1);
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
list-style: none;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all var(--transition-fast);
}
.language-switcher.open .language-switcher-list {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.language-switcher-option {
display: block;
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
color: var(--color-gray-700);
text-decoration: none;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.language-switcher-option:hover {
background: var(--color-gray-50);
color: var(--color-gray-900);
}
.language-switcher-option.active {
background: rgba(0, 102, 102, 0.08);
color: var(--color-primary);
font-weight: 500;
}
.language-switcher-option:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: -2px;
}
/* Mobile: Slightly larger touch targets */
@media (max-width: 768px) {
.language-switcher-toggle {
padding: var(--space-2);
}
.language-switcher-label {
display: none;
}
.language-switcher-toggle::before {
content: '';
width: 20px;
height: 20px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.language-switcher-option {
padding: var(--space-3) var(--space-4);
}
}
Add import to themes/quotify/assets/css/app.css after other component imports:
@import 'components/language-switcher.css';
Add JavaScript for dropdown toggle to the partial (inline script at bottom):
<script>
(function() {
var switcher = document.querySelector('[data-language-switcher]');
if (!switcher) return;
var toggle = switcher.querySelector('.language-switcher-toggle');
var list = switcher.querySelector('.language-switcher-list');
toggle.addEventListener('click', function(e) {
e.preventDefault();
var isOpen = switcher.classList.contains('open');
switcher.classList.toggle('open');
toggle.setAttribute('aria-expanded', !isOpen);
});
// Close on click outside
document.addEventListener('click', function(e) {
if (!switcher.contains(e.target)) {
switcher.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
}
});
// Close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && switcher.classList.contains('open')) {
switcher.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
toggle.focus();
}
});
})();
</script>
Add the language switcher partial between the nav and the mobile menu toggle:
<!-- Desktop Navigation -->
<nav class="main-nav" aria-label="Main navigation">
{% partial 'nav' %}
</nav>
<!-- Language Switcher -->
<div class="header-actions">
{% partial 'language-switcher' %}
</div>
<!-- Mobile Menu Toggle -->
Add CSS for header-actions positioning (add to header.htm inline styles or ensure it exists in header CSS):
The header-actions div should be positioned to the right, before the mobile menu toggle. If using flexbox on header-inner, it will naturally flow to the right.
Style addition (can be inline or in existing header CSS):
.header-actions {
display: flex;
align-items: center;
gap: var(--space-3);
margin-left: auto;
}
@media (max-width: 768px) {
.header-actions {
order: 2; /* After logo, before menu toggle */
}
}
For each layout, add component registration in the configuration section:
[localePicker]
[alternateHrefLangElements]
In the <head> section, after the meta tags and before stylesheets, add:
<!-- Alternate language URLs for SEO -->
{% component 'alternateHrefLangElements' %}
This generates:
<link rel="alternate" hreflang="en" href="https://quotify.pro/current-page">
<link rel="alternate" hreflang="pl" href="https://quotify.pro/pl/current-page">
<link rel="alternate" hreflang="de" href="https://quotify.pro/de/current-page">
For default.htm - add both components For dashboard.htm - add both components For empty.htm - check if it exists, add both components if present
The localePicker component is needed so the language-switcher partial can access its properties (locales, activeLocale, etc.). All layouts register localePicker and alternateHrefLangElements components, and output hreflang in head All layouts have LocalePicker for switcher functionality and hreflang tags for SEO
1. Visual: Language switcher dropdown visible in header on all pages 2. Functional: Click switcher, dropdown opens with language options 3. Functional: Click a language option, page reloads in that language 4. Accessibility: Switcher focusable, Escape closes dropdown 5. SEO: View page source, confirm hreflang tags present for en, pl, de 6. Mobile: On mobile viewport, switcher shows icon only (label hidden) 7. Styling: Matches design palette (teal primary, proper spacing)<success_criteria>
- Language switcher partial exists with dropdown UI
- Header displays language switcher near right side
- Dropdown shows all enabled locales (English, Polski, Deutsch)
- Clicking language triggers localePicker::onSwitchLocale
- All layouts register localePicker and alternateHrefLangElements components
- hreflang tags present in page source for all enabled locales
- Responsive design: icon-only on mobile </success_criteria>