--- 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) Po zakończeniu: `.paul/phases/19-extras-translations-admin/19-01-SUMMARY.md`