Files
2026-02-18 01:31:41 +01:00

389 lines
12 KiB
Markdown

---
phase: 15-locale-detection-routing
plan: 02
type: execute
wave: 2
depends_on: ["15-01"]
files_modified:
- themes/quotify/partials/header.htm
- themes/quotify/partials/language-switcher.htm
- themes/quotify/layouts/default.htm
- themes/quotify/layouts/dashboard.htm
- themes/quotify/layouts/empty.htm
- themes/quotify/assets/css/components/language-switcher.css
- themes/quotify/assets/css/app.css
autonomous: true
must_haves:
truths:
- "Language switcher dropdown visible in header"
- "Clicking language option switches locale and reloads page"
- "Current language name shown in native form (Polski, Deutsch, English)"
- "All pages have hreflang tags for SEO"
artifacts:
- path: "themes/quotify/partials/language-switcher.htm"
provides: "Dropdown language switcher UI"
contains: "data-request=\"localePicker::onSwitchLocale\""
- path: "themes/quotify/partials/header.htm"
provides: "Header with language switcher"
contains: "language-switcher"
- path: "themes/quotify/layouts/default.htm"
provides: "Layout with hreflang and LocalePicker"
contains: "alternateHrefLangElements"
key_links:
- from: "themes/quotify/partials/language-switcher.htm"
to: "localePicker::onSwitchLocale"
via: "data-request attribute"
pattern: "data-request.*localePicker::onSwitchLocale"
- from: "themes/quotify/layouts/default.htm"
to: "AlternateHrefLangElements component"
via: "component registration"
pattern: "\\[localePicker\\]|\\[alternateHrefLangElements\\]"
---
<objective>
Create language switcher UI in header and integrate hreflang tags for SEO across all layouts.
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
</objective>
<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create language switcher partial and CSS</name>
<files>
themes/quotify/partials/language-switcher.htm
themes/quotify/assets/css/components/language-switcher.css
themes/quotify/assets/css/app.css
</files>
<action>
Create `themes/quotify/partials/language-switcher.htm`:
```twig
{% 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`:
```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:
```css
@import 'components/language-switcher.css';
```
Add JavaScript for dropdown toggle to the partial (inline script at bottom):
```html
<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>
```
</action>
<verify>Files exist and contain expected content: dropdown HTML, CSS styles, JS toggle logic</verify>
<done>Language switcher partial created with accessible dropdown, CSS styling, and toggle JavaScript</done>
</task>
<task type="auto">
<name>Task 2: Integrate language switcher into header</name>
<files>themes/quotify/partials/header.htm</files>
<action>
Update `themes/quotify/partials/header.htm` to include the language switcher in the desktop navigation area, near the right side of the header.
Add the language switcher partial between the nav and the mobile menu toggle:
```twig
<!-- 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):
```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 */
}
}
```
</action>
<verify>Header partial includes language-switcher partial with header-actions wrapper</verify>
<done>Language switcher integrated into site header, visible on all pages</done>
</task>
<task type="auto">
<name>Task 3: Register components and add hreflang to all layouts</name>
<files>
themes/quotify/layouts/default.htm
themes/quotify/layouts/dashboard.htm
themes/quotify/layouts/empty.htm
</files>
<action>
Update all three layouts to:
1. Register localePicker and alternateHrefLangElements components
2. Add hreflang output in head section
For each layout, add component registration in the configuration section:
```ini
[localePicker]
[alternateHrefLangElements]
```
In the `<head>` section, after the meta tags and before stylesheets, add:
```twig
<!-- Alternate language URLs for SEO -->
{% component 'alternateHrefLangElements' %}
```
This generates:
```html
<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.).
</action>
<verify>All layouts register localePicker and alternateHrefLangElements components, and output hreflang in head</verify>
<done>All layouts have LocalePicker for switcher functionality and hreflang tags for SEO</done>
</task>
</tasks>
<verification>
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)
</verification>
<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>
<output>
After completion, create `.planning/phases/15-locale-detection-routing/15-02-SUMMARY.md`
</output>