434 lines
14 KiB
Markdown
434 lines
14 KiB
Markdown
---
|
|
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>
|