389 lines
12 KiB
Markdown
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>
|