This commit is contained in:
2026-04-22 22:00:50 +02:00
parent 16be247ce1
commit e979fbe755
46 changed files with 5302 additions and 274 deletions

View File

@@ -0,0 +1,303 @@
---
phase: 19-extras-translations-admin
plan: 01
type: execute
wave: 1
depends_on: ["17-01"]
files_modified:
- wp-content/plugins/carei-reservation/includes/class-admin-panel.php
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
autonomous: false
delegation: off
---
<objective>
## Goal
Panel administratora do tłumaczenia nazw dodatkowych opcji (extras) zwracanych dynamicznie z API Softra. Plugin auto-zbiera wszystkie napotkane polskie nazwy extras do opcji WP (`carei_extras_seen`). W panelu `Rezerwacje → Tłumaczenia extras` admin widzi listę zebranych nazw PL i obok pole EN do wypełnienia. Gdy frontend pobiera pricelist w locale EN, REST endpoint automatycznie podmienia PL na EN override'y (fallback: PL gdy brak tłumaczenia).
## Purpose
Phase 18 przetłumaczyła statyczne stringi pluginu. Phase 17 zrobiła słownik błędów Softra (13 ręcznych mapowań). Ale dynamiczne pozycje z `/pricelist/list` (fotelik, GPS, dodatkowy kierowca, łańcuchy śniegowe itp.) są nadal po polsku w wersji EN — bo backend Softra zawsze odpowiada po polsku. Phase 19 zamyka tę lukę: admin ma UI gdzie raz wpisze tłumaczenia, plugin używa ich automatycznie.
## Output
- Option `carei_extras_seen` (array of unique PL names, updated runtime przy każdym pricelist request)
- Option `carei_extras_translations` (map PL → EN, managed przez admin)
- Submenu `wp-admin → Rezerwacje → Tłumaczenia extras` z formularzem: lista seen names + input EN + submit
- Helper `Carei_Admin_Panel::translate_extra_name($pl_name, $locale)` — wg locale + override'u, z fallbackiem do PL
- `Carei_REST_Proxy` przy zwracaniu pricelist: dla każdego `$item['name']` → dopisuje do seen + podmienia na EN jeśli locale = en
- Frontend bez zmian (JS już ma `?lang=` w requeście)
</objective>
<context>
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/phases/17-bilingual-packages-and-softra-errors/17-01-SUMMARY.md
@.paul/phases/18-en-translation/18-01-SUMMARY.md
@wp-content/plugins/carei-reservation/includes/class-admin-panel.php
@wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
</context>
<acceptance_criteria>
## AC-1: Auto-collect PL names
```gherkin
Given frontend woła `/wp-json/carei/v1/pricelist` (PL lub EN)
When Softra API zwraca `additionalItems` z polskimi nazwami
Then każda unikalna nazwa jest dopisana do opcji `carei_extras_seen`
And opcja jest arrayem stringów (deduplikowana)
And zapis do `carei_extras_seen` NIE blokuje response (fire-and-forget lub szybki update_option)
And kolejne żądania nie duplikują wpisów
```
## AC-2: Panel admin zarządza tłumaczeniami
```gherkin
Given admin wchodzi w `wp-admin Rezerwacje Tłumaczenia extras`
When widzi listę zebranych PL nazw
Then dla każdej nazwy jest pole `text` z obecnym tłumaczeniem EN (lub puste)
And istnieje przycisk `Zapisz tłumaczenia`
And nagłówek strony i etykiety są po EN/PL zgodnie z admin locale (przez __())
When admin wpisuje tłumaczenia i klika Zapisz
Then opcja `carei_extras_translations` zostaje zaktualizowana (sanitize_text_field na każdym value)
And redirect z flagą `?carei_saved=1` pokazuje komunikat "Zapisano."
And przy następnej wizycie panelu tłumaczenia są wyświetlone zgodnie z zapisem
```
## AC-3: REST pricelist zwraca EN nazwy gdy są override
```gherkin
Given w `carei_extras_translations` są zapisane tłumaczenia np. "Fotelik dziecięcy" "Child car seat"
When frontend EN (Polylang = EN albo `?lang=en`) woła `/pricelist`
Then response zawiera `additionalItems` z `name` podmienionym wg override
And dla nazw bez override response zawiera oryginalny PL (graceful fallback)
And inne pola pozycji (`price`, `code`, `maxPrice` itp.) nietknięte
When frontend PL woła ten sam endpoint
Then response zawiera oryginalne PL nazwy (bez zmian względem obecnego zachowania zero regresji)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Admin panel submenu + helper + zapisy w options</name>
<files>wp-content/plugins/carei-reservation/includes/class-admin-panel.php</files>
<action>
1. Dodaj stałe klasy:
```php
const EXTRAS_SEEN_OPTION = 'carei_extras_seen';
const EXTRAS_TRANSLATIONS_OPTION = 'carei_extras_translations';
```
2. Dodaj static helpery:
```php
public static function get_extras_seen() {
$seen = get_option( self::EXTRAS_SEEN_OPTION, array() );
return is_array( $seen ) ? array_values( array_unique( array_filter( array_map( 'strval', $seen ) ) ) ) : array();
}
public static function get_extras_translations() {
$map = get_option( self::EXTRAS_TRANSLATIONS_OPTION, array() );
return is_array( $map ) ? $map : array();
}
public static function remember_extra_name( $pl_name ) {
$pl_name = trim( (string) $pl_name );
if ( $pl_name === '' ) return;
$seen = self::get_extras_seen();
if ( ! in_array( $pl_name, $seen, true ) ) {
$seen[] = $pl_name;
sort( $seen );
update_option( self::EXTRAS_SEEN_OPTION, $seen, false ); // autoload=false (może być duża lista)
}
}
public static function translate_extra_name( $pl_name, $locale = null ) {
if ( $locale === null ) {
$locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
$locale = ( 0 === strpos( (string) $locale, 'en' ) ) ? 'en' : 'pl';
}
if ( $locale !== 'en' ) return $pl_name;
$map = self::get_extras_translations();
if ( isset( $map[ $pl_name ] ) && $map[ $pl_name ] !== '' ) {
return $map[ $pl_name ];
}
return $pl_name; // fallback
}
```
3. Zarejestruj submenu w `register_protection_packages_page()` lub osobną metodą `register_extras_translations_page()` (prefer osobną dla czystości):
```php
add_action( 'admin_menu', array( $this, 'register_extras_translations_page' ) );
add_action( 'admin_post_carei_save_extras_translations', array( $this, 'handle_extras_translations_save' ) );
```
I w konstruktorze dopisz akcje.
4. Metoda `register_extras_translations_page()`:
```php
public function register_extras_translations_page() {
add_submenu_page(
'edit.php?post_type=' . self::POST_TYPE,
__( 'Tłumaczenia extras', 'carei-reservation' ),
__( 'Tłumaczenia extras', 'carei-reservation' ),
'manage_options',
'carei-extras-translations',
array( $this, 'render_extras_translations_page' )
);
}
```
5. Metoda `render_extras_translations_page()`:
- Tytuł strony, komunikat `?carei_saved=1`
- Formularz POST do `admin-post.php` z nonce
- Tabela 2 kolumny: `Nazwa PL` (readonly text), `Nazwa EN` (input text)
- Dla każdej nazwy z `get_extras_seen()` — wiersz z inputem `name="translations[PL_NAME]"` i obecnym value
- Submit button `Zapisz tłumaczenia`
- Info tekst: "Puste pole = fallback do wersji polskiej w wersji EN strony."
- Jeśli `get_extras_seen()` pusty: info "Brak zebranych pozycji — otwórz formularz rezerwacji aby załadować pricelist."
6. Metoda `handle_extras_translations_save()`:
- Check nonce + capability
- Iteracja po `$_POST['translations']` (array) — każdy klucz PL, value EN
- Sanitize: `sanitize_text_field( wp_unslash( $value ) )`
- Zbuduj clean map
- `update_option( self::EXTRAS_TRANSLATIONS_OPTION, $clean, false )`
- Redirect z `?carei_saved=1`
7. Wszystkie user-facing stringi przez `__()`/`esc_html__()`/`esc_attr__()` z textdomain `carei-reservation`.
Unikaj:
- Blokowania request frontendu na zapis seen — `update_option` z autoload=false jest szybki, ale jeśli zaburza latency — użyj `wp_cache_set` przed `update_option` (mikro-optymalizacja, na razie pomiń)
- Duplikowania nazw (array_unique)
- Auto-tłumaczenia przez AI (scope tego planu: tylko admin override)
</action>
<verify>
1. `wp-admin → Rezerwacje → Tłumaczenia extras` — strona się renderuje, widać info "Brak zebranych pozycji" (bo option jeszcze pusta)
2. `php -l` class-admin-panel.php → No syntax errors
3. Wywołanie helpera: `var_dump( Carei_Admin_Panel::translate_extra_name( 'Test', 'en' ) )` → `'Test'` (fallback)
</verify>
<done>AC-2 (częściowo — render panelu), infrastruktura dla AC-1 i AC-3.</done>
</task>
<task type="auto">
<name>Task 2: REST /pricelist auto-collect + per-locale name replacement</name>
<files>wp-content/plugins/carei-reservation/includes/class-rest-proxy.php</files>
<action>
1. Znajdź handler endpointu `/pricelist` (prawdopodobnie `get_pricelist()` lub podobna metoda) w class-rest-proxy.php.
2. Przed zwrotem response — iteracja po `additionalItems` (lub odpowiedniku):
```php
$locale = $this->resolve_locale( $request ); // helper z Phase 17
$translations = Carei_Admin_Panel::get_extras_translations();
foreach ( $pricelists as &$pricelist ) {
if ( ! isset( $pricelist['additionalItems'] ) || ! is_array( $pricelist['additionalItems'] ) ) continue;
foreach ( $pricelist['additionalItems'] as &$item ) {
if ( ! isset( $item['name'] ) || ! is_string( $item['name'] ) ) continue;
$pl_name = trim( $item['name'] );
if ( $pl_name === '' ) continue;
// Auto-collect
Carei_Admin_Panel::remember_extra_name( $pl_name );
// Translate if locale=en and override exists
if ( $locale === 'en' && isset( $translations[ $pl_name ] ) && $translations[ $pl_name ] !== '' ) {
$item['name'] = $translations[ $pl_name ];
}
}
unset( $item );
}
unset( $pricelist );
```
3. Upewnij się że resolve_locale działa tutaj (jest publiczny lub private dostępny w tej samej klasie — Phase 17 dodał jako private, więc OK bo to ta sama klasa).
4. Struktura response NIE zmienia się — frontend JS dostaje te same klucze, tylko `name` może być podmienione.
5. Jeśli Softra zwraca `error` zamiast success — nie ruszamy, passthrough.
Unikaj:
- Modyfikacji cached response (jeśli pricelist jest cached — lepiej żeby cache też uwzględniał locale, ale na razie zostaw cache PL a mapowanie w locie; jeśli `/pricelist` NIE ma cache po stronie PHP, to proste)
- Dodawania `name_en` w response — zachowujemy schemat (tylko `name` rozwiązane per locale)
</action>
<verify>
1. `php -l` class-rest-proxy.php → No syntax errors
2. Wywołanie z fronta w PL: pricelist names po polsku (bez zmian)
3. Wywołanie z fronta w EN: nazwy spotkane po raz pierwszy → zapisane w `carei_extras_seen`, nadal po polsku w response (bo brak tłumaczeń)
4. Po wypełnieniu tłumaczeń w adminie i kolejnym requeście EN: nazwy z override podmienione
</verify>
<done>AC-1 i AC-3 satysfakcjonowane.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- Submenu `wp-admin → Rezerwacje → Tłumaczenia extras` z listą zebranych nazw + pola EN
- Helpery w Admin_Panel: `remember_extra_name()`, `get_extras_seen()`, `get_extras_translations()`, `translate_extra_name()`
- Auto-collect: każdy request `/pricelist` zbiera unikalne nazwy PL do option `carei_extras_seen`
- Per-locale replacement: EN requesty dostają override z `carei_extras_translations` (fallback: PL)
- Zero zmian w JS frontendu (już jest `?lang=` z Phase 17)
</what-built>
<how-to-verify>
1. Deploy 2 pliki PHP (`class-admin-panel.php`, `class-rest-proxy.php`) przez SFTP
2. **Auto-collect test:**
- Otwórz stronę z modalem rezerwacji w PL, wypełnij krok 1 (daty + oddział + klasa) — pricelist się załaduje
- Wejdź `wp-admin → Rezerwacje → Tłumaczenia extras` → lista PL nazw jest wypełniona (powinno być ~515 pozycji, zależnie od Softra)
3. **Admin zapis:**
- Wypełnij kilka tłumaczeń EN (np. "Fotelik dziecięcy" → "Child car seat", "GPS / Nawigacja" → "GPS / Navigation", "Dodatkowy kierowca" → "Additional driver")
- Kliknij "Zapisz tłumaczenia" → komunikat "Zapisano" pojawia się
- Odśwież stronę → tłumaczenia wciąż tam
4. **Frontend PL:**
- Modal w PL, sekcja "Opcje dodatkowe" — nazwy po polsku (identyczne jak przed zmianą)
5. **Frontend EN:**
- Polylang switcher → EN
- Modal otwiera się, "Additional options" → nazwy które wypełniłeś = EN, reszta = PL (fallback)
- `document.documentElement.lang` = "en-*" → `?lang=en` w requeście → backend zwraca EN names
6. **DevTools Network:**
- `GET /wp-json/carei/v1/pricelist?...&lang=en` → response zawiera `additionalItems[i].name` z EN (gdzie jest override) lub PL (gdzie brak)
- `GET ...&lang=pl` → wszystko po PL
7. **Test pustego override:**
- Wyczyść jedno pole EN w adminie, zapisz
- Frontend EN → ta pozycja wraca do PL (fallback)
8. **Zero regresji:**
- Pełen flow rezerwacji w PL działa bez zmian
- Panel pakietów ochronnych (Phase 13/17) bez zmian
- Inne widgety (mapa, miasta, oddziały) bez zmian
**Kryterium przejścia:** admin może raz wypełnić tłumaczenia → frontend EN automatycznie używa. Zero regresji w PL.
</how-to-verify>
<resume-signal>Napisz "approved" aby zamknąć plan, albo opisz problemy.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Phase 17 struktura `Carei_REST_Proxy::resolve_locale()` (reuse, nie refactor)
- Phase 17 helper `Carei_Softra_API::map_error_message()` — osobny mechanizm dla błędów
- Phase 13 panel pakietów ochronnych — osobna strona w menu
- Schema CPT `carei_reservation`, meta keys, slugi statusów
- Struktura payloadów do Softra (klucze JSON `additionalItems`, `price`, `code` itp.) — tylko `name` per locale
- Frontend JS — bez zmian (już pobiera z `?lang=`)
- Phase 18 `.po`/`.mo` — statyczne stringi, nie ruszamy
## SCOPE LIMITS
- Nie tłumaczymy nazw krajów w sekcji wyjazdu zagranicznego (lookup keys, dane biznesowe COUNTRY_FLAGS)
- Nie tłumaczymy nazw klas pojazdów z Softra (np. „Opel Astra Combi") — dane biznesowe
- Nie auto-tłumaczymy przez AI — tylko admin override
- Nie dodajemy CSV export/import tłumaczeń — jeśli user zgłosi potrzebę, osobna faza
- Nie cacheujemy mapy tłumaczeń separately — WP options autoload cache wystarczy
- Nie ruszamy `/protection-packages` endpoint (Phase 17 territory)
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] `grep -n "carei_extras_seen\|carei_extras_translations" includes/` → oba klucze w kodzie
- [ ] Panel `wp-admin → Rezerwacje → Tłumaczenia extras` renderuje się bez błędów
- [ ] Auto-collect działa (po jednym request pricelist, lista seen nie pusta)
- [ ] Save tłumaczeń update'uje option z komunikatem success
- [ ] REST pricelist PL = PL names, EN z override = EN, EN bez override = PL fallback
- [ ] `php -l` na obu plikach → No syntax errors
- [ ] Zero regresji w PL (human-verify)
- [ ] AC-1, AC-2, AC-3 pass
</verification>
<success_criteria>
- 2 auto tasks ukończone
- Checkpoint human-verify zatwierdzony
- Plugin pozwala adminowi zarządzać tłumaczeniami extras przez UI
- Kompletna dwujęzyczność (Phase 16+17+18+19 razem) — żadna część UI nie pokazuje PL gdy user jest w EN (poza świadomymi wyjątkami: nazwy miast, krajów, klas pojazdów)
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/19-extras-translations-admin/19-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,129 @@
---
phase: 19-extras-translations-admin
plan: 01
subsystem: i18n
tags: [admin-panel, extras, polylang, softra-pricelist, bilingual]
requires:
- phase: 17-bilingual-packages-and-softra-errors
provides: resolve_locale() helper w REST proxy
provides:
- Option carei_extras_seen (auto-collected PL names z Softra pricelist)
- Option carei_extras_translations (admin override PL → EN)
- Submenu wp-admin → Rezerwacje → Tłumaczenia extras
- Helpery: remember_extra_name, get_extras_seen, get_extras_translations, translate_extra_name
- REST /pricelist per-locale name replacement z fallbackiem do PL
affects: []
tech-stack:
added: []
patterns:
- "Auto-collect + admin override: seen list zbiera się runtime, admin wypełnia override, fallback do oryginału"
- "Option z autoload=false — lista seen może rosnąć, nie blokuje boot WP"
key-files:
modified:
- wp-content/plugins/carei-reservation/includes/class-admin-panel.php
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
key-decisions:
- "Auto-collect przez update_option z autoload=false — prosty, brak wymogu osobnej tabeli DB"
- "Sortowanie seen alphabetically z SORT_NATURAL | SORT_FLAG_CASE — przyjazna kolejność w UI"
- "Zero zmian w JS frontendu — `?lang=` z Phase 17 wystarcza"
- "Nonce + sanitize_text_field na EN inputach — standardowa walidacja WP"
- "Fallback do PL dla pustych/niezdefiniowanych override'ów — graceful degradation"
patterns-established:
- "Admin panel UI pattern: lista seen (readonly) + input override (editable) + submit z nonce"
duration: ~20min
started: 2026-04-22
completed: 2026-04-22
---
# Phase 19 Plan 01: Extras translations admin panel — Summary
**Panel `wp-admin → Rezerwacje → Tłumaczenia extras` pozwala administratorowi zarządzać tłumaczeniami dynamicznych nazw opcji dodatkowych zwracanych z Softra API. Auto-collect PL nazw + override EN + fallback do PL. Milestone v0.8 — 100% complete.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~20min |
| Tasks | 2 auto + 1 human-verify completed |
| Files modified | 2 |
| New options in WP DB | 2 (seen, translations) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Auto-collect PL names | Pass | Każdy request `/pricelist` dopisuje nowe nazwy do `carei_extras_seen` |
| AC-2: Panel admin zarządza tłumaczeniami | Pass | Submenu, formularz z nonce, sanitize, redirect z komunikatem |
| AC-3: REST pricelist zwraca EN gdy override | Pass | Per-locale replacement z fallbackiem do PL |
## Accomplishments
- **2 nowe WP options:** `carei_extras_seen` (lista PL names) + `carei_extras_translations` (map PL → EN)
- **4 static helpery** w `Carei_Admin_Panel` — czysta API do używania z innych klas
- **Submenu admin** z pełnym UX: lista alfabetyczna, info placeholder dla pustej listy, komunikaty sukcesu, nonce protection
- **REST `/pricelist` integration** — runtime auto-collect + per-locale replacement, bez dotykania JS frontendu
- **Reuse Phase 17** `resolve_locale()` helper — brak duplikacji logiki
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `includes/class-admin-panel.php` | Modified | Stałe, helpery, submenu, render, save handler |
| `includes/class-rest-proxy.php` | Modified | `get_pricelist()` auto-collect + per-locale replacement |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| WP options zamiast custom table | Lista seen i map override są małe (<100 wpisów) — nadmierna inżynieria | Szybki deploy, standardowy WP pattern |
| `autoload=false` dla options | Lista seen rośnie z czasem — nie ładujemy jej przy każdym request | Zero impact na WP boot performance |
| Fallback do PL dla pustego override | User może nie tłumaczyć wszystkiego — nigdy nie crashujemy | Graceful degradation, spójna z Phase 17 |
| Sortowanie SORT_NATURAL | UX: "Fotelik 1+2" przed "Fotelik 10" w liście | Przyjazna kolejność w panelu admin |
| Zero zmian w JS | Phase 17 `?lang=` już działa, frontend nie musi wiedzieć o override | Minimalna surface area zmian |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Plan wykonany 1:1.
## Issues Encountered
None.
## Next Phase Readiness
**Milestone v0.8 COMPLETE.**
**System tłumaczeń teraz kompletny:**
- Statyczne stringi PHP/JS: `.po`/`.mo` (Phase 16+18)
- Pakiety ochronne: pola `_en` w DB (Phase 17)
- Błędy Softra: słownik 13 wpisów w `map_error_message()` (Phase 17)
- Dynamiczne extras: admin override (Phase 19)
**Co pozostaje po polsku w wersji EN (świadome):**
- Nazwy miast w widgetach mapa/miasta/oddziały — dane biznesowe
- Nazwy krajów w sekcji wyjazdu zagranicznego — lookup COUNTRY_FLAGS
- Nazwy klas pojazdów z Softra (np. "Opel Astra Combi") — dane biznesowe
**Concerns:** None. Admin musi wypełnić override dla nowych Softra extras — akceptowalny manual overhead.
**Blockers:** None.
---
*Phase: 19-extras-translations-admin, Plan: 01*
*Completed: 2026-04-22*