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

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
15-01
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
true
truths artifacts key_links
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
path provides contains
themes/quotify/partials/language-switcher.htm Dropdown language switcher UI data-request="localePicker::onSwitchLocale"
path provides contains
themes/quotify/partials/header.htm Header with language switcher language-switcher
path provides contains
themes/quotify/layouts/default.htm Layout with hreflang and LocalePicker alternateHrefLangElements
from to via pattern
themes/quotify/partials/language-switcher.htm localePicker::onSwitchLocale data-request attribute data-request.*localePicker::onSwitchLocale
from to via pattern
themes/quotify/layouts/default.htm AlternateHrefLangElements component component registration [localePicker]|[alternateHrefLangElements]
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

<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>
Files exist and contain expected content: dropdown HTML, CSS styles, JS toggle logic Language switcher partial created with accessible dropdown, CSS styling, and toggle JavaScript Task 2: Integrate language switcher into header themes/quotify/partials/header.htm 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:

<!-- 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 */
    }
}
Header partial includes language-switcher partial with header-actions wrapper Language switcher integrated into site header, visible on all pages Task 3: Register components and add hreflang to all layouts themes/quotify/layouts/default.htm themes/quotify/layouts/dashboard.htm themes/quotify/layouts/empty.htm 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:

[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>
After completion, create `.planning/phases/15-locale-detection-routing/15-02-SUMMARY.md`