WinterCMS research

This commit is contained in:
Jakub Zych
2026-02-18 01:31:41 +01:00
parent bec00a8bd5
commit 29766aee93
40 changed files with 8529 additions and 0 deletions

View File

@@ -0,0 +1,433 @@
---
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"
---
<objective>
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
</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/account/update.htm
@plugins/golem15/user/components/Account.php
@plugins/golem15/translate/components/LocalePicker.php
</context>
<tasks>
<task type="auto">
<name>Task 1: Add language preference section to account settings</name>
<files>themes/quotify/partials/account/update.htm</files>
<action>
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 Section -->
<div class="account-section">
<div class="account-section-header">
<h3 class="account-section-title">{{ 'Language Preference'|_ }}</h3>
<p class="account-section-description">{{ 'Choose your preferred language for the site.'|_ }}</p>
</div>
<div class="account-section-content">
<div class="form-group">
<label for="preferredLocale" class="form-label">{{ 'Preferred Language'|_ }}</label>
<select name="preferred_locale" id="preferredLocale" class="form-control">
{% set locales = {'en': 'English', 'pl': 'Polski', 'de': 'Deutsch'} %}
{% for code, name in locales %}
<option value="{{ code }}" {% if user.preferred_locale == code %}selected{% endif %}>
{{ name }}
</option>
{% endfor %}
</select>
<p class="form-hint">{{ 'This language will be used when you log in.'|_ }}</p>
</div>
</div>
</div>
```
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.
</action>
<verify>Account settings page shows Language Preference dropdown with en/pl/de options</verify>
<done>Logged-in users can select and save their preferred language in account settings</done>
</task>
<task type="auto">
<name>Task 2: Create LocaleSuggestionBanner component</name>
<files>
plugins/golem15/translate/components/LocaleSuggestionBanner.php
plugins/golem15/translate/components/localesuggestionbanner/default.htm
</files>
<action>
Create the LocaleSuggestionBanner component in the Translate plugin.
Create `plugins/golem15/translate/components/LocaleSuggestionBanner.php`:
```php
<?php
namespace Golem15\Translate\Components;
use Cms\Classes\ComponentBase;
use Golem15\Translate\Classes\Translator;
use Golem15\Translate\Models\Locale;
use Request;
use Config;
use Cookie;
class LocaleSuggestionBanner extends ComponentBase
{
public function componentDetails(): array
{
return [
'name' => '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 %}
<div class="locale-suggestion-banner" id="locale-suggestion-banner" role="alert">
<div class="container">
<div class="locale-suggestion-content">
<p class="locale-suggestion-text">
{% 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 %}
</p>
<div class="locale-suggestion-actions">
<button type="button"
class="btn btn-sm btn-primary"
data-request="{{ __SELF__ }}::onSwitchToSuggested"
data-request-data="locale: '{{ suggestedLocale }}'">
{% if suggestedLocale == 'pl' %}Przełącz na polski{% elseif suggestedLocale == 'de' %}Zu Deutsch wechseln{% else %}{{ 'Switch'|_ }}{% endif %}
</button>
<button type="button"
class="locale-suggestion-dismiss"
data-request="{{ __SELF__ }}::onDismissBanner"
data-request-update="{ '#locale-suggestion-banner': '' }"
aria-label="{{ 'Dismiss'|_ }}">
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
</div>
</div>
{% endif %}
```
Register the component in `plugins/golem15/translate/Plugin.php` registerComponents() method (if not already auto-registered).
</action>
<verify>Component file exists, template file exists, component can be registered in layout</verify>
<done>LocaleSuggestionBanner component created with browser language detection and dismissible banner</done>
</task>
<task type="auto">
<name>Task 3: Add banner CSS and integrate into layout</name>
<files>
themes/quotify/assets/css/components/locale-banner.css
themes/quotify/assets/css/app.css
themes/quotify/layouts/default.htm
</files>
<action>
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' %}
```
</action>
<verify>CSS file exists, app.css imports it, default layout registers and displays the banner component</verify>
<done>Locale suggestion banner styled and integrated into default layout</done>
</task>
</tasks>
<verification>
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)
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/15-locale-detection-routing/15-03-SUMMARY.md`
</output>