---
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
---
## 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)
@.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
## 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)
```
Task 1: Admin panel submenu + helper + zapisy w options
wp-content/plugins/carei-reservation/includes/class-admin-panel.php
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)
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)
AC-2 (częściowo — render panelu), infrastruktura dla AC-1 i AC-3.
Task 2: REST /pricelist auto-collect + per-locale name replacement
wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
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)
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
AC-1 i AC-3 satysfakcjonowane.
- 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)
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ć ~5–15 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.
Napisz "approved" aby zamknąć plan, albo opisz problemy.
## 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)
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
- 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)