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,165 @@
---
phase: 15-remove-softra-insurance
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
autonomous: false
delegation: off
---
<objective>
## Goal
Usunąć z modala rezerwacji wyświetlanie pozycji ubezpieczeniowych pobieranych z Softra API (np. "Zniesienie udziału własnego"). W sekcji "Pakiety ochronne" pozostają wyłącznie kafelki SOFT/PREMIUM zarządzane w panelu WP (Phase 13).
## Purpose
Po wdrożeniu Phase 13 (pakiety WP) w sekcji "Pakiety ochronne" równolegle wyświetlały się dwa źródła: (1) kafelki WP (SOFT/PREMIUM), (2) pozycje z Softra pricelist rozpoznawane heurystyką nazwy ("zniesienie"/"insurance"). Powoduje to dublowanie oferty i konflikt z polityką cenową Carei. Zostawiamy jedno źródło prawdy — panel WP.
## Output
- Dynamiczny kontener `#carei-insurance-container` usunięty z szablonu widgetu
- Logika JS klasyfikująca i renderująca `insuranceItems` usunięta
- Pozycje Softra rozpoznane jako „zniesienie/insurance” są **odfiltrowywane całkowicie** (nie trafiają ani do kafelków, ani do `extraItems`, ani do podsumowania)
- Pakiety WP (SOFT/PREMIUM) działają bez zmian
</objective>
<context>
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/phases/13-protection-packages/13-02-SUMMARY.md
@wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
</context>
<acceptance_criteria>
## AC-1: Brak renderowania pozycji Softra-insurance
```gherkin
Given użytkownik otworzył modal rezerwacji i wybrał pojazd/daty
When załaduje się lista dodatków z Softra pricelist zawierająca pozycję "Zniesienie udziału własnego"
Then pozycja ta NIE pojawia się w sekcji "Pakiety ochronne"
And NIE pojawia się w sekcji "Opcje dodatkowe"
And w DOM nie istnieje element `#carei-insurance-container`
```
## AC-2: Pakiety WP działają bez zmian
```gherkin
Given w panelu WP są zdefiniowane pakiety SOFT i PREMIUM z ceną/dobę
When użytkownik otworzy modal
Then w sekcji "Pakiety ochronne" widoczne są wyłącznie kafelki SOFT i PREMIUM
And zaznaczenie pakietu dodaje wiersz do podsumowania i do `grandTotal` (bez zmian względem Phase 13)
```
## AC-3: Brak wycieku do podsumowania i submit payload
```gherkin
Given użytkownik przechodzi do podsumowania bez zaznaczania żadnego pakietu WP
When pricelist zawierał pozycję Zniesienie udziału"
Then pozycja NIE pojawia się w tabeli podsumowania
And nie jest wysyłana w `priceItems` w `makebooking`
And całkowity koszt nie zawiera składnika ubezpieczenia Softra
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Usunięcie kontenera insurance z szablonu widgetu</name>
<files>wp-content/plugins/carei-reservation/includes/class-elementor-widget.php</files>
<action>
W `class-elementor-widget.php` (ok. linia 158161) usunąć blok:
- `<div class="carei-form__protection-divider" aria-hidden="true"></div>`
- `<div class="carei-form__row" id="carei-insurance-container">...komentarz...</div>`
Zachować strukturę sekcji „Pakiety ochronne" z kontenerem `#carei-protection-packages-container` (kafelki SOFT/PREMIUM).
Unikać: usuwania dividerów sekcji zewnętrznej ani innych pól formularza. Zmieniamy wyłącznie divider wewnętrzny + kontener insurance.
</action>
<verify>
grep -n "carei-insurance-container" wp-content/plugins/carei-reservation/includes/class-elementor-widget.php → brak wyników.
Ręcznie: otwórz modal, sekcja „Pakiety ochronne" pokazuje tylko kafelki WP.
</verify>
<done>AC-1 satysfakcjonowane: kontener nie istnieje w DOM.</done>
</task>
<task type="auto">
<name>Task 2: Usunięcie logiki insurance w carei-reservation.js</name>
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js</files>
<action>
1. Usunąć zmienną `insuranceContainer` z deklaracji (ok. linia 82) oraz jej przypisanie w init (`insuranceContainer = document.getElementById('carei-insurance-container');` ok. linia 107).
2. W funkcji renderującej dodatki (ok. linie 503525):
- Usunąć lokalną zmienną `insuranceItems` i blok `if (insuranceContainer) { ... }`.
- Zastąpić heurystykę „dopasuj do insurance LUB extras" filtrowaniem typu **drop** — pozycje zawierające w nazwie `zniesienie` lub `insurance` (case-insensitive) mają być **pomijane całkowicie** (nie trafiają do `extraItems`).
- Pozostałe pozycje trafiają jak dotychczas do `extraItems` i są renderowane w `extrasContainer`.
3. Sprawdzić, że przy budowaniu payloadu (`priceItems` / `makebooking`) nie ma osobnej ścieżki pobierającej z `insuranceContainer` — jeśli jest, usunąć.
Unikać: zmian w logice Phase 13 (renderProtectionPackages, onProtectionCardClick, getSelectedProtectionPayload) — ten kod zostaje nietknięty. Nie usuwać `extrasContainer`.
</action>
<verify>
grep -nE "insuranceContainer|insuranceItems|carei-insurance-container" wp-content/plugins/carei-reservation/assets/js/carei-reservation.js → brak wyników.
grep -nE "zniesienie|insurance" wp-content/plugins/carei-reservation/assets/js/carei-reservation.js → tylko w filtrze drop.
</verify>
<done>AC-1, AC-3 satysfakcjonowane: pozycje Softra-insurance są dropowane przed renderem i przed budową payloadu.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- Usunięty kontener `#carei-insurance-container` i jego divider w szablonie PHP.
- Usunięta zmienna + render logic `insuranceItems` w JS; pozycje Softra „Zniesienie udziału"/„insurance" są pomijane.
- Pakiety WP (SOFT/PREMIUM) bez zmian.
</what-built>
<how-to-verify>
1. Wypchnij przez SFTP (albo zweryfikuj lokalnie, jeśli dostępny).
2. Otwórz stronę z modalem rezerwacji (carei.pagedev.pl) z DevTools.
3. Uzupełnij krok 1 formularza tak, aby pricelist z Softra został pobrany (wybór dat/oddziału/klasy).
4. Sprawdź sekcję „Pakiety ochronne":
- Widoczne TYLKO kafelki SOFT i PREMIUM z panelu WP.
- Brak pozycji „Zniesienie udziału własnego" ani żadnej innej z Softra.
5. Sprawdź sekcję „Opcje dodatkowe":
- Pozycje Softra są (fotelik, GPS itd.), ale brak pozycji zawierających słowo „zniesienie" lub „insurance".
6. W DevTools → Elements: `document.getElementById('carei-insurance-container')``null`.
7. Przejdź do podsumowania bez zaznaczania pakietu WP:
- W tabeli brak wiersza ubezpieczenia Softra.
- `grandTotal` = suma Softra (bez insurance) (+ pakiet WP jeśli zaznaczony).
8. Zaznacz pakiet SOFT → kafelek zaznacza się, wiersz pakietu pojawia się w podsumowaniu, total rośnie o cena×doba.
</how-to-verify>
<resume-signal>Napisz "approved" aby zamknąć plan, lub opisz problemy do poprawy.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `includes/class-admin-panel.php` — panel WP pakietów (Phase 13 stable)
- `includes/class-rest-proxy.php` endpoint `/protection-packages`
- Logika Phase 13 w JS: `loadProtectionPackages`, `renderProtectionPackages`, `onProtectionCardClick`, `getSelectedProtectionPayload`, sekcja podsumowania z pakietem WP
- Kontener `#carei-protection-packages-container` i jego style
- Inne sekcje formularza (segment, daty, lokalizacja, wyjazd zagraniczny, dane osobowe)
## SCOPE LIMITS
- Nie zmieniamy heurystyki klasyfikacji innych pozycji extras
- Nie dotykamy backend (PHP) poza szablonem widgetu
- Nie ruszamy CSS — osierocone reguły `.carei-form__protection-divider` w CSS mogą zostać (brak użycia == brak efektu)
- Nie zmieniamy wywołań API Softra ani żadnych endpointów
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] `grep -n "carei-insurance-container"` → 0 wyników w `includes/` oraz `assets/js/`
- [ ] `grep -n "insuranceContainer"` w JS → 0 wyników
- [ ] Modal otwiera się, sekcja „Pakiety ochronne" pokazuje tylko SOFT/PREMIUM
- [ ] Opcje dodatkowe: brak pozycji „Zniesienie udziału"
- [ ] Podsumowanie i makebooking payload wolne od Softra-insurance
- [ ] AC-1, AC-2, AC-3 przeszły human-verify
</verification>
<success_criteria>
- Wszystkie tasks zakończone
- Checkpoint human-verify zatwierdzony ("approved")
- Brak regresji w Phase 13 (pakiety WP) i w pobieraniu extras Softra
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/15-remove-softra-insurance/15-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,111 @@
---
phase: 15-remove-softra-insurance
plan: 01
subsystem: ui
tags: [elementor, reservation-modal, pricelist, protection-packages, polylang-ready]
requires:
- phase: 13-protection-packages
provides: WP-managed SOFT/PREMIUM packages + /protection-packages REST endpoint
provides:
- Single source of truth for protection packages (panel WP)
- Drop-filter for Softra-insurance items in pricelist rendering
affects: future-extras-work
tech-stack:
added: []
patterns:
- "Pricelist drop-filter: pozycje zawierające ubezp/ochrony/zniesienie/insurance są pomijane przed renderem"
key-files:
created: []
modified:
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
key-decisions:
- "Drop zamiast kategoryzacji: pozycje Softra-insurance są całkowicie pomijane (nie trafiają do extras ani payloadu)"
patterns-established:
- "Jedno źródło prawdy dla pakietów ochronnych: panel WP (Phase 13)"
duration: ~10min
started: 2026-04-22
completed: 2026-04-22
---
# Phase 15 Plan 01: Remove Softra-insurance z modala — Summary
**W sekcji „Pakiety ochronne" modala rezerwacji pozostają wyłącznie kafelki SOFT/PREMIUM z panelu WP; pozycje ubezpieczeniowe z Softra API są pomijane przed renderem i przed budową payloadu.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~10min |
| Tasks | 2 auto + 1 human-verify completed |
| Files modified | 2 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Brak renderowania pozycji Softra-insurance | Pass | Kontener `#carei-insurance-container` usunięty z DOM; filtr drop w JS |
| AC-2: Pakiety WP działają bez zmian | Pass | Phase 13 logic nietknięta |
| AC-3: Brak wycieku do podsumowania i payloadu | Pass | Pozycje są dropowane przed `extraItems.push` — nie trafiają do summary ani do `priceItems` |
## Accomplishments
- Usunięty kontener `#carei-insurance-container` + wewnętrzny divider w szablonie Elementor widget
- Usunięta zmienna `insuranceContainer` (deklaracja + init) z `carei-reservation.js`
- Heurystyka nazewnicza `ubezp|ochrony|zniesienie|insurance` zmieniona z „kategoryzuj jako insurance" na „drop całkowicie"
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `wp-content/plugins/carei-reservation/includes/class-elementor-widget.php` | Modified | Usunięty blok insurance-container + divider (linie ~158161) |
| `wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` | Modified | Usunięta zmienna + render insuranceContainer; pozycje Softra-insurance dropowane |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Drop zamiast oddzielnej kategorii | Phase 13 już dostarcza pakiety WP — Softra-insurance staje się redundantny i konfliktuje z polityką cenową | Prostsza logika, brak sekcji do utrzymania, brak konfliktu dwóch źródeł |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Plan wykonany dokładnie jak zaplanowano.
### Deferred Items
None.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Modal rezerwacji wyświetla jedno źródło pakietów ochronnych (WP)
- Payload `makebooking` czysty — bez Softra-insurance
**Concerns:**
- Osierocona reguła CSS `.carei-form__protection-divider` pozostała w pliku (brak użycia = brak efektu wizualnego). Do opcjonalnego cleanupu w backlog.
**Blockers:** None.
---
*Phase: 15-remove-softra-insurance, Plan: 01*
*Completed: 2026-04-22*

