--- phase: 15-locale-detection-routing plan: 03 type: execute wave: 2 depends_on: ["15-01"] files_modified: - themes/quotify/partials/account/update.htm - plugins/golem15/translate/components/LocaleSuggestionBanner.php - plugins/golem15/translate/components/localesuggestionbanner/default.htm - themes/quotify/layouts/default.htm - themes/quotify/assets/css/components/locale-banner.css - themes/quotify/assets/css/app.css autonomous: true must_haves: truths: - "Account settings has language preference section" - "Logged-in users can select preferred language in profile" - "Language suggestion banner appears when browser language differs" - "Dismissing banner sets cookie to prevent showing again" artifacts: - path: "themes/quotify/partials/account/update.htm" provides: "Language preference section in account settings" contains: "preferred_locale" - path: "plugins/golem15/translate/components/LocaleSuggestionBanner.php" provides: "Component for language mismatch detection" contains: "shouldShowBanner" - path: "plugins/golem15/translate/components/localesuggestionbanner/default.htm" provides: "Banner template" contains: "locale-suggestion-banner" key_links: - from: "themes/quotify/partials/account/update.htm" to: "Account::onUpdate" via: "form submission with preferred_locale field" pattern: "name=\"preferred_locale\"" - from: "LocaleSuggestionBanner" to: "LocaleMiddleware detection" via: "Accept-Language comparison" pattern: "getSuggestedLocale|detectBrowserLocale" --- Add language preference to account settings and create locale suggestion banner for browser language mismatches. Purpose: Allow logged-in users to explicitly set their preferred language, and help new visitors discover available translations. Output: Language preference section in account settings, LocaleSuggestionBanner component with dismissible banner UI @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md @.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/account/update.htm @plugins/golem15/user/components/Account.php @plugins/golem15/translate/components/LocalePicker.php Task 1: Add language preference section to account settings themes/quotify/partials/account/update.htm Add a new "Language Preference" section to the Account Info tab in account/update.htm. Add the section between "Profile Information" and "Change Password" sections: ```twig ``` The Account component's onUpdate handler already processes all posted fields including preferred_locale (it's a fillable field on User model). Place this section at the start of the "Account Info" tab content (id="account-info"), before the Profile Information section. Account settings page shows Language Preference dropdown with en/pl/de options Logged-in users can select and save their preferred language in account settings Task 2: Create LocaleSuggestionBanner component plugins/golem15/translate/components/LocaleSuggestionBanner.php plugins/golem15/translate/components/localesuggestionbanner/default.htm Create the LocaleSuggestionBanner component in the Translate plugin. Create `plugins/golem15/translate/components/LocaleSuggestionBanner.php`: ```php 'Locale Suggestion Banner', 'description' => 'Shows banner when browser language differs from current page' ]; } public function onRun() { $this->page['showBanner'] = $this->shouldShowBanner(); $this->page['suggestedLocale'] = $this->getSuggestedLocale(); $this->page['suggestedLocaleName'] = $this->getSuggestedLocaleName(); } protected function shouldShowBanner(): bool { // Don't show if manually set (user explicitly chose a language) $cookieName = Config::get('golem15.translate::browserDetection.manualSelectionCookie', 'locale_manually_set'); if (Request::cookie($cookieName)) { return false; } // Don't show if dismissed this session if (session('locale_banner_dismissed')) { return false; } // Don't show if 1-week dismissal cookie exists if (Request::cookie('locale_banner_dismissed')) { return false; } // Check if browser language differs from current return $this->getSuggestedLocale() !== null; } protected function getSuggestedLocale(): ?string { $browserLocale = $this->detectBrowserLocale(); $currentLocale = Translator::instance()->getLocale(); if ($browserLocale && $browserLocale !== $currentLocale) { return $browserLocale; } return null; } protected function detectBrowserLocale(): ?string { $acceptLanguage = Request::header('Accept-Language'); if (!$acceptLanguage) return null; $enabledLocales = array_keys(Locale::listEnabled()); $candidates = explode(',', $acceptLanguage); foreach ($candidates as $candidate) { // Extract primary language code (e.g., "pl-PL;q=0.9" -> "pl") $code = strtolower(substr(trim(explode(';', $candidate)[0]), 0, 2)); if (in_array($code, $enabledLocales)) { return $code; } } return null; } protected function getSuggestedLocaleName(): ?string { $locale = $this->getSuggestedLocale(); if (!$locale) return null; $locales = Locale::listEnabled(); return $locales[$locale] ?? null; } public function onDismissBanner() { session(['locale_banner_dismissed' => true]); // Set 1-week dismissal cookie (7 days = 10080 minutes) Cookie::queue('locale_banner_dismissed', '1', 10080); return ['#locale-suggestion-banner' => '']; } public function onSwitchToSuggested() { $locale = post('locale'); if (!Locale::isValid($locale)) { return; } Translator::instance()->setLocale($locale, true); // Set manual selection cookie (1 year) Cookie::queue( Config::get('golem15.translate::browserDetection.manualSelectionCookie', 'locale_manually_set'), '1', Config::get('golem15.translate::browserDetection.manualSelectionExpiry', 525600) ); return \Redirect::refresh(); } } ``` Create directory and template `plugins/golem15/translate/components/localesuggestionbanner/default.htm`: ```twig {% if showBanner and suggestedLocale %} {% endif %} ``` Register the component in `plugins/golem15/translate/Plugin.php` registerComponents() method (if not already auto-registered). Component file exists, template file exists, component can be registered in layout LocaleSuggestionBanner component created with browser language detection and dismissible banner Task 3: Add banner CSS and integrate into layout themes/quotify/assets/css/components/locale-banner.css themes/quotify/assets/css/app.css themes/quotify/layouts/default.htm Create `themes/quotify/assets/css/components/locale-banner.css`: ```css /* Locale Suggestion Banner */ .locale-suggestion-banner { position: sticky; top: 0; z-index: 200; background: linear-gradient(135deg, var(--color-primary), #004d4d); color: var(--color-white); padding: var(--space-3) 0; } .locale-suggestion-content { display: flex; align-items: center; justify-content: center; gap: var(--space-4); flex-wrap: wrap; } .locale-suggestion-text { margin: 0; font-size: var(--text-sm); font-weight: 500; } .locale-suggestion-actions { display: flex; align-items: center; gap: var(--space-3); } .locale-suggestion-banner .btn-primary { background: var(--color-white); color: var(--color-primary); border-color: var(--color-white); padding: var(--space-1) var(--space-3); font-size: var(--text-sm); } .locale-suggestion-banner .btn-primary:hover { background: rgba(255, 255, 255, 0.9); } .locale-suggestion-dismiss { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; background: transparent; border: none; color: rgba(255, 255, 255, 0.8); cursor: pointer; border-radius: var(--radius-sm); transition: all var(--transition-fast); } .locale-suggestion-dismiss:hover { color: var(--color-white); background: rgba(255, 255, 255, 0.1); } .locale-suggestion-dismiss svg { width: 18px; height: 18px; } .locale-suggestion-dismiss:focus-visible { outline: 2px solid var(--color-white); outline-offset: 2px; } @media (max-width: 480px) { .locale-suggestion-content { flex-direction: column; text-align: center; gap: var(--space-3); } .locale-suggestion-actions { width: 100%; justify-content: center; } } ``` Add import to `themes/quotify/assets/css/app.css`: ```css @import 'components/locale-banner.css'; ``` Update `themes/quotify/layouts/default.htm` to: 1. Register the localeSuggestionBanner component 2. Include the banner partial after flash messages, before page-wrapper Add to component registration: ```ini [localeSuggestionBanner] ``` Add to body, after flash partial and before page-wrapper: ```twig {% component 'localeSuggestionBanner' %} ``` CSS file exists, app.css imports it, default layout registers and displays the banner component Locale suggestion banner styled and integrated into default layout 1. Account settings: Language Preference section visible with dropdown 2. Account settings: Selecting language and saving updates user.preferred_locale 3. Banner: Clear all cookies, set browser language to German, visit English page 4. Banner: German suggestion banner appears at top of page 5. Banner: Click "Zu Deutsch wechseln" switches to German and sets cookie 6. Banner: Click X to dismiss, banner disappears 7. Banner: Refresh page, banner does not reappear (cookie prevents) 8. Banner: After 1 week cookie expires, banner would show again (can't test easily) - Account settings has Language Preference section with en/pl/de dropdown - Saving preference updates User::preferred_locale in database - LocaleSuggestionBanner component detects browser language mismatch - Banner appears when browser language differs from page language - Banner shows localized text ("Diese Seite ist auf Deutsch verfügbar") - Switch button changes language and sets manual selection cookie - Dismiss button hides banner and sets 1-week cookie - Banner respects locale_manually_set cookie (doesn't show if user already chose) After completion, create `.planning/phases/15-locale-detection-routing/15-03-SUMMARY.md`