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,305 @@
---
phase: 17-bilingual-packages-and-softra-errors
plan: 01
type: execute
wave: 1
depends_on: ["16-01"]
files_modified:
- wp-content/plugins/carei-reservation/includes/class-admin-panel.php
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
- wp-content/plugins/carei-reservation/includes/class-softra-api.php
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js (drobne — użycie istniejących reject* kluczy)
autonomous: false
delegation: off
---
<objective>
## Goal
(1) Panel administratora pakietów ochronnych SOFT/PREMIUM dostaje dodatkowe pola `name_en` i `description_en`. REST endpoint `/protection-packages` zwraca wariant zgodny z aktualnym locale (`get_locale()` lub parametr `?lang=`): PL → pola bazowe, EN → pola `_en` z fallbackiem do bazowych gdy puste. (2) Słownik mapowania ~1215 typowych komunikatów Softra API (PL stringi zwracane przez zewnętrzny system) na lokalizowane klucze — warstwa w `Carei_Softra_API` / `Carei_REST_Proxy` podmienia `message` w `WP_Error`/response na tekst wg current locale.
## Purpose
Bez Phase 17 użytkownik na wersji EN widziałby (1) polskie nazwy pakietów ochronnych wprowadzone przez admina w panelu WP, (2) polskie komunikaty z Softra API przy konflikcie rezerwacji / braku pojazdu / błędzie walidacji. Phase 17 domyka jedyne dwa źródła „obcych" polskich tekstów pozostałych po Phase 16 — po niej całość UI jest dwujęzyczna bez wyjątków.
## Output
- Panel `Rezerwacje → Pakiety ochronne`: każdy pakiet (SOFT/PREMIUM) ma 4 pola tekstowe zamiast 2 — `name` + `name_en`, `description` + `description_en` (cena `pricePerDay` pozostaje jedna, wspólna)
- Option `carei_protection_packages` w DB ma nową strukturę: `soft: {name, name_en, description, description_en, pricePerDay, enabled}`, analogicznie premium
- REST `/protection-packages` zwraca wariant zlokalizowany wg `determine_locale()` — klucze `name`/`description` w odpowiedzi są już właściwe dla języka (EN lub PL), frontend nie musi wiedzieć nic o wariantach
- Słownik mapowania w `Carei_Softra_API` (nowa metoda `map_error_message( $pl_message )`) — zwraca sparowany klucz tłumaczenia z textdomain `carei-reservation` dla znanych komunikatów, albo oryginał jeśli brak mapowania
- Komunikaty w REST response / `WP_Error` przechodzą przez filtr mapowania przed zwróceniem do frontu
- Zero regresji w wersji PL — wszystkie pakiety i błędy wyświetlają się identycznie jak po Phase 16
</objective>
<context>
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/phases/13-protection-packages/13-02-SUMMARY.md
@.paul/phases/16-i18n-plugin-refactor/16-01-SUMMARY.md
@wp-content/plugins/carei-reservation/includes/class-admin-panel.php
@wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
@wp-content/plugins/carei-reservation/includes/class-softra-api.php
@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
</context>
<acceptance_criteria>
## AC-1: Panel pakietów obsługuje pola _en
```gherkin
Given administrator wchodzi w `wp-admin Rezerwacje Pakiety ochronne`
When wypełnia pola `Nazwa` / `Nazwa (EN)` / `Opis` / `Opis (EN)` dla SOFT i PREMIUM i klika "Zapisz"
Then wszystkie 4 pola tekstowe per pakiet są zapisane w option `carei_protection_packages`
And po odświeżeniu strony formularz pokazuje zapisane wartości (w tym EN)
And walidacja/sanityzacja jest konsystentna między polami PL i EN
And brak EN (puste pole `name_en`/`description_en`) jest poprawny to oznacza fallback do PL"
```
## AC-2: REST endpoint zwraca wariant per locale
```gherkin
Given w DB są zapisane pakiety z polami PL i EN
When frontend PL woła `/wp-json/carei/v1/protection-packages` (locale = pl_PL)
Then odpowiedź zawiera `soft.name` i `soft.description` z wariantu PL
And analogicznie dla premium
When frontend EN (Polylang język EN) woła ten sam endpoint
Then odpowiedź zawiera `soft.name` = wartość `name_en` (lub PL jeśli pole EN puste)
And `soft.description` = `description_en` z fallbackiem
And struktura odpowiedzi nie zmienia kluczy (`name`, `description`, `pricePerDay`, `enabled`) tylko treści
And frontend JS nie wymaga żadnej zmiany logicznej (oprócz ewentualnego `?lang=` jeśli Polylang nie ustawia locale serwerowo)
```
## AC-3: Błędy Softra mapowane na lokalizowane stringi
```gherkin
Given użytkownik próbuje zarezerwować pojazd niedostępny w danym terminie
When Softra API zwraca message "Brak dostępnego pojazdu w wybranym terminie" (lub podobny)
Then `Carei_Softra_API::map_error_message()` rozpoznaje string
And zwraca lokalizowany wariant zgodny z current locale (`__('rejectCarNotFound', ...)` lub bezpośredni tekst EN)
And REST response `message` zawiera tekst w języku zgodnym z UI
And dla nieznanych komunikatów przepuszcza oryginał (graceful fallback)
And słownik pokrywa co najmniej 12 typowych komunikatów (lista w Task 2)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Panel admina — pola name_en / description_en + zapis/odczyt</name>
<files>wp-content/plugins/carei-reservation/includes/class-admin-panel.php</files>
<action>
1. Zaktualizuj `get_protection_packages_defaults()` — dodaj domyślne klucze `name_en` i `description_en` per pakiet (wartości puste albo angielskie defaulty typu `'Protection SOFT'` / `'Protection PREMIUM'`).
2. Zaktualizuj `get_protection_packages()` — metodę czyszczenia/merge z defaultami, żeby obsłużyła nowe pola (użytkownicy z starą strukturą dostają puste EN, bez crasha).
3. W `render_protection_packages_page()`:
- Dla każdego pakietu (SOFT, PREMIUM) dodać pod polami PL dwa kolejne wiersze:
- `<input type="text" name="packages[soft][name_en]">` z labelką `esc_html__( 'Nazwa (EN)', 'carei-reservation' )`
- `<textarea name="packages[soft][description_en]">` z labelką `esc_html__( 'Opis (EN)', 'carei-reservation' )`
- Wizualnie oddzielone (np. `<small>` note: „puste = fallback do wersji polskiej")
- Placeholdery w `esc_attr__()`
4. W `handle_protection_packages_save()`:
- Sanitize `name_en` przez `sanitize_text_field()`
- Sanitize `description_en` przez `sanitize_textarea_field()`
- Zapisz oba pola w tej samej strukturze co PL
- Nie ruszaj walidacji `pricePerDay` ani `enabled` — bez zmian
5. Nie zmieniaj slugów meta, endpointu REST (Task 2), żadnej logiki biznesowej poza polem form.
Unikaj:
- Dodawania nowego mechanizmu po stronie DB (np. osobnej opcji dla EN) — wszystko w istniejącym `carei_protection_packages`
- Tłumaczenia opisu na bieżąco w panelu admin — admin wpisuje ręcznie EN
- Refaktoru istniejącej struktury defaulti — tylko DODAJ klucze
</action>
<verify>
1. Otwórz `wp-admin → Rezerwacje → Pakiety ochronne` — widoczne 4 pola tekstowe per pakiet (PL name, EN name, PL description, EN description) + cena + enabled
2. Wpisz testowe wartości w EN, zapisz → po odświeżeniu zachowane
3. `get_option( 'carei_protection_packages' )` w wp-admin → Tools → Site Health → Info (lub `wp db query` na locale) zawiera nowe klucze `name_en`, `description_en`
4. `php -l class-admin-panel.php` → No syntax errors
5. Stary kod readujący `$packages['soft']['name']` dalej działa (brak BC break)
</verify>
<done>AC-1 satysfakcjonowane.</done>
</task>
<task type="auto">
<name>Task 2: REST endpoint per-locale + mapowanie błędów Softra</name>
<files>
wp-content/plugins/carei-reservation/includes/class-rest-proxy.php,
wp-content/plugins/carei-reservation/includes/class-softra-api.php
</files>
<action>
**Część A — REST `/protection-packages` zwraca wariant zlokalizowany:**
1. W `class-rest-proxy.php` odnajdź handler endpointu `/protection-packages` (prawdopodobnie metoda typu `get_protection_packages()`).
2. Dodaj helper `resolve_locale()`:
```php
private function resolve_locale( $request ) {
$lang = $request->get_param( 'lang' );
if ( $lang && in_array( strtolower( $lang ), array( 'pl', 'en' ), true ) ) {
return strtolower( $lang );
}
$locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
return ( 0 === strpos( $locale, 'en' ) ) ? 'en' : 'pl';
}
```
- Polylang przy żądaniach REST powinien ustawić locale automatycznie (filter `locale` lub `determine_locale`). Gdyby nie — frontend może dopisać `?lang=en` jawnie (fallback). JS zostaje bez zmian jeśli Polylang dobrze współpracuje z WP REST.
3. W handlerze endpointu: po pobraniu `$packages = Carei_Admin_Panel::get_protection_packages()`:
- Dla lokalu `'en'`: podmień `$pkg['name']` na `$pkg['name_en']` gdy niepuste, inaczej pozostaw `$pkg['name']`. Analogicznie `description`.
- Dla lokalu `'pl'`: bez zmian.
- Usuń z odpowiedzi klucze `name_en`/`description_en` (frontend nie musi ich widzieć — unika leakowania i mylenia schematu).
4. Odpowiedź REST zachowuje schemat: `{ soft: { name, description, pricePerDay, enabled }, premium: {...} }`.
**Część B — Mapowanie błędów Softra:**
5. W `class-softra-api.php` dodaj nową public static method:
```php
public static function map_error_message( $original_message ) {
if ( ! is_string( $original_message ) || '' === trim( $original_message ) ) {
return $original_message;
}
$dict = array(
'Brak dostępnego pojazdu w wybranym terminie' => __( 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.', 'carei-reservation' ),
'Nieprawidłowy zakres dat' => __( 'Nieprawidłowy zakres dat', 'carei-reservation' ),
'Nie znaleziono oddziału' => __( 'Nie znaleziono oddziału', 'carei-reservation' ),
'Klient o tych danych już istnieje' => __( 'Klient o tych danych już istnieje w systemie', 'carei-reservation' ),
'Nieprawidłowy numer PESEL' => __( 'Nieprawidłowy numer PESEL', 'carei-reservation' ),
'Cennik wygasł' => __( 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.', 'carei-reservation' ),
'Token wygasł' => __( 'Sesja wygasła. Odśwież stronę.', 'carei-reservation' ),
'Nieprawidłowe dane logowania' => __( 'Błąd autoryzacji API. Skontaktuj się z administratorem.', 'carei-reservation' ),
'Brak uprawnień' => __( 'Brak uprawnień do wykonania operacji.', 'carei-reservation' ),
'Błąd serwera' => __( 'Błąd serwera. Spróbuj ponownie za chwilę.', 'carei-reservation' ),
'Przekroczono limit rezerwacji' => __( 'Przekroczono limit rezerwacji dla tego klienta.', 'carei-reservation' ),
'Nieprawidłowy numer telefonu' => __( 'Podaj poprawny numer telefonu (min. 9 cyfr).', 'carei-reservation' ),
'Wymagane pole' => __( 'To pole jest wymagane.', 'carei-reservation' ),
);
// Exact match
if ( isset( $dict[ $original_message ] ) ) {
return $dict[ $original_message ];
}
// Fuzzy: prefix match (Softra bywa mało przewidywalny z końcówkami)
foreach ( $dict as $pl_key => $translated ) {
if ( 0 === stripos( $original_message, $pl_key ) ) {
return $translated;
}
}
return $original_message; // graceful fallback
}
```
- Zasada: `__()` przechodzi przez textdomain `carei-reservation` → dla locale EN pobiera tłumaczenie z `.mo` (Phase 18). W PL zwraca ten sam tekst co msgid (co jest OK — zachowuje polski oryginał).
6. Zintegruj mapowanie:
- W miejscach gdzie `Carei_Softra_API` zwraca błąd (np. metoda `make_booking`, `get_car_classes`, itp.) — po odebraniu `$response['error']['message']` lub `$response['message']` (sprawdź strukturę w istniejącym kodzie) zawiń przez `self::map_error_message( $msg )` przed utworzeniem `WP_Error` / przed zwrotem.
- Jeśli API używa `WP_Error` już z warstwy `rest-proxy` — zaktualizuj tam (przechwyć message, mapuj, przekaż dalej).
**Część C — Frontend (drobna zmiana w JS):**
7. W `carei-reservation.js` w miejscach gdzie łapiesz `err.message` z odpowiedzi API przy niepowodzeniu rezerwacji — jeśli obecnie wyświetlasz surowy message, zostaw bez zmian (backend już mapuje). Jeśli masz heurystykę kategoryzacji po kluczach (np. "no car available" → pokaż specjalny alert), rozważ aktualizację. W MINIMUM: upewnij się że fallbacki `rejectCarNotFound` itd. w `careiI18n` są używane gdzie trzeba. Prawdopodobnie zmiana zero/minimal.
8. Jeśli frontend musi jawnie dopisać `?lang=` przy requeście `/protection-packages` (bo Polylang nie ustawia locale REST automatycznie) — dodaj to w `loadProtectionPackages()`:
```js
var lang = (document.documentElement.lang || '').toLowerCase().indexOf('en') === 0 ? 'en' : 'pl';
fetch(REST_URL + 'protection-packages?lang=' + lang, {...})
```
Unikaj:
- Tłumaczenia komunikatów po stronie JS — wszystko leci z PHP (`__()` rozwiązuje per locale)
- Wprowadzania nowego schemat odpowiedzi (`name_pl` / `name_en` w payloadzie) — odpowiedź zawsze ma `name` w właściwym języku
- Nadpisywania obecnej logiki reject — tylko dodaj warstwę mapowania
</action>
<verify>
1. `php -l` dla obu zmienionych plików → No syntax errors
2. W PL (Polylang = PL): `curl /wp-json/carei/v1/protection-packages` → `soft.name` = polska nazwa, `soft.description` = polski opis
3. W EN (Polylang = EN) albo `?lang=en`: `soft.name` = angielska nazwa (lub polska fallback gdy puste)
4. Odpowiedź REST NIE zawiera kluczy `name_en`/`description_en` (czyste API)
5. Wywołanie błędnej rezerwacji (niedostępny pojazd) → komunikat zwrócony do frontendu = tłumaczenie z `__()` zamiast surowego Softra-PL (w EN)
6. Nieznany komunikat Softra (spoza słownika) → passthrough bez zmian (graceful)
</verify>
<done>AC-2, AC-3 satysfakcjonowane.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- Panel `wp-admin → Rezerwacje → Pakiety ochronne` ma teraz 4 pola tekstowe per pakiet (`name`, `name_en`, `description`, `description_en`) + cena + enabled
- Option `carei_protection_packages` przechowuje wersje PL i EN każdego pakietu
- REST `/protection-packages` zwraca wariant per locale (Polylang EN → pola `_en` z fallbackiem do PL)
- Słownik `Carei_Softra_API::map_error_message()` z 12+ wpisami + exact/fuzzy match, wbudowany w ścieżkę zwrotu błędów API
- Wersja PL — zero regresji, wersja EN — pakiety i błędy w języku EN (po załadowaniu .mo w Phase 18)
</what-built>
<how-to-verify>
1. Wypchnij zmienione 34 pliki przez SFTP
2. **Admin panel test:**
- `wp-admin → Rezerwacje → Pakiety ochronne`
- Sprawdź że są widoczne nowe pola: „Nazwa (EN)", „Opis (EN)" dla SOFT i PREMIUM
- Wypełnij testowo: SOFT EN name = „SOFT Protection", EN description = „Basic protection package..."
- PREMIUM EN name = „PREMIUM Protection", EN description = „Enhanced protection..."
- Kliknij „Zapisz" → odśwież stronę → wartości EN są zachowane
3. **Frontend PL (Polylang = PL):**
- Otwórz modal rezerwacji → sekcja „Pakiety ochronne"
- Kafelki SOFT/PREMIUM pokazują **polskie** nazwy i opisy (jak zapisane w polach PL)
- Cena `X.XX zł/doba` — bez zmian
4. **Frontend EN (Polylang = EN):**
- Przełącz język na EN w switcherze Polylang
- Otwórz modal rezerwacji → sekcja „Pakiety ochronne"
- Kafelki pokazują **angielskie** nazwy i opisy (wartości z pól `_en`)
- Jeśli któreś pole `_en` zostawiłeś puste → frontend powinien pokazać PL wariant (fallback)
5. **Test fallbacku:**
- Wróć do admin → pole EN jednego pakietu wyczyść
- Frontend EN → ten pakiet pokazuje polski oryginał (graceful)
6. **Error mapping test:**
- Wymuś błąd Softra (np. data w przeszłości albo niedostępny pojazd — jeśli API tak odpowiada)
- PL: komunikat w języku polskim (bez zmian)
- EN: komunikat po angielsku (po załadowaniu .mo w Phase 18) albo oryginalne PL jeśli Phase 18 jeszcze niedostępne
- Uwaga: **dopóki .po/.mo nie istnieją (Phase 18), `__()` zwraca oryginalny polski string w obu językach.** To jest OK — Phase 17 dostarcza infrastrukturę, Phase 18 dostarcza treść.
7. **DevTools Network:**
- `GET /wp-json/carei/v1/protection-packages` w PL → odpowiedź z PL
- `GET /wp-json/carei/v1/protection-packages?lang=en` → odpowiedź z EN
- Oba responsy nie zawierają pól `name_en`/`description_en` w payloadzie
8. **Brak regresji:**
- Cały flow rezerwacji PL działa jak po Phase 16 — nic się nie popsuło
**Kryterium przejścia:** Admin zapisuje i odczytuje EN pola, REST zwraca wariant per locale, error mapping działa (graceful fallback dla nieznanych), PL bez zmian.
</how-to-verify>
<resume-signal>Napisz "approved" lub opisz co nie działa.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Schema `carei_reservation` CPT, meta keys (`_carei_*`), statusy w DB
- Logika JWT auth w `Carei_Softra_API` (cache, retry)
- Phase 13 struktura REST endpointu `/protection-packages` — klucze odpowiedzi (`name`, `description`, `pricePerDay`, `enabled`) pozostają, zmienia się treść per locale
- Phase 15 filtr Softra-insurance (drop) — nietknięty
- Phase 16 textdomain i wp_localize_script — tylko ROZSZERZAMY (dokładamy nowe klucze jeśli potrzeba), nie refaktorujemy
- Frontend JS — minimalne zmiany (ewentualnie `?lang=` parameter), bez refactoru flow
## SCOPE LIMITS
- Nie generujemy tłumaczeń `.po`/`.mo` — to Phase 18
- Nie dodajemy mechanizmu Polylang "String Translation" per-post-meta (nadkomplikacja) — option w DB z polami `_en` wystarczy dla 2 pakietów
- Nie rozszerzamy słownika Softra na >20 pozycji — 1215 pokrywa realne przypadki, resztę mapujemy iteracyjnie gdy się pojawią
- Nie tłumaczymy statusów rezerwacji w DB (`nowe`/`przeczytane`/`zrealizowane`) — tylko UI labels (już zrobione w Phase 16)
- Nie dotykamy Elementora ani treści stron
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] Panel admin pokazuje 4 pola per pakiet (PL + EN name, PL + EN description)
- [ ] Zapisanie EN w panelu → dane utrzymują się po reload
- [ ] REST `/protection-packages` w PL zwraca polskie, w EN zwraca angielskie (lub PL fallback)
- [ ] Odpowiedź REST nie zawiera `name_en`/`description_en` (czyste API)
- [ ] `Carei_Softra_API::map_error_message()` istnieje, słownik ma >= 12 wpisów
- [ ] Exact match + fuzzy prefix match działają
- [ ] Zero regresji w PL (human-verify)
- [ ] AC-1, AC-2, AC-3 przeszły weryfikację
</verification>
<success_criteria>
- Wszystkie 2 auto tasks zakończone
- Checkpoint human-verify zatwierdzony ("approved")
- PL bez regresji
- EN: pakiety pokazują wartości z pól `_en` (lub PL fallback)
- Infrastruktura mapowania błędów Softra gotowa (tłumaczenia pojawią się po Phase 18)
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/17-bilingual-packages-and-softra-errors/17-01-SUMMARY.md`
</output>