Files
carei.pagedev.pl/.paul/phases/19-extras-translations-admin/19-01-PLAN.md
2026-04-22 22:00:50 +02:00

16 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
phase plan type wave depends_on files_modified autonomous delegation
19-extras-translations-admin 01 execute 1
17-01
wp-content/plugins/carei-reservation/includes/class-admin-panel.php
wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
false 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

<acceptance_criteria>

AC-1: Auto-collect PL names

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

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

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>

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ć ~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.
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

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