View File

@@ -0,0 +1,316 @@
---
phase: 16-i18n-plugin-refactor
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/carei-reservation/carei-reservation.php
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
- 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-search-widget.php
- wp-content/plugins/carei-reservation/includes/class-cities-widget.php
- wp-content/plugins/carei-reservation/includes/class-map-widget.php
- wp-content/plugins/carei-reservation/includes/class-branches-widget.php
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
- wp-content/plugins/carei-reservation/languages/carei-reservation.pot (nowy)
autonomous: false
delegation: off
---
<objective>
## Goal
Przygotować plugin `carei-reservation` do dwujęzyczności: wszystkie user-facing stringi w PHP owinięte w `__()`/`esc_html__()`/`esc_attr__()` z textdomain `carei-reservation`; stringi w JS zmigrowane do `wp_localize_script` (obiekt `careiI18n`) tłumaczony po stronie PHP; textdomain ładowany w bootstrapie; wygenerowany plik `.pot` gotowy do tłumaczenia w Phase 18.
## Purpose
Plugin pokrywa ~100% interakcji użytkownika w języku rezerwacji (modal, hero search, admin panel, widgety mapa/miasta/oddziały). Bez i18n żaden zewnętrzny translator (Polylang, Automatic Translate Addon, Loco) nie jest w stanie tłumaczyć tej zawartości — Polylang widzi tylko treść WordPressa/Elementora, a stringi w JS nie istnieją w DOM-ie serwerowo. i18n-refactor jest twardym blokerem dla Phase 17 i 18 milestone'u v0.7.
## Output
- 8 plików PHP z stringami owiniętymi w funkcje i18n
- Bootstrap `carei-reservation.php` ładuje `load_plugin_textdomain` na `plugins_loaded`
- `carei-reservation.js` nie ma hardkodowanych stringów PL — używa obiektu `careiI18n`
- `class-elementor-widget.php` enqueue `wp_localize_script('carei-reservation', 'careiI18n', [...])` z kluczami zmapowanymi 1:1 na użycia w JS
- `languages/carei-reservation.pot` zawiera ~80150 wpisów gotowych do tłumaczenia
- Strona po polsku działa **identycznie jak przed zmianami** (żaden tekst się nie zmienia — tylko jest owinięty)
</objective>
<context>
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/phases/13-protection-packages/13-02-SUMMARY.md
@.paul/phases/15-remove-softra-insurance/15-01-SUMMARY.md
@wp-content/plugins/carei-reservation/carei-reservation.php
@wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
@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-search-widget.php
@wp-content/plugins/carei-reservation/includes/class-cities-widget.php
@wp-content/plugins/carei-reservation/includes/class-map-widget.php
@wp-content/plugins/carei-reservation/includes/class-branches-widget.php
@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
</context>
<acceptance_criteria>
## AC-1: PHP i18n kompletny, textdomain załadowany
```gherkin
Given plugin carei-reservation jest aktywny
When wyszukamy `grep -rn "__\|_e\|esc_html__\|esc_attr__" wp-content/plugins/carei-reservation/`
Then każdy user-facing string w plikach PHP jest owinięty w odpowiednią funkcję z textdomain 'carei-reservation'
And bootstrap `carei-reservation.php` wywołuje `load_plugin_textdomain('carei-reservation', false, dirname(plugin_basename(__FILE__)) . '/languages/')` na haku `plugins_loaded`
And żaden user-facing literał PL nie pozostaje bez opakowania (z wyjątkiem: komentarzy, kluczy meta zaczynających się od `_`, nazw taxonomii jako slugi)
```
## AC-2: JS migrated do careiI18n, brak regresji w PL
```gherkin
Given modal rezerwacji jest otwierany w języku polskim
When użytkownik przechodzi przez pełny flow (wybór dat/oddziału/klasy podsumowanie booking)
Then wszystkie etykiety, komunikaty błędów, placeholdery i nagłówki są identyczne tekstowo jak przed refactorem (AC: parity wizualna 1:1)
And plik `carei-reservation.js` NIE zawiera żadnego literału z polskimi znakami diakrytycznymi (ąćęłńóśźż) ani polskich słów kluczowych
And w DevTools Console: `typeof window.careiI18n === 'object'` zwraca `true` po załadowaniu strony
And obiekt `careiI18n` zawiera klucze odpowiadające wszystkim zmigrowanym stringom
```
## AC-3: .pot wygenerowany, gotowy do tłumaczenia
```gherkin
Given Task 1 i 2 zakończone
When otworzymy `wp-content/plugins/carei-reservation/languages/carei-reservation.pot`
Then plik zawiera nagłówek z metadanymi pluginu (Project-Id-Version, Language: en)
And zawiera wpisy `msgid` dla wszystkich stringów z plików PHP (włącznie z kluczami z careiI18n w wp_localize_script)
And wszystkie `msgstr` są puste (ready for translation)
And ilość wpisów >= 80 (szacunek na podstawie skanu)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: PHP i18n — wrap stringów + textdomain bootstrap</name>
<files>
wp-content/plugins/carei-reservation/carei-reservation.php,
wp-content/plugins/carei-reservation/includes/class-elementor-widget.php,
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-search-widget.php,
wp-content/plugins/carei-reservation/includes/class-cities-widget.php,
wp-content/plugins/carei-reservation/includes/class-map-widget.php,
wp-content/plugins/carei-reservation/includes/class-branches-widget.php
</files>
<action>
1. W `carei-reservation.php` w bootstrap pluginu dodać (na haku `plugins_loaded` lub przy init):
```php
add_action( 'plugins_loaded', function () {
load_plugin_textdomain( 'carei-reservation', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
} );
```
2. Utworzyć katalog `wp-content/plugins/carei-reservation/languages/` (pusty placeholder lub `.gitkeep`).
3. Dla każdego z 8 plików PHP — owinąć wszystkie user-facing stringi:
- Tekst w HTML-u → `esc_html__( 'tekst', 'carei-reservation' )` lub `esc_html_e( 'tekst', 'carei-reservation' )` (wewnątrz `echo`)
- Atrybuty (placeholder, aria-label, title, alt) → `esc_attr__( 'tekst', 'carei-reservation' )`
- Etykiety kontrolek Elementor (`add_control`, `add_responsive_control`) — `'label' => esc_html__( 'tekst', 'carei-reservation' )`
- Labels CPT i meta box (`class-admin-panel.php`) → `__( 'tekst', 'carei-reservation' )` w tablicach labels
- Kolumny admin listy, filtry, akcje masowe → `__()` / `esc_html__()`
- Komunikaty błędów z REST proxy (`class-rest-proxy.php`) w `WP_Error` → `__( 'komunikat', 'carei-reservation' )`
- Tytuły widgetów Elementor i ich opisy (get_title, get_keywords) → `esc_html__()`
4. NIE OWIJAJ:
- Slugów CPT/post_type/taxonomy (`carei_reservation`)
- Kluczy meta (`_carei_protection_package`, `_carei_status`)
- Nazw pól w payloadach do Softra API (klucze JSON)
- Kluczy tablic konfiguracyjnych, nazw hooków, nazw klas/funkcji
- Komentarzy PHP i docblocków
- Logów error_log (to stringi techniczne, nie user-facing)
- Wartości statusów w DB (`nowe`, `przeczytane`, `zrealizowane`) — zostają jako slug; tłumaczymy tylko UI labels (osobny klucz → label mapping)
5. Komunikaty z Softra API (Phase 17 zajmie się mapowaniem — w tym planie NIE ruszamy error message coming FROM Softra). Owijamy tylko nasze własne błędy.
6. Użyj konsystentnie `'carei-reservation'` jako textdomain — bez wyjątków.
Unikaj: globalnego find/replace bez kontekstu — każdy string wymaga decyzji czy jest user-facing czy nie. Nie zmieniaj logiki biznesowej, tylko opakowanie tekstu.
</action>
<verify>
1. `grep -rn "esc_html__\|__(\|esc_attr__" wp-content/plugins/carei-reservation/includes/ | wc -l` → >= 60 wystąpień
2. `grep -rn "load_plugin_textdomain" wp-content/plugins/carei-reservation/` → 1 wystąpienie w bootstrap
3. Ręcznie otwórz stronę z modalem po polsku → każdy tekst identyczny jak przed zmianą (nic się nie przetłumaczyło bo .po jeszcze nie ma)
4. Otwórz wp-admin → Rezerwacje → lista + szczegóły → wszystkie etykiety po PL identyczne
5. `php -l` na każdym z 8 plików → No syntax errors
</verify>
<done>AC-1 satysfakcjonowane.</done>
</task>
<task type="auto">
<name>Task 2: JS i18n — migracja stringów do careiI18n przez wp_localize_script</name>
<files>
wp-content/plugins/carei-reservation/assets/js/carei-reservation.js,
wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
</files>
<action>
1. W `class-elementor-widget.php` — w metodzie enqueue skryptów (tam gdzie `wp_enqueue_script('carei-reservation', ...)` ) dodać **po** enqueue:
```php
wp_localize_script( 'carei-reservation', 'careiI18n', array(
'selectSegment' => esc_html__( 'Wybierz segment', 'carei-reservation' ),
'selectBranch' => esc_html__( 'Wybierz oddział', 'carei-reservation' ),
'selectDates' => esc_html__( 'Wybierz daty', 'carei-reservation' ),
'loading' => esc_html__( 'Ładowanie...', 'carei-reservation' ),
'errorNetwork' => esc_html__( 'Błąd połączenia. Spróbuj ponownie.', 'carei-reservation' ),
'errorRequired' => esc_html__( 'To pole jest wymagane', 'carei-reservation' ),
// ...wszystkie pozostałe stringi użyte w JS
) );
```
**Uwaga:** jeśli widget renderuje się w więcej niż jednym miejscu (search widget też ładuje ten sam JS?), upewnij się że `wp_localize_script` leci tylko raz per żądanie (handle `carei-reservation` jest globalny).
2. W `carei-reservation.js` — odnaleźć WSZYSTKIE polskie stringi literalne i zamienić na odwołania do `careiI18n.kluczX`:
- Polskie znaki diakrytyczne (ąćęłńóśźż) są dobrym pierwotnym filtrem do wyszukania
- Etykiety w `buildExtraCard`, nagłówki sekcji, komunikaty walidacji, błędy API, teksty przycisków, placeholdery setowane z JS, toast/error summary
- Stringi budowane dynamicznie (`'Pakiet ochronny: ' + name + ' — ' + price`) → użyj wrappera z template string: `careiI18n.protectionLine.replace('%name%', name).replace('%price%', price)` i podobnie w PHP `sprintf()` nie jest potrzebny po stronie JS — użyj prostego `.replace()` z placeholderami `%name%`/`%days%`/`%total%`. Alternatywnie: przetłumaczony szablon z `{name}` / `{days}`.
- Lista krajów, dni tygodnia, nazwy miesięcy (jeśli są hardkodowane PL) → do `careiI18n` albo do natywnego `Intl.DateTimeFormat(locale)` tam gdzie to możliwe
- Konsol.log/error → **NIE ruszaj** (to techniczne)
3. Dla stringów z formatowaniem liczbowo-pluralnym (np. "1 doba" / "2 doby" / "5 dób") zdefiniuj w careiI18n 3 warianty (`dayOne`, `dayFew`, `dayMany`) i w JS napisz pomocnika `pluralPl(n, one, few, many)` który używa reguł polskich. Dla EN wystarczy `dayOne` / `dayOther`.
4. Końcowa weryfikacja: `grep -nE "[ąćęłńóśźż]" wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` → zero wyników (poza komentarzami jeśli są).
Unikaj:
- Zmiany logiki biznesowej (walidacja, flow booking, API calls) — tylko tekst
- Tworzenia dziesięciu funkcji pomocniczych — jedna `t(key)` z fallbackiem do `key` wystarczy
- Zmiany `console.*` komunikatów (techniczne, nie user-facing)
- Nie twórz duplikatów stringów — jeśli „Wybierz oddział" występuje w 3 miejscach, ma 1 klucz
</action>
<verify>
1. `grep -cP "[ąćęłńóśźż]" wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` → 0 (lub tylko w komentarzach)
2. Otwórz modal w przeglądarce → `window.careiI18n` w console → obiekt z kluczami
3. Kliknij przez cały flow rezerwacji w języku polskim → tekst identyczny jak przed Task 2
4. DevTools Network → brak 404 dla `carei-reservation.js`
5. `node --check wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` → OK (walidacja składni)
</verify>
<done>AC-2 satysfakcjonowane.</done>
</task>
<task type="auto">
<name>Task 3: Generate .pot file w languages/</name>
<files>wp-content/plugins/carei-reservation/languages/carei-reservation.pot</files>
<action>
1. Opcja A (preferowana jeśli dostępne WP-CLI na serwerze):
```bash
wp i18n make-pot wp-content/plugins/carei-reservation wp-content/plugins/carei-reservation/languages/carei-reservation.pot --domain=carei-reservation
```
2. Opcja B (ręcznie, jeśli brak WP-CLI):
- Skan wszystkich plików PHP (8 sztuk) i wyciągnięcie wszystkich `__()`, `esc_html__()`, `esc_attr__()` z textdomain `carei-reservation`
- Dodatkowo wyciągnięcie kluczy z `careiI18n` w `class-elementor-widget.php` (bo są też owinięte w `esc_html__`)
- Stworzenie pliku `.pot` zgodnie z formatem gettext:
```
# Copyright (C) 2026 Carei
# This file is distributed under the same license as the Carei Reservation plugin.
msgid ""
msgstr ""
"Project-Id-Version: Carei Reservation 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-22\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: manual\n"
"Language-Team: \n"
#: includes/class-elementor-widget.php:XXX
msgid "Złóż zapytanie o rezerwację"
msgstr ""
[...kolejne wpisy...]
```
- Każdy msgid pojawia się raz (deduplikacja), wszystkie msgstr puste
3. Jeśli opcja A zadziała → zweryfikuj liczbę wpisów (`grep -c "^msgid " plik.pot` >= 80)
4. Jeśli używamy B → spróbuj użyć **Loco Translate** (plugin w WP) do skanu i wygenerowania .pot — to najszybsza ścieżka w praktyce (Loco Translate → Plugins → Carei Reservation → Create template). Wtedy Task 3 staje się głównie zadaniem w wp-admin (nie w kodzie), i plik pojawia się w `languages/carei-reservation.pot` generowany przez Loco.
Unikaj: ręcznego edytowania .pot jeśli WP-CLI lub Loco dostępne. Automatyczne narzędzie nie przegapi stringa.
</action>
<verify>
1. Plik `languages/carei-reservation.pot` istnieje
2. `grep -c "^msgid " wp-content/plugins/carei-reservation/languages/carei-reservation.pot` → >= 80
3. Plik kończy się pustą linią i ma poprawny nagłówek (UTF-8)
4. Otwórz w POEdit / text editor → czy widoczne są stringi w formacie `msgid "Wybierz oddział"` z pustym `msgstr ""`
</verify>
<done>AC-3 satysfakcjonowane: .pot gotowy dla Phase 18 (tłumaczenie).</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- 8 plików PHP z hardkodowanymi PL stringami przeniesionymi do `__()`/`esc_html__()`/`esc_attr__()` z textdomain `carei-reservation`
- Bootstrap `carei-reservation.php` ładuje textdomain przez `load_plugin_textdomain` na `plugins_loaded`
- `carei-reservation.js` bez hardkodowanych PL — wszystkie stringi przez `careiI18n` globalny obiekt
- `class-elementor-widget.php` robi `wp_localize_script('carei-reservation', 'careiI18n', [...])` z PHP-owymi tłumaczeniami
- `languages/carei-reservation.pot` zawiera >= 80 wpisów gotowych do tłumaczenia
- **Strona po polsku działa identycznie jak przed Phase 16** — to jest kluczowe: żaden tekst się nie zmienia, zmienia się TYLKO sposób jego dostarczenia do frontendu
</what-built>
<how-to-verify>
1. Wypchnij cały plugin przez SFTP (całe `wp-content/plugins/carei-reservation/`)
2. W wp-admin: Plugins → deaktywuj i aktywuj ponownie „Carei Reservation" (żeby upewnić się że bootstrap nie crashuje)
3. Otwórz stronę z modalem rezerwacji PO POLSKU (Polylang = PL)
4. Pełen flow:
a. Wypełnij krok 1: segment, daty, oddział, klasa
b. Sprawdź sekcję „Pakiety ochronne" (SOFT/PREMIUM) — tekst identyczny
c. Sprawdź sekcję „Opcje dodatkowe"
d. Sekcja „Wyjazd zagraniczny" — wyszukiwarka krajów
e. Krok 2: podsumowanie → złóż rezerwację
f. Success view
5. Sprawdź hero search form (mini formularz w hero) — etykiety, placeholder, CTA
6. Sprawdź widgety: mapa Polski (tooltipy), grid miast, grid oddziałów — wszystkie teksty po PL
7. Wejdź w wp-admin → Rezerwacje:
- Lista z kolumnami, filtrem statusu — etykiety po PL
- Kliknij rezerwację → meta box szczegółów po PL
- Status dropdown (nowe/przeczytane/zrealizowane) po PL
8. Wejdź w wp-admin → Rezerwacje → Pakiety ochronne — formularz edycji SOFT/PREMIUM — etykiety po PL
9. DevTools Console (na stronie frontowej):
- `window.careiI18n` → obiekt z kluczami
- `typeof window.careiI18n.selectBranch` → `'string'` (lub jakikolwiek klucz który był migrowany)
10. Sprawdź plik `wp-content/plugins/carei-reservation/languages/carei-reservation.pot` — otwórz, zobacz stringi
11. (Opcjonalnie) Zainstaluj Loco Translate → Plugins → Carei Reservation → zobacz że plugin jest rozpoznany jako "translatable"
**Kryterium przejścia:** PL działa IDENTYCZNIE jak przed zmianą, zero regresji tekstowych.
</how-to-verify>
<resume-signal>Napisz "approved" aby zamknąć plan, albo opisz które miejsca pokazują nietłumaczone / regresyjne stringi.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Logika biznesowa pluginu: `class-softra-api.php` (API calls, JWT auth, caching) — nie owijamy stringów technicznych ani nie zmieniamy flow
- Slugi CPT (`carei_reservation`), kluczy meta (`_carei_*`), nazw taxonomii, statusów w bazie (`nowe`, `przeczytane`, `zrealizowane`)
- Struktura payloadów wysyłanych do Softra API (klucze JSON, wartości boolean, nazwy pól)
- `class-softra-api.php` — ten plik nie ma user-facing stringów; zostaje nietknięty (mapowanie komunikatów z Softra = Phase 17)
- Hooki, nazwy akcji, nazwy filtrów WordPress
- Logika Phase 13 (pakiety ochronne): UI labels owijamy, ale struktura danych (REST `/protection-packages`, meta `_carei_protection_package`) bez zmian
- Logika Phase 15 (drop Softra-insurance): filtr pozostaje, komunikaty JS też owijamy w careiI18n
## SCOPE LIMITS
- Nie generujemy tłumaczeń EN w tym planie — tylko `.pot`. Phase 18 zajmie się `.po`/`.mo`
- Nie dodajemy dwujęzycznych pól w panelu pakietów ochronnych — Phase 17
- Nie mapujemy komunikatów z Softra API — Phase 17
- Nie tłumaczymy treści dynamicznych z bazy (np. nazwy pakietów w DB zostają po PL na tym etapie — zmieni to Phase 17)
- Nie dotykamy CSS
- Nie dotykamy treści w Elementorze (strony, widgety natywne) — te już są tłumaczone przez Polylang addon
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] `grep -rn "load_plugin_textdomain" wp-content/plugins/carei-reservation/carei-reservation.php` → 1 wynik
- [ ] `grep -rnE "__\(|esc_html__|esc_attr__" wp-content/plugins/carei-reservation/includes/ | wc -l` → >= 60
- [ ] `grep -cP "[ąćęłńóśźż]" wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` → 0 (lub tylko komentarze)
- [ ] `wp-content/plugins/carei-reservation/languages/carei-reservation.pot` istnieje, >= 80 wpisów msgid
- [ ] `php -l` dla wszystkich 8 plików → No syntax errors
- [ ] Strona PL działa bez regresji (checkpoint human-verify)
- [ ] AC-1, AC-2, AC-3 przeszły weryfikację
</verification>
<success_criteria>
- Wszystkie 3 auto tasks zakończone
- Checkpoint human-verify zatwierdzony ("approved")
- Brak regresji w języku polskim
- `.pot` gotowy do Phase 18
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/16-i18n-plugin-refactor/16-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,156 @@
---
phase: 16-i18n-plugin-refactor
plan: 01
subsystem: i18n
tags: [polylang, gettext, pot, wp-localize-script, textdomain, elementor]
requires:
- phase: 13-protection-packages
provides: panel WP pakietów SOFT/PREMIUM
- phase: 15-remove-softra-insurance
provides: czysty pricelist bez Softra-insurance
provides:
- textdomain 'carei-reservation' ładowany na plugins_loaded
- wszystkie user-facing PHP stringi w __()/esc_html__()/esc_attr__()
- helper I18N/t()/tFmt()/pluralPl() w carei-reservation.js
- wp_localize_script('careiI18n', [...]) z 78 kluczami
- languages/carei-reservation.pot (157 msgid)
affects: [17-bilingual-packages, 18-en-translation]
tech-stack:
added: []
patterns:
- "Helper I18N + t(key, fallback) w JS z obiektem window.careiI18n"
- "pluralPl(n, one, few, many) — polskie reguły pluralizacji (1=one, 2-4 bez 12-14=few, reszta=many)"
- "Drop Softra passthrough errors z tłumaczenia — mapowanie zostaje na Phase 17"
key-files:
created:
- wp-content/plugins/carei-reservation/languages/carei-reservation.pot
modified:
- wp-content/plugins/carei-reservation/carei-reservation.php
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
- 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-search-widget.php
- wp-content/plugins/carei-reservation/includes/class-cities-widget.php
- wp-content/plugins/carei-reservation/includes/class-map-widget.php
- wp-content/plugins/carei-reservation/includes/class-branches-widget.php
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
key-decisions:
- "Bootstrap: wp_localize_script dla careiI18n leci w wp_enqueue_scripts (nie w Elementor widget render) — obiekt dostępny na każdej stronie gdzie ładowany jest skrypt"
- "Helper get_status_label() zamiast statycznej tablicy labels w Admin panelu — pozwala tłumaczyć w runtime po załadowaniu textdomain"
- "Softra API passthrough errors nietłumaczone w Phase 16 — Phase 17 zajmie się mapowaniem"
- "Fallback english w t()/tFmt() = oryginalny polski string (żeby bez .po strona dalej działała po polsku)"
patterns-established:
- "Klucze careiI18n: camelCase, grupowane semantycznie (selectX, btnX, labelX, errorX, thX, rejectX, announceX)"
- "Multi-line UI stringi z HTML (np. <strong>%count% %unit%</strong>) przez wp_kses po stronie PHP, renderowane jako innerHTML w JS"
duration: ~45min
started: 2026-04-22
completed: 2026-04-22
---
# Phase 16 Plan 01: i18n refactor pluginu — Summary
**Plugin carei-reservation przygotowany do dwujęzyczności: 8 plików PHP owiniętych w funkcje gettext, JS zmigrowany do obiektu `window.careiI18n` przez `wp_localize_script`, wygenerowany `.pot` z 157 unikalnymi msgid gotowy do tłumaczenia w Phase 18.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~45min |
| Tasks | 3 auto + 1 human-verify completed |
| Files modified | 9 |
| Files created | 1 (.pot) |
| Delegation | 5 agentów równolegle (1 elementor-widget, 1 admin-panel, 1 JS, 1 dla 4 widgetów, 1 .pot) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: PHP i18n kompletny, textdomain załadowany | Pass | `load_plugin_textdomain` na plugins_loaded; ~105 stringów PHP owiniętych |
| AC-2: JS migrated do careiI18n, brak regresji w PL | Pass | `window.careiI18n` = 78 kluczy, grep diakrytyków w JS = 0 poza fallbackami/API payloads; human-verify PL OK |
| AC-3: .pot wygenerowany | Pass | 157 msgid, UTF-8, nagłówek poprawny, msgstr puste |
## Accomplishments
- **8 plików PHP** z user-facing stringami w funkcjach i18n (carei-reservation, elementor-widget, admin-panel, rest-proxy, search/cities/map/branches widgets)
- **`carei-reservation.js`** (1573 linie) — ~70 literałów przez `t()`/`tFmt()` + helper `pluralPl()` dla polskich form
- **`wp_localize_script('careiI18n', [...])`** z 78 kluczami w bootstrapie
- **`.pot`** gotowy do tłumaczenia — 157 unikalnych msgid w `languages/`
- Dekonstrukcja admin-panelu: statyczna tablica labels statusów rozbita na helper `get_status_label()` (wymóg WP — translation w runtime)
- Zgoda RODO z osadzonym `<a>` — owinięcie przez `wp_kses(__(...))`
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `carei-reservation.php` | Modified | load_plugin_textdomain + wp_localize_script('careiI18n', 78 kluczy) |
| `includes/class-elementor-widget.php` | Modified | ~30 stringów (Elementor controls + HTML modal) |
| `includes/class-admin-panel.php` | Modified | ~55 stringów + helper get_status_label() |
| `includes/class-rest-proxy.php` | Modified | 2 stringi (Invalid nonce, Softra not configured) |
| `includes/class-search-widget.php` | Modified | 8 stringów (hero search form) |
| `includes/class-cities-widget.php` | Modified | 1 string (get_title) |
| `includes/class-map-widget.php` | Modified | 3 stringi (Oddział %s, ul. %s, get_title) |
| `includes/class-branches-widget.php` | Modified | 3 stringi |
| `assets/js/carei-reservation.js` | Modified | ~70 literałów → t()/tFmt() + helpery + pluralPl() |
| `languages/carei-reservation.pot` | Created | 157 msgid, UTF-8, ready for Phase 18 |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| `careiI18n` w bootstrapie (nie w Elementor render) | Skrypt jest rejestrowany w bootstrapie → localize leci raz, dostępne globalnie | Upraszcza architekturę, nie ma per-widget duplikacji |
| Fallback w `t('key', 'polski string')` | Bez .mo plik dalej działa po polsku; bez niego crashowałoby `undefined` | Zero-downtime dla PL, bezpieczne wrap bez straty funkcjonalności |
| Softra error messages nietłumaczone | Zewnętrzne API zwraca polskie błędy — mapowanie = Phase 17 | Zostaje jasna granica między „nasze" a „Softra" stringami |
| Statuses slugi niezmienione (`nowe`/`przeczytane`/`zrealizowane`) | Dane w DB, klucze logiki; tylko UI labels idą przez `get_status_label()` | Brak regresji w filtrach i warunkach logicznych |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | Admin panel: statyczna tablica statuses → helper (konieczne dla WP i18n timing) |
| Scope additions | 0 | — |
| Deferred | 1 | Softra API error mapping (świadomie przełożone na Phase 17) |
### Auto-fixed Issues
**1. [Admin panel] Statyczna tablica `$statuses` z labels nie może być tłumaczona przed init**
- Found during: Task 1 (admin-panel refactor)
- Issue: Tablica jako property klasy = translation wywoływane przed załadowaniem textdomain → pusty string
- Fix: Rozbicie na helper `get_status_label($key)` z switchem, wywoływany w runtime po plugins_loaded
- Files: class-admin-panel.php
- Verification: human-verify PL — statusy na liście, w dropdown i meta boxie wyglądają identycznie jak przed
### Deferred Items
- Phase 17: mapowanie ~10-15 typowych komunikatów Softra API na lokalizowane wersje (np. `Brak pojazdów w tym terminie` → reject key + tłumaczenie)
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- `.pot` z 157 msgid — gotowy input dla Phase 18 (tłumaczenie na EN przez GPT-4.1 mini lub Loco Translate)
- Infrastruktura i18n kompletna — Phase 17 i 18 mogą budować na niej bez refactoru
- Polski UI działa bez regresji (confirmed by user: „język polski jest ok.")
**Concerns:**
- `window.careiI18n` rośnie (78 kluczy) — dla performance można rozważyć lazy-load w przyszłości, ale na razie ~3KB JSON to szum
- Niektóre msgid zawierają HTML (`Wybrano: <strong>%count% %unit%</strong>`) — wymagają translatora świadomego tagów; .pot ma to oznaczone wp_kses whitelist
- Softra error messages (Phase 17) — w JS są klucze `rejectCarNotFound` itd. jako fallbacki, ale aktualny kod Phase 16 nie używa jeszcze mapowania → Phase 17 musi dodać warstwę w rest-proxy lub softra-api
**Blockers:** None.
---
*Phase: 16-i18n-plugin-refactor, Plan: 01*
*Completed: 2026-04-22*

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>

View File

@@ -0,0 +1,138 @@
---
phase: 17-bilingual-packages-and-softra-errors
plan: 01
subsystem: i18n
tags: [polylang, softra-errors, rest-api, protection-packages, bilingual]
requires:
- phase: 13-protection-packages
provides: panel WP pakietów SOFT/PREMIUM + REST /protection-packages
- phase: 16-i18n-plugin-refactor
provides: textdomain carei-reservation + __() we wszystkich plikach
provides:
- Pola name_en / description_en w panelu pakietów + zapis/odczyt
- REST /protection-packages zwraca wariant per locale (z fallbackiem PL)
- Carei_Softra_API::extract_softra_message() + map_error_message() ze słownikiem 13 komunikatów
- JS loadProtectionPackages() dodaje ?lang= na podstawie document.documentElement.lang
affects: [18-en-translation]
tech-stack:
added: []
patterns:
- "Locale resolution: ?lang= param → determine_locale() → get_locale() fallback"
- "Softra error mapping: exact match → fuzzy prefix match → graceful passthrough"
- "Bilingual fields pattern: base field + _en variant w tej samej WP option, fallback gdy _en puste"
key-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
key-decisions:
- "Klucze _en nie wyciekają do REST response — frontend dostaje już rozwiązane name/description"
- "map_error_message przez __() zamiast surowych PL→EN — czeka na .mo (Phase 18) bez osobnego słownika angielskiego"
- "document.documentElement.lang zamiast explicit Polylang API — działa cross-plugin (TranslatePress, WPML) jeśli ktoś zmieni w przyszłości"
- "Default name_en w defaults (SOFT Protection / PREMIUM Protection) — admin dostaje sensowną propozycję od razu"
patterns-established:
- "Bilingual option: {base, base_en, base_description, description_en} — jedna opcja w WP DB, fallback gdy puste EN"
- "REST locale resolution z priorytetem ?lang=<pl|en> — explicit > implicit (determine_locale)"
duration: ~25min
started: 2026-04-22
completed: 2026-04-22
---
# Phase 17 Plan 01: Dwujęzyczne pakiety + mapowanie błędów Softra — Summary
**Panel admina pakietów ochronnych zyskał pola name_en/description_en, REST /protection-packages zwraca wariant per locale z fallbackiem, a błędy z Softra API przechodzą przez słownik 13 typowych komunikatów owinięty w __() — infrastruktura EN gotowa dla Phase 18.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25min |
| Tasks | 2 auto + 1 human-verify completed |
| Files modified | 4 |
| Delegation | 0 (inline — precyzyjne zmiany) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Panel pakietów obsługuje pola _en | Pass | 4 pola tekstowe per pakiet, sanitize + zapis w tej samej WP option, fallback gdy EN puste |
| AC-2: REST endpoint zwraca wariant per locale | Pass | ?lang= param + determine_locale fallback; _en klucze nie wyciekają |
| AC-3: Błędy Softra mapowane | Pass | 13 wpisów w słowniku + exact match + fuzzy prefix + graceful passthrough |
## Accomplishments
- **Admin panel:** 4 pola tekstowe per pakiet (PL + EN dla name i description) + placeholder + opis "Puste = fallback do wersji polskiej"
- **`Carei_REST_Proxy::resolve_locale()`** — helper z priorytetem `?lang=pl|en``determine_locale()``get_locale()`
- **`/protection-packages`** zwraca `name`/`description` rozwiązane per locale, `_en` klucze ukryte w payloadzie
- **`Carei_Softra_API::extract_softra_message()`** — parser JSON odpowiedzi Softra (pola `message`/`error`/`details`/`description`)
- **`Carei_Softra_API::map_error_message()`** — 13 typowych komunikatów PL → `__()` z textdomain, exact + fuzzy prefix match
- **Integracja w `request()`** — błędy HTTP 4xx/5xx przechodzą przez extract→map przed `WP_Error`
- **Frontend `loadProtectionPackages()`** dodaje `?lang=` na podstawie `document.documentElement.lang`
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `includes/class-admin-panel.php` | Modified | Pola _en + sanitize/save |
| `includes/class-rest-proxy.php` | Modified | resolve_locale() + per-locale response |
| `includes/class-softra-api.php` | Modified | extract_softra_message() + map_error_message() + integracja w request() |
| `assets/js/carei-reservation.js` | Modified | ?lang= param w loadProtectionPackages() |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| `_en` keys nie w REST response | Czyste API — frontend nie wie o wariantach, dostaje już rozwiązany string | Brak zmian w JS poza lang= param |
| `map_error_message` przez `__()` a nie statyczne EN | Użyj istniejącej infrastruktury textdomain — Phase 18 automatycznie dostarcza tłumaczenia | Jeden słownik tłumaczeń, jedno miejsce aktualizacji |
| `document.documentElement.lang` zamiast Polylang-specific | Niezależne od konkretnego pluginu i18n; Polylang, TranslatePress, WPML wszystkie ustawiają html@lang | Future-proof przy zmianie wtyczki tłumaczeniowej |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 1 | Dodano `extract_softra_message()` — plan zakładał prostsze podejście, ale Softra zwraca różne struktury JSON |
| Deferred | 0 | — |
### Scope additions
**1. [Robustness] Helper `extract_softra_message()`**
- Found during: Task 2b (integracja mapowania w request())
- Issue: Softra API zwraca różne struktury błędu (message w root, zagnieżdżone w error, albo raw string) — prosty `$body['message']` by nie pokrył wszystkich przypadków
- Fix: Dedykowany parser z iteracją przez typowe klucze (`message`, `error`, `details`, `description`) + rekurencja dla stringa JSON
- Files: class-softra-api.php
- Verification: Edge cases sprawdzone w głowie — array passthrough, string JSON, nested, empty fallback
- Impact: +30 linii kodu, ale gwarantuje że mapowanie działa niezależnie od variantu odpowiedzi Softra
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Kompletna infrastruktura dla EN — Phase 18 tylko generuje `.po``.mo` i wrzuca w `languages/`
- Pakiety ochronne (SOFT/PREMIUM) — admin wypełni pola EN, frontend natychmiast pokaże (nie czeka na .mo)
- Błędy Softra — słownik 13 msgid gotowy do tłumaczenia w .po
**Concerns:**
- Jeśli w przyszłości Softra doda nowy komunikat → słownik trzeba ręcznie rozszerzać (mamy fuzzy prefix match, ale nie pokrywa wszystkiego)
- Performance `loadProtectionPackages()` — doszedł `?lang=` param; nie wpływa na caching (WordPress nie cachuje tego endpointu, Phase 13 save też inwaliduje)
- Admin EN pole może zostać puste → fallback do PL jest poprawny, ale trzeba to wyraźnie komunikować w UI (już jest: "Puste = fallback do wersji polskiej")
**Blockers:** None. Phase 18 może ruszyć.
---
*Phase: 17-bilingual-packages-and-softra-errors, Plan: 01*
*Completed: 2026-04-22*

View File

@@ -0,0 +1,283 @@
---
phase: 18-en-translation
plan: 01
type: execute
wave: 1
depends_on: ["16-01", "17-01"]
files_modified:
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po (nowy)
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.po (nowy, symlink/kopia en_US)
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.mo (nowy, binarny)
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.mo (nowy, binarny)
autonomous: false
delegation: off
---
<objective>
## Goal
Dostarczyć kompletne tłumaczenie EN dla pluginu carei-reservation: przetłumaczyć wszystkie 157 msgid z `carei-reservation.pot` na `msgstr` angielskie w pliku `carei-reservation-en_US.po`, skompilować do `.mo`, zduplikować jako `en_GB` (Polylang w WordPress może używać różnych locale EN — pokrywamy oba najczęstsze warianty). Po uploadzie plików UI pluginu na wersji angielskiej przełącza się całkowicie.
## Purpose
Phase 16 dostarczyła infrastrukturę (`__()`, textdomain, `.pot`), Phase 17 dodała bilingual pakiety + mapowanie błędów Softra. **Phase 18 to jedyna faza, która wizualnie przełączy cały plugin na EN.** Do tej pory użytkownik EN widzi oryginalne polskie stringi — brak `.mo` = brak tłumaczeń, `__('Wybierz segment pojazdu', ...)` zwraca polski msgid.
## Output
- `carei-reservation-en_US.po` — 157 msgstr wypełnionych profesjonalnym angielskim tłumaczeniem branży wynajmu samochodów
- `carei-reservation-en_US.mo` — binarny plik zgodny z formatem gettext (magic bytes `0x950412de`, big-endian byte order)
- `carei-reservation-en_GB.po` + `.mo` — identyczna kopia (lub drobne różnice `rental` vs `hire`, `license` vs `licence` — opcjonalne)
- Po załadowaniu przez `load_plugin_textdomain` (już wpięte w Phase 16) — wszystkie `__()`/`esc_html__()` i `wp_localize_script('careiI18n')` zwracają EN w locale `en_*`
</objective>
<context>
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/phases/16-i18n-plugin-refactor/16-01-SUMMARY.md
@.paul/phases/17-bilingual-packages-and-softra-errors/17-01-SUMMARY.md
@wp-content/plugins/carei-reservation/languages/carei-reservation.pot
</context>
<acceptance_criteria>
## AC-1: Plik .po kompletnie przetłumaczony
```gherkin
Given istnieje plik `languages/carei-reservation-en_US.po`
When policzymy niepuste msgstr (`grep -c '^msgstr "[^"]' plik.po`)
Then wszystkie 157 wpisów ma wypełnione msgstr (nie-puste)
And nagłówek zawiera Language: en_US, poprawne Plural-Forms (nplurals=2; plural=(n != 1))
And msgid zawierające placeholdery `%count%`, `%name%`, `%price%`, `%status%` itp. mają je **zachowane w msgstr** (nietknięte te same tokeny)
And msgid zawierające tagi HTML (`<strong>...</strong>`) mają je zachowane
And tłumaczenie zachowuje branżowy ton: `doba` `day`, `oddział` `location`, `pakiet ochronny` `protection package`, `rezerwacja` `reservation`, `zł` `PLN`, itp.
```
## AC-2: Plik .mo poprawny binarnie
```gherkin
Given istnieje plik `languages/carei-reservation-en_US.mo`
When otworzymy go jako binary
Then pierwsze 4 bajty to magic number `0xde120495` (little-endian) LUB `0x950412de` (big-endian)
And zawiera wszystkie tłumaczenia z .po
And daje się poprawnie sparsować przez `gettext` (WordPress i18n czyta go bez błędu)
And jest < 50 KB (typowy rozmiar dla 157 wpisów)
```
## AC-3: EN UI działa po uploadzie
```gherkin
Given administrator ustawił Polylang locale na en_US albo en_GB
When użytkownik otwiera stronę w EN
Then modal rezerwacji ma WSZYSTKIE etykiety po angielsku (segment, dates, location, protection packages, booking summary, success)
And hero search form po angielsku
And widgety (mapa, grid miast, grid oddziałów) po angielsku gdzie są stringi UI
And wp-admin Rezerwacje (dla administratora z EN locale) po angielsku
And błędy z Softra API (dla znanych komunikatów) po angielsku
And pakiety ochronne pokazują wartości z pól `_en` (lub PL fallback gdy puste z Phase 17)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Translate .pot → carei-reservation-en_US.po</name>
<files>wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po</files>
<action>
1. Przeczytaj `languages/carei-reservation.pot` (157 msgid, ~13.7 KB)
2. Dla każdego wpisu msgid wygeneruj angielskie tłumaczenie w msgstr:
- **Ton:** profesjonalny, branża wynajmu samochodów (car rental)
- **Kluczowe terminy:**
- „doba" / „dób" / „doby" → `day` / `days` (bez skomplikowanej pluralizacji — EN ma tylko one/other)
- „oddział" → `location` (nie `branch` — bardziej rental-naturalne)
- „pakiet ochronny" → `protection package`
- „rezerwacja" → `reservation` (nie `booking` — spójne z nazwą pluginu)
- „Złóż zapytanie o rezerwację" → `Request a reservation` (skrócone, CTA-friendly)
- „klient" → `customer`
- „zł" → `PLN`
- „Wyjazd zagraniczny" → `International travel`
- „Zniesienie udziału" → `Deductible waiver` (jeśli gdzieś zostało)
- „najemca" → `renter`
- „segment pojazdu" → `vehicle segment`
- „miejsce odbioru" / „miejsce zwrotu" → `pickup location` / `return location`
- „politykę prywatności" → `privacy policy`
- „Pakiet SOFT" → `SOFT Package` (nazwy własne bez zmian)
- **Placeholdery:** tokeny typu `%name%`, `%count%`, `%price%`, `%status%`, `%days%`, `%total%`, `%unit%`, `%no%`, `%msg%`, `%perDay%`, `%min%`, `%max%`, `%label%`**ZACHOWAJ DOKŁADNIE** jak w oryginale (nie tłumacz, nie zmieniaj kolejności placeholderów jeśli to zmieniałoby znaczenie; ale możesz zmienić kolejność słów wokół nich)
- **HTML tagi:** `<strong>...</strong>` → ZACHOWAJ
- **%s** (sprintf) → ZACHOWAJ
3. Zapisz jako `wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po` z nagłówkiem:
```
# English (US) translation for Carei Reservation
# This file is distributed under the same license as the Carei Reservation plugin.
msgid ""
msgstr ""
"Project-Id-Version: Carei Reservation 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-22 12:00+0000\n"
"PO-Revision-Date: 2026-04-22 12:00+0000\n"
"Last-Translator: Carei\n"
"Language-Team: English\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Domain: carei-reservation\n"
```
4. Zachowaj komentarze `#:` z referencjami plik:linia (dla kontekstu tłumacza przy przyszłych zmianach)
5. Sanity check per wpis:
- msgid niepuste → msgstr niepuste
- `%TOKEN%` w msgid → `%TOKEN%` w msgstr (ten sam token)
- `<strong>` w msgid → `<strong>` w msgstr (dokładnie ten sam tag)
Unikaj:
- Dosłownego tłumaczenia słowo-w-słowo (kluczowe są frazy rental, nie słownikowe mapowania)
- Zachowywania polskich diakrytyków w EN (ąćęłńóśźż nie powinny być w msgstr)
- Tłumaczenia nazw własnych (`SOFT`, `PREMIUM`, `Carei`, `Softra`)
- Zmiany placeholderów (`%count%` → `%iloscDni%` = katastrofa w runtime)
</action>
<verify>
1. Plik `carei-reservation-en_US.po` istnieje
2. `grep -c '^msgid "[^"]' plik.po` ≈ 158 (157 + header)
3. `grep -c '^msgstr "[^"]' plik.po` ≈ 158 (wszystkie wypełnione)
4. `grep -cP "[ąćęłńóśźż]" plik.po` → tylko w msgid (oryginalne PL), zero w msgstr
5. Spot-check 10 wpisów: ton angielski branżowy, placeholdery zachowane, HTML zachowany
</verify>
<done>AC-1 satysfakcjonowane.</done>
</task>
<task type="auto">
<name>Task 2: Kompilacja .po → .mo (+ kopia en_GB)</name>
<files>
wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.mo,
wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.po,
wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.mo
</files>
<action>
**Problem:** `msgfmt` (standardowy kompilator gettext) nie jest dostępny w systemie developera. Python `msgfmt.py` również nie dostępny.
**Rozwiązanie:** Wygeneruj plik `.mo` binarnie programistycznie. Możliwe ścieżki (wybierz dostępną):
1. **Preferowana:** Napisz tymczasowy skrypt Node.js w sandbox używający pakietu `gettext-parser` (dostępny przez npm):
```js
// Wymaga: npm i gettext-parser
const fs = require('fs');
const gp = require('gettext-parser');
const po = fs.readFileSync('carei-reservation-en_US.po');
const parsed = gp.po.parse(po);
const mo = gp.mo.compile(parsed);
fs.writeFileSync('carei-reservation-en_US.mo', mo);
```
2. **Alternatywa PHP:** Napisz własny skrypt PHP implementujący format `.mo` (magic 0x950412de, header 7×uint32, offset tables, string data). Format jest w dokumentacji gettext: https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html
- Parsuj `.po` ręcznie (rozpoznaj msgid/msgstr, unescape)
- Zbuduj binarny layout: header → offset/length tables for originals → offset/length tables for translations → string blobs (null-terminated)
- Zapisz jako binary file
3. **Loco Translate (wp-admin):** gdyby powyższe zawiodły — upload `.po` do wp-content/languages/plugins/ i niech Loco Translate w `wp-admin → Loco Translate → Plugins → Carei Reservation → en_US → Sync → Save` wygeneruje `.mo` w server-side.
**Preferuj opcję 1** (Node.js + gettext-parser) — działa deterministycznie w dev environment, bez potrzeby wp-admin.
**Po skompilowaniu en_US:**
- Skopiuj `.po` jako `carei-reservation-en_GB.po` (zmień `Language: en_US\n` → `Language: en_GB\n` w nagłówku)
- Opcjonalnie: drobne poprawki UK-english (license→licence, color→colour, itp.) — na tym etapie zostaw identyczne
- Skompiluj `.po` → `.mo` tym samym skryptem
**Weryfikacja binarnej poprawności `.mo`:**
```bash
# Pierwsze 4 bajty = magic number (little-endian 0x950412de)
xxd -l 4 carei-reservation-en_US.mo
# Powinno pokazać: 0000000: de12 0495 (le) lub 9504 12de (be)
```
</action>
<verify>
1. Pliki istnieją: `en_US.po`, `en_US.mo`, `en_GB.po`, `en_GB.mo` w `languages/`
2. Rozmiar `.mo` między 550 KB
3. Magic number poprawny (xxd/hexdump pierwsze 4 bajty)
4. PHP sanity check (opcjonalny): `php -r "$mo=file_get_contents('path.mo'); echo bin2hex(substr($mo,0,4));"` → `de120495` lub `950412de`
</verify>
<done>AC-2 satysfakcjonowane.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- `carei-reservation-en_US.po` + `.mo` z 157 przetłumaczonymi wpisami
- `carei-reservation-en_GB.po` + `.mo` jako kopia en_US
- Po uploadzie wszystkie `__()`/`esc_html__()` + `careiI18n` w pluginie zwracają EN przy locale en_*
</what-built>
<how-to-verify>
1. **Deploy:** wypchnij 4 nowe pliki (`carei-reservation-en_US.po`, `.mo`, `en_GB.po`, `.mo`) do `wp-content/plugins/carei-reservation/languages/` na serwer
2. **Cache:** wyczyść cache pluginu (jeśli masz WP Rocket / Autoptimize) + opcache PHP (zwykle wystarczy reboot fpm, albo `wp cache flush`)
3. **Admin panel pakietów:**
- Zaloguj się do wp-admin (jeśli twoja konto admina jest PL → zostawcie PL dla admina, albo zmień WP user locale na EN w Users → Profile → Language: English (United States))
- `Rezerwacje → Pakiety ochronne` — labele powinny być po EN (jeśli admin locale = EN)
- Uzupełnij pola EN dla SOFT i PREMIUM (np. `SOFT Protection` + `Basic damage coverage with 2000 PLN deductible`; `PREMIUM Protection` + `Full damage waiver, zero deductible`)
- Zapisz
4. **Frontend EN — modal rezerwacji:**
- Przełącz Polylang switcher na EN
- Otwórz stronę z przyciskiem → przycisk pokazuje `Request a reservation` (lub podobne)
- Modal otwiera się, WSZYSTKIE labele po angielsku: `Vehicle segment`, `From`, `To`, `Pickup location`, `Return location`, `Protection packages`, `Additional options`, `International travel`, `First name`, `Last name`, `Email`, `Phone`, `I agree to the privacy policy`, `Send request`
- Pakiety ochronne: `SOFT Protection` / `PREMIUM Protection` z angielskim opisem (z Phase 17)
- Komunikaty walidacji (spróbuj wysłać pusty form) po EN: `Enter first name`, `Enter a valid email`, itp.
- Podsumowanie → `Reservation summary`, `Subtotal`, `VAT`, `Total`, `Confirm reservation`
- Success: `Reservation confirmed`, `Order number: X`
5. **Hero search form:** `Vehicle segment`, `From`, `To`, `Pickup location`, `Check availability`
6. **Widgety:**
- Mapa Polski: tooltipy `Location: {city}`, `ul. {street}` → po EN
- Grid miast, grid oddziałów → po EN
7. **Błędy Softra:**
- Spróbuj zarezerwować niedostępny pojazd / nieprawidłową datę — komunikat po EN (z słownika Phase 17 via `__()`)
8. **DevTools sanity:**
- `window.careiI18n.selectSegment` → `"Select vehicle segment"` (lub podobne)
- `window.careiI18n.dayOne` → `"day"`, `dayOther` → `"days"` (jeśli dodałeś `dayOther` jako en-plural)
- Network: `/wp-json/carei/v1/protection-packages?lang=en` → response z EN treścią
9. **Powrót do PL:**
- Przełącz Polylang na PL
- Wszystko wraca do polskiego bez regresji (oryginał zachowany)
**Kryterium przejścia:** cała UI pluginu w EN (modal + hero + widgety + admin + error messages). Zero polskich literałów przy locale EN (poza treściami z DB które admin zostawił puste → fallback PL z Phase 17).
**Znane limitacje (NIE blokują approve):**
- Treści stron Elementora (nagłówki hero, sekcje marketingowe) — tłumaczone osobno przez Polylang addon (poza scope tej fazy)
- Menu, footer, inne treści WP — Polylang / Polylang Strings Translation (poza scope)
</how-to-verify>
<resume-signal>Napisz "approved" aby zamknąć Milestone v0.7, albo wskaż stringi które pozostały po polsku pomimo EN locale.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Żaden plik PHP/JS pluginu (Phase 1617 już to załatwiły)
- Plik `.pot` (baseline — nie nadpisujemy bazowego template)
- Textdomain, mechanizm `load_plugin_textdomain` (Phase 16)
- Panel admina pakietów — tylko DANE w bazie mogą być wypełnione przez admina (to nie zmiana kodu)
- `carei-reservation.css` — styling niezależny od języka
- `mu-plugins/fix-sprintf-global.php` — dalej potrzebny dla Polylang addon
## SCOPE LIMITS
- Nie tłumaczymy innych pluginów (tylko carei-reservation)
- Nie tłumaczymy Elementora ani treści stron (Polylang addon)
- Nie tłumaczymy nazw krajów w sekcji wyjazdu zagranicznego (dane biznesowe COUNTRY_FLAGS — pozostają po polsku bo backend tak zwraca)
- Nie tłumaczymy slugów URL, permalink structures
- Nie generujemy dodatkowych locale (fr, de, itp.) — tylko en_US + en_GB
- Nie zmieniamy generatora .pot ani pipeline-u i18n
</boundaries>
<verification>
Przed zamknięciem planu:
- [ ] `carei-reservation-en_US.po` istnieje, 157 wpisów, msgstr wszystkie wypełnione
- [ ] `carei-reservation-en_US.mo` istnieje, magic number poprawny
- [ ] `carei-reservation-en_GB.po` + `.mo` — kopia en_US
- [ ] Brak polskich diakrytyków w msgstr
- [ ] Placeholdery `%TOKEN%` zachowane 1:1 między msgid i msgstr
- [ ] HTML tagi zachowane w msgstr
- [ ] Human-verify — pełen flow EN bez regresji PL
- [ ] AC-1, AC-2, AC-3 pass
</verification>
<success_criteria>
- Task 12 ukończone
- Checkpoint zatwierdzony
- Milestone v0.7 — 100% complete
- Plugin carei-reservation pełnoprawnie dwujęzyczny (PL + EN)
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/18-en-translation/18-01-SUMMARY.md`
Następnie: `/paul:complete-milestone v0.7`
</output>

View File

@@ -0,0 +1,168 @@
---
phase: 18-en-translation
plan: 01
subsystem: i18n
tags: [gettext, po, mo, translation, flatpickr, bilingual]
requires:
- phase: 16-i18n-plugin-refactor
provides: .pot (157 msgid) + textdomain carei-reservation
- phase: 17-bilingual-packages-and-softra-errors
provides: bilingual pakiety + mapowanie Softra przez __()
provides:
- carei-reservation-en_US.po + .mo (158 wpisów przetłumaczonych)
- carei-reservation-en_GB.po + .mo (kopia en_US)
- PHP mini-kompilator po2mo.php (bez potrzeby msgfmt)
- Flatpickr jako cross-browser date picker z PL/EN locale (scope addition)
- Kompaktowy CSS theme dla flatpickr w kolorach Carei
affects: []
tech-stack:
added:
- Flatpickr 4.6.13 (CDN jsdelivr, enqueued z dependencies)
patterns:
- "MO compilation: własny parser+packer PHP bez msgfmt/Python"
- "Flatpickr static:true dla modala (popup w container) + default dla hero"
- "disableMobile:true dla spójności UX PL/EN na wszystkich urządzeniach"
key-files:
created:
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.mo
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.po
- wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.mo
modified:
- wp-content/plugins/carei-reservation/carei-reservation.php
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
- wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
key-decisions:
- "Tłumaczenie przez agenta z pre-zdefiniowanym słownikiem terminów branżowych (~50 mapowań PL→EN car rental)"
- "Własny kompilator PHP zamiast msgfmt/Loco — deterministyczny, niezależny od środowiska"
- "Flatpickr z CDN jsdelivr — brak lokalnych plików, szybki deploy; fallback do native picker jeśli CDN padnie"
- "static:true dla modala — rozwiązuje konflikt focus-trap + z-index"
- "disableMobile:true — jednolity UX zamiast native iOS spinner / Android Material"
- "en_GB = kopia en_US — brak realnej potrzeby różnicowania UK/US na tym etapie"
patterns-established:
- "Własny po2mo compiler w PHP (stored w ~/temp) — reusable dla przyszłych tłumaczeń"
- "CSS override per-klasa flatpickr (compact height/width/spacing) w kolorach Carei #2F2482"
duration: ~40min
started: 2026-04-22
completed: 2026-04-22
---
# Phase 18 Plan 01: EN translation (.po/.mo) + QA — Summary
**Plugin carei-reservation dostarczony w wersji dwujęzycznej: 158 wpisów przetłumaczonych na EN, skompilowanych do .mo (en_US + en_GB), plus cross-browser Flatpickr jako date picker z locale PL/EN. Milestone v0.7 — 100% complete.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~40min |
| Tasks | 2 auto + 1 human-verify + 1 scope addition (Flatpickr) |
| Files created | 4 (.po + .mo × 2 locale) |
| Files modified | 3 (bootstrap PHP, JS, CSS) |
| Delegation | 1 agent (tłumaczenie .pot → .po) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Plik .po kompletnie przetłumaczony | Pass | 158 msgstr, placeholdery zachowane, HTML OK, zero PL diakrytyków |
| AC-2: Plik .mo poprawny binarnie | Pass | Magic 0x950412de, version 0, N=158, 9455 bytes |
| AC-3: EN UI działa po uploadzie | Pass | User confirmed "approved" po pełnym teście |
## Accomplishments
- **158 wpisów PL→EN** przez agenta z uzgodnionym słownikiem terminów rental (`doba→day`, `oddział→location`, `zł→PLN`, `pakiet ochronny→protection package`, etc.)
- **Własny PHP `.mo` compiler** (po2mo.php, ~150 linii) — parsuje .po, sortuje, pakuje binarnie wg gettext spec
- **en_US + en_GB** — dwa locale EN pokryte (Polylang może używać dowolnego)
- **Flatpickr scope addition:** CDN enqueue + JS init + kompaktowy CSS w kolorach Carei. Modal z `static:true` (popup w container, bypass focus-trap). Hero z default renderowaniem. Jednolity UX desktop + mobile (`disableMobile:true`).
- **Weryfikacja binarna `.mo`** przez PHP: `Magic: 0x950412de, Version: 0, N: 158`
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `languages/carei-reservation-en_US.po` | Created | 158 wpisów PL→EN |
| `languages/carei-reservation-en_US.mo` | Created | Binarka gettext, 9455 bytes |
| `languages/carei-reservation-en_GB.po` | Created | Kopia en_US z `Language: en_GB\n` |
| `languages/carei-reservation-en_GB.mo` | Created | Skompilowana en_GB |
| `carei-reservation.php` | Modified | Flatpickr enqueue (CDN + pl locale) + deps |
| `assets/js/carei-reservation.js` | Modified | `initDatePickers()` + static:true dla modal + graceful fallback |
| `assets/css/carei-reservation.css` | Modified | Kompaktowy flatpickr theme w kolorach Carei |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Własny po2mo.php zamiast msgfmt | msgfmt/Python msgfmt niedostępne w dev env; Loco Translate wymaga wp-admin flow | Deterministic compilation, reusable dla przyszłych locale (fr, de) |
| Flatpickr scope addition | User zgłosił potrzebę tłumaczenia kalendarza natywnego (browser używa OS locale) — flatpickr jedyne sensowne rozwiązanie | +40KB JS (CDN), ale spójne UX + locale-aware |
| CDN jsdelivr | Szybki deploy, brak zarządzania plikami lokalnymi | Dependency na CDN; graceful fallback do native picker |
| `static:true` dla modal | Default popup w body → konflikt z focus-trap + z-index modala | Picker w containerze inputa — kompatybilne z modalem |
| `disableMobile:true` | Native mobile (iOS spinner, Android Material) ignoruje strony locale | Jednolity UX PL/EN niezależnie od OS użytkownika |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 2 | Flatpickr popup ukryty w modalu (static:true), native mobile picker (disableMobile:true) |
| Scope additions | 1 | Flatpickr integracja (cross-browser date picker z i18n) — user request podczas apply |
| Deferred | 0 | — |
### Scope additions
**1. [UX] Flatpickr date picker z i18n**
- Found during: Task 3 (human-verify) — user zapytał o tłumaczenie kalendarza
- Problem: Natywny `<input type="datetime-local">` używa locale OS przeglądarki, ignoruje WP locale
- Fix: Integracja Flatpickr 4.6.13 z CDN — enqueue + init per input + kompaktowy CSS theme
- Files: carei-reservation.php, carei-reservation.js, carei-reservation.css
- Verification: User "jest ok" po kompaktowym themie; PL kalendarz, EN kalendarz, oba na desktop + mobile
### Auto-fixed Issues
**1. [Modal] Flatpickr popup nie otwierał się w modalu**
- Issue: Default append do body + focus-trap z Phase 4 → picker focus tracony przed interakcją
- Fix: `static: true` w opts — popup renderowany wewnątrz `.carei-form__date-wrap`
- Verification: User confirmed kalendarz otwiera się w modalu po zmianie
**2. [Mobile] Inny kalendarz na mobile vs desktop**
- Issue: Flatpickr default `disableMobile: false` → na mobile fallback do native OS picker (iOS spinner / Android Material)
- Fix: `disableMobile: true` — flatpickr wszędzie, jednolity wygląd + locale
- Verification: User confirmed "na obu jest flatpickr"
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| msgfmt niedostępny lokalnie | Własny PHP kompilator (po2mo.php) — reusable |
| Modal calendar nie otwiera | static:true flatpickr option |
| Mobile inny picker | disableMobile:true |
| Hero kalendarz po polsku mimo EN | Dodanie hero inputs do initDatePickers() (wcześniej tylko modal) |
## Next Phase Readiness
**Milestone v0.7 COMPLETE** — plugin carei-reservation pełnoprawnie dwujęzyczny (PL + EN):
- Infrastruktura: textdomain + __() + careiI18n (Phase 16)
- Bilingual dane: pakiety pól _en + mapowanie Softra errors (Phase 17)
- Tłumaczenia: .po/.mo dla en_US + en_GB (Phase 18)
- UX: Flatpickr cross-browser + locale-aware (Phase 18 scope addition)
**Out of scope dla kolejnych milestones:**
- Tłumaczenie treści stron Elementora (Polylang Automatic Translate Addon)
- Menu, footer, theme stringi (Polylang String Translation)
- Nazwy miast/krajów (dane biznesowe z API Softra)
- Inne locale (fr, de) — dodaje się przez sam `.po/.mo` bez zmian w kodzie
**Blockers:** None.
---
*Phase: 18-en-translation, Plan: 01*
*Completed: 2026-04-22*

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*