---
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 settingsthemes/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
{{ 'Language Preference'|_ }}
{{ 'Choose your preferred language for the site.'|_ }}
{{ 'This language will be used when you log in.'|_ }}
```
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 optionsLogged-in users can select and save their preferred language in account settingsTask 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 %}
{% if suggestedLocale == 'pl' %}
Ta strona jest dostepna po polsku
{% elseif suggestedLocale == 'de' %}
Diese Seite ist auf Deutsch verfügbar
{% else %}
{{ 'This page is available in'|_ }} {{ suggestedLocaleName }}
{% endif %}
{% 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 layoutLocaleSuggestionBanner component created with browser language detection and dismissible bannerTask 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 componentLocale 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)