WinterCMS research
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
# Phase 15: Locale Detection & Routing - Research
|
||||
|
||||
**Researched:** 2026-02-02
|
||||
**Domain:** WinterCMS locale routing, URL prefix patterns, browser detection, preference persistence
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
This phase extends the existing Golem15\Translate plugin infrastructure to implement URL-based locale routing with automatic browser detection and persistent preference storage. The core infrastructure already exists and is well-implemented - LocaleMiddleware handles the priority cascade (URL > User DB > Session > Browser > Default), Translator manages locale state, and LocalePicker/AlternateHrefLangElements components are available.
|
||||
|
||||
The primary work involves: (1) Setting the `locale_manually_set` cookie when switching locales via URL prefix or LocalePicker, (2) Creating a language suggestion banner component for mismatched browser languages, (3) Adding a language preference section to the account settings page, and (4) Integrating AlternateHrefLangElements into all layouts for SEO.
|
||||
|
||||
**Primary recommendation:** Leverage existing plugin infrastructure completely. Focus on UI integration (language switcher in header, suggestion banner, account settings), cookie management enhancement, and ensuring logged-in users' preferences sync to DB.
|
||||
|
||||
## Standard Stack
|
||||
|
||||
The established libraries/tools for this domain:
|
||||
|
||||
### Core
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| Golem15\Translate | Current | Full translation infrastructure | Already in codebase, handles locales, middleware, components |
|
||||
| Golem15\User | Current | User model with `preferred_locale` field | Already has preference field, integrated with middleware |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| LocaleMiddleware | Current | Priority cascade for locale detection | Already registered in CmsController |
|
||||
| Translator | Current | Singleton for locale state management | Use for all locale operations |
|
||||
| LocalePicker | Current | Component for language switching | Embed in header |
|
||||
| AlternateHrefLangElements | Current | Generate hreflang tags | Add to all layouts |
|
||||
|
||||
### Already Configured
|
||||
The following is already configured in `/config/golem15/translate/config.php`:
|
||||
- `prefixDefaultLocale: false` - English (default) has no URL prefix
|
||||
- `disableLocalePrefixRoutes: false` - URL prefixes enabled
|
||||
- `browserDetection.enabled: true` - Browser detection active
|
||||
- `browserDetection.manualSelectionCookie: 'locale_manually_set'` - Cookie name defined
|
||||
|
||||
**No additional package installation required.**
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Existing Infrastructure Overview
|
||||
```
|
||||
plugins/golem15/translate/
|
||||
|-- classes/
|
||||
| |-- LocaleMiddleware.php # Priority cascade: URL > User > Session > Browser > Default
|
||||
| |-- Translator.php # Singleton, setLocale/getLocale/loadLocaleFromRequest
|
||||
|-- components/
|
||||
| |-- LocalePicker.php # onSwitchLocale handler, forceUrl property
|
||||
| |-- AlternateHrefLangElements.php # Generates hreflang tags
|
||||
|-- config/config.php # browserDetection settings
|
||||
|-- routes.php # Registers /{locale}/* route groups
|
||||
```
|
||||
|
||||
### Pattern 1: Locale Priority Cascade (Existing)
|
||||
**What:** The middleware implements a clear priority order for locale resolution.
|
||||
**When to use:** Already runs on every CMS request.
|
||||
**Implementation (from LocaleMiddleware.php):**
|
||||
```php
|
||||
// Priority 1: URL prefix (explicit override)
|
||||
if (!$translator->loadLocaleFromRequest()) {
|
||||
// Priority 2: Authenticated user's preferred_locale from DB
|
||||
if (!$this->loadLocaleFromUser($translator)) {
|
||||
// Priority 3: Session (previous selection)
|
||||
$localeLoaded = $translator->loadLocaleFromSession();
|
||||
|
||||
// Priority 4: Browser Accept-Language (only without manual selection cookie)
|
||||
if (!$localeLoaded && !$this->hasManualLocaleSelection($request)) {
|
||||
$localeLoaded = $this->loadLocaleFromBrowser($translator, $request);
|
||||
}
|
||||
|
||||
// Priority 5: Default locale fallback
|
||||
if (!$localeLoaded) {
|
||||
$translator->setLocale($translator->getDefaultLocale());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: URL Prefix Routing (Existing)
|
||||
**What:** Routes with locale prefix are registered dynamically.
|
||||
**When to use:** Automatically for any URL with `/pl/` or `/de/` prefix.
|
||||
**Implementation (from routes.php):**
|
||||
```php
|
||||
$locale = $translator->getLocale();
|
||||
Route::group(['prefix' => $locale, 'middleware' => 'web'], function () {
|
||||
Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?');
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: LocalePicker Component Usage
|
||||
**What:** Provides locale switching with proper URL handling.
|
||||
**When to use:** In theme partials for language switcher.
|
||||
**Example:**
|
||||
```twig
|
||||
{# In layout or header partial #}
|
||||
{% component 'localePicker' %}
|
||||
|
||||
{# Custom implementation #}
|
||||
<div class="language-switcher">
|
||||
<button class="language-current">{{ activeLocaleName }}</button>
|
||||
<ul class="language-list">
|
||||
{% for code, name in locales %}
|
||||
<li>
|
||||
<a href="#"
|
||||
data-request="localePicker::onSwitchLocale"
|
||||
data-request-data="locale: '{{ code }}'">
|
||||
{{ name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern 4: Manual Selection Cookie
|
||||
**What:** Cookie that prevents browser auto-detection after manual selection.
|
||||
**Gap Found:** Cookie is READ by LocaleMiddleware but NOT SET anywhere.
|
||||
**Required Enhancement:**
|
||||
```php
|
||||
// In LocalePicker::onSwitchLocale() - set cookie
|
||||
Cookie::queue(
|
||||
Config::get('golem15.translate::browserDetection.manualSelectionCookie', 'locale_manually_set'),
|
||||
'1',
|
||||
Config::get('golem15.translate::browserDetection.manualSelectionExpiry', 525600) // 1 year
|
||||
);
|
||||
|
||||
// In routes.php - set cookie when URL prefix detected
|
||||
if ($translator->loadLocaleFromRequest()) {
|
||||
Cookie::queue('locale_manually_set', '1', 525600);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Hreflang Implementation
|
||||
**What:** Automatic generation of alternate language links for SEO.
|
||||
**When to use:** In all layouts' `<head>` section.
|
||||
**Example:**
|
||||
```twig
|
||||
{# In layout head #}
|
||||
{% component 'alternateHrefLangElements' %}
|
||||
|
||||
{# Generates #}
|
||||
<link rel="alternate" hreflang="en" href="https://quotify.pro/jobs">
|
||||
<link rel="alternate" hreflang="pl" href="https://quotify.pro/pl/jobs">
|
||||
<link rel="alternate" hreflang="de" href="https://quotify.pro/de/jobs">
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
- **Auto-redirect without user consent:** Never silently redirect users based on browser language. Use suggestion banners instead.
|
||||
- **Coupling locale with location/currency:** Locale is language preference only. Don't assume location from language.
|
||||
- **Modal popups for language selection:** Use non-modal banners - users instinctively dismiss modals.
|
||||
- **Overwriting DB preference on URL prefix visit:** URL prefix should only affect the current session, not update the user's stored preference.
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
Problems that look simple but have existing solutions:
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Locale detection from URL | Custom URL parser | `Translator::loadLocaleFromRequest()` | Handles edge cases, segment extraction |
|
||||
| Accept-Language parsing | Custom header parser | `LocaleMiddleware::parseAcceptLanguage()` | Handles quality values, malformed headers |
|
||||
| Locale validation | Custom list check | `Locale::isValid($locale)` | Uses cached locale list |
|
||||
| Session persistence | Custom session handling | `Translator::setLocale($locale, true)` | Second param enables session storage |
|
||||
| Hreflang generation | Custom link builder | `AlternateHrefLangElements` component | Handles static pages, CMS pages, URL params |
|
||||
|
||||
**Key insight:** The Golem15\Translate plugin has comprehensive infrastructure. The only gaps are: (1) cookie setting on manual selection, (2) UI components for the theme, (3) user preference sync to DB.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Cookie Not Being Set on Manual Selection
|
||||
**What goes wrong:** Users who switch language via picker or URL prefix still get browser detection on next visit.
|
||||
**Why it happens:** LocaleMiddleware reads `locale_manually_set` cookie but it's never written.
|
||||
**How to avoid:** Add cookie setting in LocalePicker::onSwitchLocale() and routes.php when URL prefix is detected.
|
||||
**Warning signs:** Browser detection triggers even after user has explicitly chosen a language.
|
||||
|
||||
### Pitfall 2: User Preference Not Syncing to Database
|
||||
**What goes wrong:** Logged-in users switch language but it's not saved to their profile.
|
||||
**Why it happens:** LocalePicker only sets session, doesn't update `User::preferred_locale`.
|
||||
**How to avoid:** Extend onSwitchLocale to update `Auth::getUser()->preferred_locale` if authenticated.
|
||||
**Warning signs:** User logs in on another device and sees wrong language.
|
||||
|
||||
### Pitfall 3: Hreflang Without x-default
|
||||
**What goes wrong:** Search engines don't know which URL to use for unmatched languages.
|
||||
**Why it happens:** Missing x-default hreflang tag.
|
||||
**How to avoid:** Ensure AlternateHrefLangElements or custom implementation includes `<link rel="alternate" hreflang="x-default">` pointing to default locale URL.
|
||||
**Warning signs:** Search Console shows hreflang warnings.
|
||||
|
||||
### Pitfall 4: Modal Popup for Language Suggestion
|
||||
**What goes wrong:** Users instinctively close modals without reading.
|
||||
**Why it happens:** Using modal instead of dismissable banner.
|
||||
**How to avoid:** Use sticky top/bottom banner that doesn't block interaction.
|
||||
**Warning signs:** High dismissal rates, low language switch conversions.
|
||||
|
||||
### Pitfall 5: Snowboard data-request-success
|
||||
**What goes wrong:** Callbacks fail silently.
|
||||
**Why it happens:** WinterCMS Snowboard framework removed `data-request-success` as unsafe (uses eval).
|
||||
**How to avoid:** Use `Snowboard.request()` with `success` callback function.
|
||||
**Warning signs:** No callback execution after AJAX request.
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from existing codebase:
|
||||
|
||||
### Language Switcher Dropdown (Theme Implementation)
|
||||
```twig
|
||||
{# 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="true">
|
||||
<span class="language-switcher-label">{{ activeLocaleName }}</span>
|
||||
<svg class="language-switcher-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="language-switcher-list" role="menu">
|
||||
{% for code, name in locales %}
|
||||
<li role="none">
|
||||
<a href="#" role="menuitem"
|
||||
class="language-switcher-option {% if code == activeLocale %}active{% endif %}"
|
||||
data-request="localePicker::onSwitchLocale"
|
||||
data-request-data="locale: '{{ code }}'">
|
||||
{{ name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Language Suggestion Banner Component
|
||||
```php
|
||||
// plugins/golem15/translate/components/LocaleSuggestionBanner.php
|
||||
namespace Golem15\Translate\Components;
|
||||
|
||||
use Cms\Classes\ComponentBase;
|
||||
use Golem15\Translate\Classes\Translator;
|
||||
use Golem15\Translate\Models\Locale;
|
||||
use Request;
|
||||
use Config;
|
||||
|
||||
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
|
||||
if (Request::cookie('locale_manually_set')) {
|
||||
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) {
|
||||
$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
|
||||
\Cookie::queue('locale_banner_dismissed', '1', 60 * 24 * 7); // 7 days
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
public function onSwitchToSuggested()
|
||||
{
|
||||
$locale = post('locale');
|
||||
if (!Locale::isValid($locale)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Translator::instance()->setLocale($locale, true);
|
||||
|
||||
// Set manual selection cookie
|
||||
\Cookie::queue(
|
||||
Config::get('golem15.translate::browserDetection.manualSelectionCookie', 'locale_manually_set'),
|
||||
'1',
|
||||
Config::get('golem15.translate::browserDetection.manualSelectionExpiry', 525600)
|
||||
);
|
||||
|
||||
return \Redirect::refresh();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Banner Partial Template
|
||||
```twig
|
||||
{# components/localesuggestionbanner/default.htm #}
|
||||
{% if showBanner and suggestedLocale %}
|
||||
<div class="locale-suggestion-banner" data-locale-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="btn btn-sm btn-link"
|
||||
data-request="{{ __SELF__ }}::onDismissBanner"
|
||||
data-request-update="{ '{{ __SELF__ }}': '@self' }">
|
||||
<svg class="icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Syncing Preference to User Database
|
||||
```php
|
||||
// Extend LocalePicker::onSwitchLocale in Plugin.php boot()
|
||||
\Golem15\Translate\Components\LocalePicker::extend(function($component) {
|
||||
$component->bindEvent('component.beforeRunAjaxHandler', function($handler) use ($component) {
|
||||
if ($handler === 'onSwitchLocale') {
|
||||
$locale = post('locale');
|
||||
|
||||
// Set manual selection cookie
|
||||
\Cookie::queue(
|
||||
\Config::get('golem15.translate::browserDetection.manualSelectionCookie', 'locale_manually_set'),
|
||||
'1',
|
||||
\Config::get('golem15.translate::browserDetection.manualSelectionExpiry', 525600)
|
||||
);
|
||||
|
||||
// Update user preference in DB if logged in
|
||||
if (\Auth::check()) {
|
||||
$user = \Auth::getUser();
|
||||
$user->preferred_locale = $locale;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Account Settings Language Preference Section
|
||||
```twig
|
||||
{# Add to account/update.htm - new 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Modal popups for language | Non-modal sticky banners | 2022+ | Better UX, lower dismissal rates |
|
||||
| Auto-redirect on language detection | Suggestion banner + user choice | Best practice | SEO-safe, respects user intent |
|
||||
| Coupling language/region/currency | Decoupled preferences | Industry standard | Users can mix preferences |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `data-request-success` attribute: Removed from WinterCMS Snowboard as unsafe (eval). Use `Snowboard.request()` with callback.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Things that couldn't be fully resolved:
|
||||
|
||||
1. **x-default hreflang tag**
|
||||
- What we know: AlternateHrefLangElements generates per-locale links
|
||||
- What's unclear: Whether it includes x-default automatically
|
||||
- Recommendation: Verify in testing, add manually if missing
|
||||
|
||||
2. **URL prefix cookie setting location**
|
||||
- What we know: Cookie should be set when URL has locale prefix
|
||||
- What's unclear: Best place to intercept (routes.php vs middleware)
|
||||
- Recommendation: Test in routes.php first; middleware as fallback
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- `/plugins/golem15/translate/classes/LocaleMiddleware.php` - Priority cascade implementation
|
||||
- `/plugins/golem15/translate/classes/Translator.php` - Locale state management
|
||||
- `/plugins/golem15/translate/components/LocalePicker.php` - Component implementation
|
||||
- `/plugins/golem15/translate/components/AlternateHrefLangElements.php` - Hreflang generation
|
||||
- `/plugins/golem15/translate/config/config.php` - Default configuration
|
||||
- `/config/golem15/translate/config.php` - Project-specific config
|
||||
- `/plugins/golem15/user/models/User.php` - preferred_locale field
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- [Smashing Magazine: Designing Better Language Selector](https://www.smashingmagazine.com/2022/05/designing-better-language-selector/) - UX patterns for language selectors
|
||||
- [Google Search Central: Localized Versions](https://developers.google.com/search/docs/specialty/international/localized-versions) - Hreflang best practices
|
||||
- [TranslatePress: Automatic User Language Detection](https://translatepress.com/docs/addons/automatic-user-language-detection/) - Pop-up vs redirect patterns
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- WebSearch results on cookie banner patterns - General UX guidance
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH - Direct inspection of existing codebase
|
||||
- Architecture: HIGH - Verified from actual plugin implementation
|
||||
- Pitfalls: HIGH - Identified from code gaps and CLAUDE.md constraints
|
||||
- Code examples: MEDIUM - Based on existing patterns, needs testing
|
||||
|
||||
**Research date:** 2026-02-02
|
||||
**Valid until:** 2026-03-02 (30 days - stable infrastructure)
|
||||
Reference in New Issue
Block a user