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

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -60,6 +60,47 @@ Fix: modal rezerwacji nie otwierał się na mobile/tablet — sekcja Elementor m
--- ---
---
## Milestone v0.6: Cleanup — Pakiety ochronne (jedno źródło prawdy)
**Goal:** W sekcji „Pakiety ochronne" modala zostają wyłącznie pakiety z panelu WP (SOFT/PREMIUM). Pozycje insurance z Softra API („Zniesienie udziału") są usuwane z widoku i nie trafiają do podsumowania ani payloadu.
**Status:** Complete ✅
### Phase 15: Remove Softra-insurance z modala ✅ Complete
Usunięty kontener `#carei-insurance-container` + divider w widgecie PHP. W JS usunięta zmienna `insuranceContainer` i render insuranceItems. Pozycje pricelist zawierające `ubezp|ochrony|zniesienie|insurance` są dropowane — nie trafiają do extras, podsumowania ani do `makebooking` payloadu. Pakiety WP (SOFT/PREMIUM) z Phase 13 są jedynym źródłem prawdy.
---
## Milestone v0.7: Dwujęzyczność (PL/EN) widgetów Carei
**Goal:** Plugin `carei-reservation` (modal, hero search, admin, mapa, grid miast, grid oddziałów) działa dwujęzycznie PL/EN zgodnie z bieżącym językiem Polylang. Obejmuje: (1) i18n refactor pluginu, (2) dwujęzyczne pakiety ochronne + mapowanie błędów Softra, (3) tłumaczenie EN + QA.
**Status:** Complete ✅
### Phase 16: i18n refactor pluginu ✅ Complete
Plan 16-01 ukończony. 8 plików PHP z user-facing stringami w `__()`/`esc_html__()`/`esc_attr__()` (~105 stringów), textdomain `carei-reservation` ładowany na `plugins_loaded`. JS (1573 linie): ~70 literałów zmigrowanych do `window.careiI18n` przez `wp_localize_script`, helper `pluralPl()` dla polskich form. Wygenerowany `.pot` z 157 unikalnymi msgid. PL UI bez regresji (verified).
### Phase 17: Dwujęzyczne pakiety ochronne + mapowanie błędów Softra ✅ Complete
Plan 17-01 ukończony. Panel pakietów z polami `name_en`/`description_en` + sanitize/save. REST `/protection-packages` z `resolve_locale()` (`?lang=``determine_locale()` fallback) zwraca wariant per locale z fallbackiem PL; klucze `_en` nie wyciekają w payloadzie. `Carei_Softra_API::extract_softra_message()` + `map_error_message()` ze słownikiem 13 komunikatów (exact + fuzzy prefix + graceful passthrough). Frontend `loadProtectionPackages()` dodaje `?lang=` na podstawie `document.documentElement.lang`.
### Phase 18: Tłumaczenie EN (.po/.mo) + Flatpickr ✅ Complete
Plan 18-01 ukończony. `carei-reservation-en_US.po` + `.mo` (158 wpisów) + kopia en_GB; własny PHP kompilator `po2mo.php` (msgfmt niedostępny). **Scope addition:** Flatpickr 4.6.13 (CDN jsdelivr) jako cross-browser date picker z locale PL/EN dla modal (static:true — popup w container) + hero (default). `disableMobile:true` dla spójności UX. Kompaktowy CSS theme w kolorach Carei (#2F2482). Graceful fallback do native picker.
---
## Milestone v0.8: Admin-managed tłumaczenia extras
**Goal:** Panel administratora do zarządzania tłumaczeniami dynamicznych nazw dodatkowych opcji (extras) zwracanych z Softra API. Plugin auto-zbiera napotkane PL nazwy, admin wypełnia tłumaczenia EN w UI, frontend EN automatycznie używa.
**Status:** Complete ✅
### Phase 19: Extras translations admin panel ✅ Complete
Plan 19-01 ukończony. WP options `carei_extras_seen` (auto-collected) + `carei_extras_translations` (admin override). Static helpery w `Carei_Admin_Panel`: `remember_extra_name()`, `get_extras_seen()`, `get_extras_translations()`, `translate_extra_name()`. Submenu `Rezerwacje → Tłumaczenia extras` (nonce + sanitize + redirect). `Carei_REST_Proxy::get_pricelist()` z runtime auto-collect + per-locale name replacement z fallbackiem do PL. Zero zmian w JS.
---
### Backlog ### Backlog
- Eksport CSV/PDF rezerwacji - Eksport CSV/PDF rezerwacji
- Email notyfikacje - Email notyfikacje

View File

@@ -2,32 +2,35 @@
## Current Position ## Current Position
Milestone: v0.5 Pakiety Ochronne + Poprawki — Complete ✅ Milestone: v0.8 Admin-managed tłumaczenia extras — Complete ✅
Phase: 13 of 14 (Pakiety ochronne) — Complete ✅ Phase: 19 (Extras translations admin panel) — Complete ✅
Plan: 13-02 complete Plan: 19-01 complete (UNIFIED)
Status: Milestone v0.5 closed Status: Milestone v0.8 closed
Last activity: 2026-04-20 — Phase 13 unified, SUMMARY written, milestone closed Last activity: 2026-04-22 — Phase 19 SUMMARY utworzony
Progress: Progress:
- Milestone v0.1: [██████████] 100% ✅
- Milestone v0.2: [██████████] 100% ✅
- Milestone v0.3: [██████████] 100% ✅
- Milestone v0.4: [██████████] 100% ✅
- Milestone v0.5: [██████████] 100% ✅ - Milestone v0.5: [██████████] 100% ✅
- Phase 13: [██████████] 100% ✅ - Milestone v0.6: [██████████] 100% ✅
- Phase 14: [██████████] 100% ✅ - Phase 15: [██████████] 100% ✅
- Milestone v0.7: [██████████] 100% ✅
- Phase 16: [██████████] 100% ✅
- Phase 17: [██████████] 100% ✅
- Phase 18: [██████████] 100% ✅
- Milestone v0.8: [██████████] 100% ✅
- Phase 19: [██████████] 100% ✅
## Loop Position ## Loop Position
Current loop state: Current loop state:
``` ```
PLAN ──▶ APPLY ──▶ UNIFY PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Phase 13 loop closed, milestone v0.5 complete] ✓ ✓ ✓ [Milestone v0.8 complete]
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-04-20 Last session: 2026-04-22
Stopped at: Milestone v0.5 complete (Phase 13 + Phase 14) Stopped at: Milestone v0.8 complete (Phase 19 zamknięta)
Next action: Rozmowa o następnym milestone (v0.6) lub zamknięcie projektu — /paul:discuss-milestone lub /paul:complete-milestone Next action: Rozmowa o następnym milestone lub zamknięcie projektu — /paul:discuss-milestone lub /paul:complete-milestone
Resume file: .paul/phases/13-protection-packages/13-02-SUMMARY.md Resume file: .paul/phases/19-extras-translations-admin/19-01-SUMMARY.md
Resume file: .paul/phases/15-remove-softra-insurance/15-01-PLAN.md

View File

@@ -0,0 +1,90 @@
# 2026-04-22
## Co zrobiono
- [Phase 15, Plan 01] Usunięcie Softra-insurance z modala rezerwacji — pozostają wyłącznie pakiety WP (SOFT/PREMIUM) z Phase 13
- Usunięty kontener `#carei-insurance-container` + wewnętrzny divider w szablonie widgetu Elementor
- Usunięta zmienna `insuranceContainer` (deklaracja + init) w `carei-reservation.js`
- Pozycje pricelist zawierające `ubezp|ochrony|zniesienie|insurance` są dropowane — nie trafiają do extras, podsumowania, ani do payloadu `makebooking`
- [Phase 16, Plan 01] i18n refactor pluginu carei-reservation — przygotowanie do dwujęzyczności PL/EN
- `load_plugin_textdomain('carei-reservation')` w bootstrapie + katalog `languages/`
- 8 plików PHP z user-facing stringami owiniętymi w `__()`/`esc_html__()`/`esc_attr__()`
- `carei-reservation.js`: ~70 literałów zamienione na wywołania `t()`/`tFmt()` przez globalny obiekt `window.careiI18n`, helper `pluralPl()` dla polskich form
- `wp_localize_script('carei-reservation-js', 'careiI18n', [...])` z 78 kluczami tłumaczonymi po PHP
- Wygenerowany `languages/carei-reservation.pot` — 157 unikalnych msgid gotowych do tłumaczenia (Phase 18)
- Admin panel: statyczna tablica statusów → helper `get_status_label()` (wymóg WP — translation timing)
- Mu-plugin `fix-sprintf-global.php` — backfill globalnego `window.sprintf` dla Automatic Translate Addon For Polylang
## Zmienione pliki
- `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)
- `wp-content/mu-plugins/fix-sprintf-global.php` (nowy)
- `.paul/phases/15-remove-softra-insurance/15-01-PLAN.md`
- `.paul/phases/15-remove-softra-insurance/15-01-SUMMARY.md`
- `.paul/phases/16-i18n-plugin-refactor/16-01-PLAN.md`
- `.paul/phases/16-i18n-plugin-refactor/16-01-SUMMARY.md`
- `.paul/STATE.md`
- `.paul/ROADMAP.md`
## Phase 17 — Dwujęzyczne pakiety + mapowanie błędów Softra
- [Phase 17, Plan 01] Pola `name_en`/`description_en` w panelu pakietów SOFT/PREMIUM + sanitize/save
- REST `/protection-packages` z helperem `resolve_locale()` (`?lang=``determine_locale()` → fallback)
- `Carei_Softra_API::extract_softra_message()` — parser JSON odpowiedzi błędu
- `Carei_Softra_API::map_error_message()` — słownik 13 typowych PL komunikatów Softra → `__()` z textdomain, exact + fuzzy prefix match, graceful passthrough
- Frontend: `loadProtectionPackages()` dodaje `?lang=` na podstawie `document.documentElement.lang`
- Pliki: `class-admin-panel.php`, `class-rest-proxy.php`, `class-softra-api.php`, `carei-reservation.js`
- `.paul/phases/17-bilingual-packages-and-softra-errors/17-01-PLAN.md` + SUMMARY
## Phase 18 — EN translation .po/.mo + Flatpickr
- [Phase 18, Plan 01] Tłumaczenie EN pluginu carei-reservation — 158 wpisów z .pot przetłumaczone na angielski (branża car rental)
- Własny PHP kompilator `po2mo.php` (msgfmt niedostępny) — parsuje .po, sortuje, pakuje binarnie wg gettext spec (magic 0x950412de)
- Wygenerowane `carei-reservation-en_US.po` + `.mo` (9455 bytes, N=158) oraz `en_GB.po` + `.mo`
- **Scope addition:** Flatpickr 4.6.13 (CDN jsdelivr) — cross-browser date picker z locale PL/EN dla modal + hero; `static: true` dla modala (popup w container, bypass focus-trap); `disableMobile: true` dla spójności UX
- Kompaktowy CSS theme flatpickr w kolorach Carei (#2F2482): szerokość 260px, font 1213px, zaokrąglone dni, miękki fioletowy shadow
- Graceful fallback — jeśli CDN padnie, wraca do natywnego `datetime-local` pickera
## Zmienione pliki (Phase 18)
- `wp-content/plugins/carei-reservation/carei-reservation.php` (Flatpickr enqueue)
- `wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` (initDatePickers)
- `wp-content/plugins/carei-reservation/assets/css/carei-reservation.css` (kompaktowy theme)
- `wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po` (nowy)
- `wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.mo` (nowy)
- `wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.po` (nowy)
- `wp-content/plugins/carei-reservation/languages/carei-reservation-en_GB.mo` (nowy)
- `.paul/phases/18-en-translation/18-01-PLAN.md` + SUMMARY
## Milestone v0.7 — Dwujęzyczność (PL/EN) ✅ COMPLETE
Plugin carei-reservation działa w pełni dwujęzycznie (PL + EN), z Flatpickr pokrywającym też kalendarze. Phase 16 (infrastruktura i18n) + Phase 17 (bilingual pakiety + Softra errors) + Phase 18 (tłumaczenia + UX).
## Phase 19 — Extras translations admin panel
- [Phase 19, Plan 01] Admin panel do zarządzania tłumaczeniami dynamicznych nazw extras z Softra API
- Nowe WP options: `carei_extras_seen` (auto-collected PL names), `carei_extras_translations` (admin override PL → EN)
- Helpery w `Carei_Admin_Panel`: `remember_extra_name()`, `get_extras_seen()`, `get_extras_translations()`, `translate_extra_name()`
- Submenu `wp-admin → Rezerwacje → Tłumaczenia extras` (lista PL + inputy EN + zapis z nonce/sanitize)
- `Carei_REST_Proxy::get_pricelist()` — runtime auto-collect + per-locale replacement nazw z fallbackiem do PL
- Reuse `resolve_locale()` z Phase 17 — brak duplikacji
## Zmienione pliki (Phase 19)
- `wp-content/plugins/carei-reservation/includes/class-admin-panel.php`
- `wp-content/plugins/carei-reservation/includes/class-rest-proxy.php`
- `.paul/phases/19-extras-translations-admin/19-01-PLAN.md` + SUMMARY
## Milestone v0.8 — Admin-managed tłumaczenia extras ✅ COMPLETE
System tłumaczeń pluginu kompletny: statyczne stringi (.po/.mo), bilingual pakiety + błędy Softra, dynamiczne extras z admin override. Użytkownik EN widzi całość w języku angielskim (poza świadomie pominiętymi danymi biznesowymi: nazwy miast/krajów/klas pojazdów).

View File

@@ -0,0 +1,189 @@
{"ts":"2026-04-22T12:06:07Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/carei.pagedev.pl/.paul/phases/15-remove-softra-insurance\"\",\"description\":\"Create phase 15 dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"int","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:07:05Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\15-remove-softra-insurance\\\\15-01-PLAN.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:07:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:07:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:08:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:08:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:08:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:22:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:22:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:22:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:23:25Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\15-remove-softra-insurance\\\\15-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:23:33Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\changelog\\\\2026-04-22.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:23:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:23:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:23:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:23:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T12:24:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T13:13:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\mu-plugins\\\\fix-sprintf-global.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:24:42Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/carei.pagedev.pl/.paul/phases/16-i18n-plugin-refactor\"\",\"description\":\"Create phase 16 dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interr","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:26:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\16-i18n-plugin-refactor\\\\16-01-PLAN.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:27:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:27:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:27:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:27:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:27:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:33:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\carei-reservation.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:33:34Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/languages\"\",\"description\":\"Create languages dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:33:45Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/includes/\"*.php \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/ass","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:34:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:40Z","tool":"Bash","cmd":"php -l \"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-elementor-widget.php\"\",\"description\":\"Validate PHP syntax\"},\","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:35:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:36:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:37:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:38:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:39:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:40:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:42:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:42:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-rest-proxy.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-rest-proxy.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:23Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/includes/class-rest-proxy.php\"\",\"description\":\"Check PHP syntax\"},\"tool_response\":{\"stdout\":\"No","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-search-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-cities-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-map-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-map-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-map-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-branches-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:43:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-branches-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:44:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-branches-widget.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:45:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\carei-reservation.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:46:50Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/carei-reservation.php\" 2>&1; php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:52:17Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\16-i18n-plugin-refactor\\\\16-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:52:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\changelog\\\\2026-04-22.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:52:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:52:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:52:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:52:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:53:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:54:50Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/carei.pagedev.pl/.paul/phases/17-bilingual-packages-and-softra-errors\"\",\"description\":\"Create phase 17 dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:56:59Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\17-bilingual-packages-and-softra-errors\\\\17-01-PLAN.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:57:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:57:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:57:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:57:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T17:57:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:01:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:01:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:01:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:01:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-rest-proxy.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:02:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-softra-api.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:02:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:02:51Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/includes/class-admin-panel.php\" 2>&1; php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:08:15Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\17-bilingual-packages-and-softra-errors\\\\17-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:08:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\changelog\\\\2026-04-22.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:08:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:08:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:08:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:08:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:09:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:10:08Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/carei.pagedev.pl/.paul/phases/18-en-translation\"; which msgfmt 2>&1 || echo \"msgfmt not available\"\",\"description\":\"Create dir + check msgfmt\"},\"tool_","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:12:19Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\18-en-translation\\\\18-01-PLAN.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:12:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:12:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:12:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:12:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:12:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:29:56Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\languages\\\\carei-reservation-en_US.po","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:30:05Z","tool":"Bash","cmd":"awk '/^msgstr/' \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po\" | grep -cP \"[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]\"; gre","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:30:50Z","tool":"Write","file":"C:\\\\Users\\\\jacek\\\\temp\\\\po2mo.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:30:59Z","tool":"Bash","cmd":"php \"C:/Users/jacek/temp/po2mo.php\" \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po\" \"C:/visual studio code/projekty/car","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:31:08Z","tool":"Bash","cmd":"cp \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.po\" \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/ca","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:31:19Z","tool":"Bash","cmd":"php -r \"\\\\$f='C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/languages/carei-reservation-en_US.mo'; \\\\$m=file_get_contents(\\\\$f,false,null,0,28); \\\\$h=un","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:39:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\carei-reservation.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:40:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:40:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:42:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:46:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:47:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:51:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:13Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\18-en-translation\\\\18-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\changelog\\\\2026-04-22.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:57:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T18:58:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:21:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:24:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:25:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:27:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:30:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:34:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:36:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:37:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:41:22Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/carei.pagedev.pl/.paul/phases/19-extras-translations-admin\"\",\"description\":\"Create phase 19 dir\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"i","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:43:05Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\19-extras-translations-admin\\\\19-01-PLAN.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:43:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:43:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:43:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:43:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:43:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:48:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:49:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:50:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-rest-proxy.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:51:06Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/includes/class-admin-panel.php\" 2>&1; php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:57:31Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\19-extras-translations-admin\\\\19-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:57:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\changelog\\\\2026-04-22.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:57:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:58:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:58:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:58:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}
{"ts":"2026-04-22T19:58:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"}

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*

View File

@@ -19,11 +19,12 @@
"lmtime": 1775065726885, "lmtime": 1775065726885,
"modified": false "modified": false
}, },
".claude": {},
".env": { ".env": {
"type": "-", "type": "-",
"size": 95, "size": 93,
"lmtime": 1774264688610, "lmtime": 1774264688610,
"modified": false "modified": true
}, },
".htaccess": { ".htaccess": {
"type": "-", "type": "-",
@@ -49,6 +50,7 @@
"lmtime": 0, "lmtime": 0,
"modified": false "modified": false
}, },
".paul": {},
"readme.html": { "readme.html": {
"type": "-", "type": "-",
"size": 7425, "size": 7425,
@@ -99,16 +101,16 @@
"css": { "css": {
"carei-reservation.css": { "carei-reservation.css": {
"type": "-", "type": "-",
"size": 40069, "size": 42236,
"lmtime": 1775067001902, "lmtime": 1776638170414,
"modified": false "modified": false
} }
}, },
"js": { "js": {
"carei-reservation.js": { "carei-reservation.js": {
"type": "-", "type": "-",
"size": 63668, "size": 72838,
"lmtime": 1775064606160, "lmtime": 1776638422166,
"modified": false "modified": false
} }
} }
@@ -122,8 +124,14 @@
"includes": { "includes": {
"class-admin-panel.php": { "class-admin-panel.php": {
"type": "-", "type": "-",
"size": 19203, "size": 27964,
"lmtime": 1774456454972, "lmtime": 1776637707828,
"modified": false
},
"class-branches-widget.php": {
"type": "-",
"size": 4090,
"lmtime": 1775066975977,
"modified": false "modified": false
}, },
"class-cities-widget.php": { "class-cities-widget.php": {
@@ -134,9 +142,9 @@
}, },
"class-elementor-widget.php": { "class-elementor-widget.php": {
"type": "-", "type": "-",
"size": 20123, "size": 18213,
"lmtime": 1774871052622, "lmtime": 1776637740783,
"modified": true "modified": false
}, },
"class-map-widget.php": { "class-map-widget.php": {
"type": "-", "type": "-",
@@ -146,8 +154,8 @@
}, },
"class-rest-proxy.php": { "class-rest-proxy.php": {
"type": "-", "type": "-",
"size": 11384, "size": 12395,
"lmtime": 1775064502720, "lmtime": 1776637720639,
"modified": false "modified": false
}, },
"class-search-widget.php": { "class-search-widget.php": {
@@ -161,12 +169,6 @@
"size": 9235, "size": 9235,
"lmtime": 1775064492252, "lmtime": 1775064492252,
"modified": false "modified": false
},
"class-branches-widget.php": {
"type": "-",
"size": 4090,
"lmtime": 1775066975977,
"modified": false
} }
} }
} }

BIN
wp-admin/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-content/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-content/languages/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,22 @@
<?php
/**
* Plugin Name: Fix: global sprintf backfill
* Description: Przywraca globalny `window.sprintf` dla pluginów liczących na starsze API wp.i18n (np. Automatic Translate Addon For Polylang). Naprawia "ReferenceError: sprintf is not defined" w bulk translate.
* Version: 1.0.0
* Author: Carei
*
* Musi leżeć w wp-content/mu-plugins/ — WP ładuje stąd automatycznie, bez aktywacji.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'admin_enqueue_scripts', function () {
wp_enqueue_script( 'wp-i18n' );
wp_add_inline_script(
'wp-i18n',
'if (window.wp && window.wp.i18n && typeof window.sprintf === "undefined") { window.sprintf = window.wp.i18n.sprintf; }',
'after'
);
}, 999 );

BIN
wp-content/plugins/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1647,3 +1647,94 @@ button.carei-reservation-trigger:hover {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
/* ──────────────────────────────────────────────────────────────
Flatpickr — compact theme + Carei colors
────────────────────────────────────────────────────────────── */
.flatpickr-calendar {
width: 260px !important;
font-size: 13px !important;
box-shadow: 0 6px 20px rgba(47, 36, 130, 0.15);
border-radius: 8px;
}
.flatpickr-calendar.open { z-index: 100000; }
.flatpickr-months { padding: 4px 0; }
.flatpickr-month { height: 28px; }
.flatpickr-current-month {
font-size: 13px;
padding: 4px 0 0 0;
height: 24px;
}
.flatpickr-current-month .flatpickr-monthDropdown-months,
.flatpickr-current-month input.cur-year {
font-size: 13px;
font-weight: 600;
}
.flatpickr-prev-month,
.flatpickr-next-month {
padding: 4px 8px;
height: 28px;
}
.flatpickr-prev-month svg,
.flatpickr-next-month svg { width: 12px; height: 12px; }
.flatpickr-weekdays { height: 24px; }
.flatpickr-weekday {
font-size: 11px !important;
font-weight: 600;
color: #6b6b8a !important;
}
.dayContainer {
padding: 2px;
width: 252px;
min-width: 252px;
max-width: 252px;
}
.flatpickr-day {
height: 30px;
line-height: 30px;
font-size: 12px;
max-width: 32px;
border-radius: 4px;
margin: 1px 0;
}
.flatpickr-day.today { border-color: #2F2482; }
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected:hover {
background: #2F2482 !important;
border-color: #2F2482 !important;
color: #fff !important;
}
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover {
background: rgba(47, 36, 130, 0.08);
}
.flatpickr-time {
height: 32px;
border-top: 1px solid rgba(47, 36, 130, 0.12);
}
.flatpickr-time input {
font-size: 13px;
height: 32px;
}
.flatpickr-time .flatpickr-time-separator,
.flatpickr-time .flatpickr-am-pm {
height: 32px;
line-height: 32px;
font-size: 13px;
}
.flatpickr-time input:hover,
.flatpickr-time input:focus {
background: rgba(47, 36, 130, 0.05);
}
.flatpickr-time .numInputWrapper span {
width: 12px;
height: 50%;
}
@media (max-width: 380px) {
.flatpickr-calendar { width: calc(100vw - 24px) !important; }
.dayContainer { width: 100%; min-width: 0; max-width: 100%; }
.flatpickr-day { max-width: none; }
}

View File

@@ -1,6 +1,26 @@
(function () { (function () {
'use strict'; 'use strict';
// ─── i18n Helpers ─────────────────────────────────────────────
var I18N = (window.careiI18n || {});
function t(key, fallback) {
return (key in I18N) ? I18N[key] : (fallback || key);
}
function tFmt(key, params, fallback) {
var str = t(key, fallback);
if (params && typeof str === 'string') {
Object.keys(params).forEach(function (k) {
str = str.replace(new RegExp('%' + k + '%', 'g'), params[k]);
});
}
return str;
}
function pluralPl(n, one, few, many) {
if (n === 1) return one;
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) return few;
return many;
}
var REST_URL = (window.careiReservation && window.careiReservation.restUrl) || '/wp-json/carei/v1/'; var REST_URL = (window.careiReservation && window.careiReservation.restUrl) || '/wp-json/carei/v1/';
var NONCE = (window.careiReservation && window.careiReservation.nonce) || ''; var NONCE = (window.careiReservation && window.careiReservation.nonce) || '';
@@ -48,13 +68,13 @@
if (!r.ok) { if (!r.ok) {
var status = r.status; var status = r.status;
return r.json().then(function (body) { return r.json().then(function (body) {
var msg = (body && body.message) || (body && body.detail) || ('Błąd API: HTTP ' + status); var msg = (body && body.message) || (body && body.detail) || tFmt('errorApiHttp', {status: status}, 'Błąd API: HTTP %status%');
var err = new Error(msg); var err = new Error(msg);
err.httpStatus = status; err.httpStatus = status;
throw err; throw err;
}).catch(function (parseErr) { }).catch(function (parseErr) {
if (parseErr.httpStatus) throw parseErr; if (parseErr.httpStatus) throw parseErr;
var err = new Error('Błąd API: HTTP ' + status); var err = new Error(tFmt('errorApiHttp', {status: status}, 'Błąd API: HTTP %status%'));
err.httpStatus = status; err.httpStatus = status;
throw err; throw err;
}); });
@@ -64,13 +84,13 @@
function handleFetchError(err, retryFn, isRetry) { function handleFetchError(err, retryFn, isRetry) {
if (err.name === 'AbortError') { if (err.name === 'AbortError') {
throw new Error('Przekroczono czas oczekiwania. Spróbuj ponownie.'); throw new Error(t('errorTimeout', 'Przekroczono czas oczekiwania. Spróbuj ponownie.'));
} }
if (err instanceof TypeError && !isRetry) { if (err instanceof TypeError && !isRetry) {
return retryFn(); return retryFn();
} }
if (err instanceof TypeError) { if (err instanceof TypeError) {
throw new Error('Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.'); throw new Error(t('errorNetwork', 'Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.'));
} }
throw err; throw err;
} }
@@ -79,7 +99,7 @@
var overlay, form, segmentSelect, dateFrom, dateTo, daysCount; var overlay, form, segmentSelect, dateFrom, dateTo, daysCount;
var pickupSelect, returnSelect, returnWrap, sameReturnCheck; var pickupSelect, returnSelect, returnWrap, sameReturnCheck;
var extrasWrapper, extrasContainer, insuranceContainer, abroadSection, abroadToggle, abroadSearch, abroadInput, abroadResults, abroadSelected, errorSummary; var extrasWrapper, extrasContainer, abroadSection, abroadToggle, abroadSearch, abroadInput, abroadResults, abroadSelected, errorSummary;
var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError; var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError;
var summaryBack, summaryConfirm; var summaryBack, summaryConfirm;
var successView, successNumber, successClose; var successView, successNumber, successClose;
@@ -104,7 +124,6 @@
sameReturnCheck = document.getElementById('carei-same-return'); sameReturnCheck = document.getElementById('carei-same-return');
extrasWrapper = document.getElementById('carei-extras-wrapper'); extrasWrapper = document.getElementById('carei-extras-wrapper');
extrasContainer = document.getElementById('carei-extras-container'); extrasContainer = document.getElementById('carei-extras-container');
insuranceContainer = document.getElementById('carei-insurance-container');
protectionContainer = document.getElementById('carei-protection-packages-container'); protectionContainer = document.getElementById('carei-protection-packages-container');
abroadSection = document.getElementById('carei-abroad-section'); abroadSection = document.getElementById('carei-abroad-section');
abroadToggle = document.getElementById('carei-abroad-toggle'); abroadToggle = document.getElementById('carei-abroad-toggle');
@@ -127,6 +146,89 @@
successClose = document.getElementById('carei-success-close'); successClose = document.getElementById('carei-success-close');
} }
// ─── Flatpickr date pickers (cross-browser, locale-aware) ─────
// If Flatpickr fails to load (CDN blocked, network, etc.), fall back gracefully
// to native <input type="datetime-local"> — never break date picking.
function initDatePickers() {
if (typeof window.flatpickr !== 'function') {
console.warn('[carei] Flatpickr not loaded — keeping native datetime-local picker.');
return;
}
var isEn = (document.documentElement.lang || '').toLowerCase().indexOf('en') === 0;
var locale = 'default';
if (!isEn && window.flatpickr.l10ns && window.flatpickr.l10ns.pl) {
locale = window.flatpickr.l10ns.pl;
}
var baseOpts = {
enableTime: true,
time_24hr: true,
dateFormat: 'Y-m-d\\TH:i',
altInput: true,
altFormat: isEn ? 'Y-m-d H:i' : 'd.m.Y H:i',
minuteIncrement: 15,
minDate: 'today',
locale: locale,
allowInput: false,
// Force Flatpickr on mobile too (otherwise falls back to native iOS/Android picker — inconsistent look, locale = OS)
disableMobile: true,
// With enableTime, flatpickr doesn't auto-close on date click (time still editable).
// Auto-close 1.5s after last change — gives user time to adjust time, then dismisses.
onChange: function (selectedDates, dateStr, instance) {
if (instance._careiCloseTimer) clearTimeout(instance._careiCloseTimer);
instance._careiCloseTimer = setTimeout(function () {
instance.close();
}, 1500);
}
};
// Modal inputs — use static:true so popup renders INSIDE input container
// (bypasses focus trap + z-index conflicts with modal overlay).
var modalTargets = [dateFrom, dateTo];
// Hero search inputs — render popup to body (default behavior is fine, no modal).
var heroTargets = [
document.getElementById('carei-search-date-from'),
document.getElementById('carei-search-date-to')
];
function attach(el, opts) {
if (!el) return;
// If already initialized, destroy previous instance (idempotent re-init)
if (el._flatpickr) {
try { el._flatpickr.destroy(); } catch (e) {}
}
try {
el.setAttribute('type', 'text');
window.flatpickr(el, opts);
// Bind click on the whole field container — any click (icon, label, padding, input)
// opens the flatpickr. Simplest UX.
var field = el.closest('.carei-form__field--date') || el.closest('.carei-search-form__field');
if (field && !field.dataset.careiFpBound) {
field.dataset.careiFpBound = '1';
field.style.cursor = 'pointer';
var lastCloseAt = 0;
if (el._flatpickr) {
var origOnClose = el._flatpickr.config.onClose || [];
el._flatpickr.config.onClose = (Array.isArray(origOnClose) ? origOnClose.slice() : [origOnClose]).concat([function () { lastCloseAt = Date.now(); }]);
}
field.addEventListener('click', function (e) {
// Ignore clicks that originated inside the flatpickr popup itself.
if (e.target.closest && e.target.closest('.flatpickr-calendar')) return;
// Debounce: if we just closed (within 300ms, e.g. via date selection), don't reopen.
if (Date.now() - lastCloseAt < 300) return;
if (el._flatpickr) el._flatpickr.open();
});
}
} catch (err) {
console.error('[carei] Flatpickr init failed for', el.id, err);
try { el.setAttribute('type', 'datetime-local'); } catch (e) {}
}
}
var modalOpts = Object.assign({}, baseOpts, { static: true });
modalTargets.forEach(function (el) { attach(el, modalOpts); });
heroTargets.forEach(function (el) { attach(el, baseOpts); });
}
// ─── State ──────────────────────────────────────────────────── // ─── State ────────────────────────────────────────────────────
var mapData = null; var mapData = null;
@@ -247,23 +349,23 @@
if (Array.isArray(classes) && classes.length > 0) { if (Array.isArray(classes) && classes.length > 0) {
allSegments = classes.map(function (c) { allSegments = classes.map(function (c) {
var val = typeof c === 'string' ? c : (c.name || c); var val = typeof c === 'string' ? c : (c.name || c);
var label = typeof c === 'string' ? ('Segment ' + c) : (c.description || c.name || c); var label = typeof c === 'string' ? tFmt('segmentLabel', {name: c}, 'Segment %name%') : (c.description || c.name || c);
return { value: val, label: label }; return { value: val, label: label };
}); });
populateSelect(segmentSelect, allSegments, 'Wybierz segment pojazdu'); populateSelect(segmentSelect, allSegments, t('selectSegment', 'Wybierz segment pojazdu'));
} else { } else {
populateSelect(segmentSelect, [], 'Brak segmentów'); populateSelect(segmentSelect, [], t('noSegments', 'Brak segmentów'));
} }
if (segmentSelect) setSelectLoading(segmentSelect, false); if (segmentSelect) setSelectLoading(segmentSelect, false);
if (pickupSelect) { if (pickupSelect) {
populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); populateSelect(pickupSelect, [], t('pickupPlaceholder', 'Najpierw wybierz segment'));
pickupSelect.disabled = true; pickupSelect.disabled = true;
} }
}).catch(function (err) { }).catch(function (err) {
console.error('Failed to load initial data:', err); console.error('Failed to load initial data:', err);
if (segmentSelect) { if (segmentSelect) {
populateSelect(segmentSelect, [], 'Błąd ładowania'); populateSelect(segmentSelect, [], t('errorLoading', 'Błąd ładowania'));
setSelectLoading(segmentSelect, false); setSelectLoading(segmentSelect, false);
} }
}); });
@@ -274,7 +376,7 @@
function onSegmentChange() { function onSegmentChange() {
var selectedSegment = segmentSelect ? segmentSelect.value : ''; var selectedSegment = segmentSelect ? segmentSelect.value : '';
if (!selectedSegment || !mapData || !pickupSelect) { if (!selectedSegment || !mapData || !pickupSelect) {
if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; } if (pickupSelect) { populateSelect(pickupSelect, [], t('pickupPlaceholder', 'Najpierw wybierz segment')); pickupSelect.disabled = true; }
hideExtras(); hideExtras();
return; return;
} }
@@ -289,10 +391,10 @@
} }
}); });
if (filteredOptions.length > 0) { if (filteredOptions.length > 0) {
populateSelect(pickupSelect, filteredOptions, 'Miejsce odbioru'); populateSelect(pickupSelect, filteredOptions, t('pickupLabel', 'Miejsce odbioru'));
pickupSelect.disabled = false; pickupSelect.disabled = false;
} else { } else {
populateSelect(pickupSelect, [], 'Brak lokalizacji dla tego segmentu'); populateSelect(pickupSelect, [], t('noPickupForSegment', 'Brak lokalizacji dla tego segmentu'));
pickupSelect.disabled = true; pickupSelect.disabled = true;
} }
if (returnSelect) { if (returnSelect) {
@@ -301,7 +403,7 @@
if (b.city) label += ' — ' + b.city; if (b.city) label += ' — ' + b.city;
return { value: b.name, label: label }; return { value: b.name, label: label };
}); });
populateSelect(returnSelect, returnOptions, 'Miejsce zwrotu'); populateSelect(returnSelect, returnOptions, t('returnLabel', 'Miejsce zwrotu'));
} }
hideExtras(); hideExtras();
} }
@@ -353,7 +455,7 @@
function checkPastAndWarn(input, label) { function checkPastAndWarn(input, label) {
if (!input || !input.value) return; if (!input || !input.value) return;
if (input.value < getNowLocal()) { if (input.value < getNowLocal()) {
warnPastDate(input, label + ' — data lub godzina już minęły'); warnPastDate(input, tFmt('warnPastDate', {label: label}, '%label% — data lub godzina już minęły'));
} else { } else {
clearDateError(input); clearDateError(input);
} }
@@ -367,13 +469,13 @@
if (!dateMinListenersBound) { if (!dateMinListenersBound) {
if (dateFrom) { if (dateFrom) {
dateFrom.addEventListener('change', function () { dateFrom.addEventListener('change', function () {
checkPastAndWarn(dateFrom, 'Rozpoczęcie'); checkPastAndWarn(dateFrom, t('dateStart', 'Rozpoczęcie'));
if (dateTo && dateFrom.value) dateTo.setAttribute('min', dateFrom.value); if (dateTo && dateFrom.value) dateTo.setAttribute('min', dateFrom.value);
}); });
} }
if (dateTo) { if (dateTo) {
dateTo.addEventListener('change', function () { dateTo.addEventListener('change', function () {
checkPastAndWarn(dateTo, 'Zakończenie'); checkPastAndWarn(dateTo, t('dateEnd', 'Zakończenie'));
}); });
} }
dateMinListenersBound = true; dateMinListenersBound = true;
@@ -417,16 +519,18 @@
if (!dateFrom || !dateTo || !daysCount) return; if (!dateFrom || !dateTo || !daysCount) return;
var from = new Date(dateFrom.value), to = new Date(dateTo.value); var from = new Date(dateFrom.value), to = new Date(dateTo.value);
if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) { if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) {
daysCount.innerHTML = 'Wybrano: <strong>0 dni</strong>'; return; daysCount.innerHTML = tFmt('daysCount', {count: 0, unit: t('dayMany', 'dni')}, 'Wybrano: <strong>%count% %unit%</strong>'); return;
} }
var diff = Math.ceil((to - from) / 86400000); var diff = Math.ceil((to - from) / 86400000);
daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + '</strong>'; var dayUnit = diff === 1 ? t('dayWord', 'dzień') : t('daysWord', 'dni');
daysCount.innerHTML = tFmt('daysCount', {count: diff, unit: dayUnit}, 'Wybrano: <strong>%count% %unit%</strong>');
} }
// ─── Protection Packages (WP-managed: SOFT, PREMIUM) ────────── // ─── Protection Packages (WP-managed: SOFT, PREMIUM) ──────────
function loadProtectionPackages() { function loadProtectionPackages() {
return fetch(REST_URL + 'protection-packages', { var lang = (document.documentElement.lang || '').toLowerCase().indexOf('en') === 0 ? 'en' : 'pl';
return fetch(REST_URL + 'protection-packages?lang=' + lang, {
credentials: 'same-origin' credentials: 'same-origin'
}).then(function (r) { return r.ok ? r.json() : null; }) }).then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) { .then(function (data) {
@@ -444,7 +548,7 @@
var pkg = protectionPackages[key]; var pkg = protectionPackages[key];
if (!pkg) return; if (!pkg) return;
var price = parseFloat(pkg.pricePerDay || 0); var price = parseFloat(pkg.pricePerDay || 0);
var priceLabel = price > 0 ? price.toFixed(0) + ' zł/doba' : 'Gratis'; var priceLabel = price > 0 ? tFmt('pricePerDay', {price: price.toFixed(0)}, '%price% zł/doba') : t('free', 'Gratis');
var descHtml = pkg.description ? '<span class="carei-form__protection-package__desc">' + escHtml(pkg.description) + '</span>' : ''; var descHtml = pkg.description ? '<span class="carei-form__protection-package__desc">' + escHtml(pkg.description) + '</span>' : '';
var card = document.createElement('label'); var card = document.createElement('label');
card.className = 'carei-form__protection-package'; card.className = 'carei-form__protection-package';
@@ -500,7 +604,7 @@
var pricelist = pricelists[0]; var pricelist = pricelists[0];
currentPriceListId = pricelist.id; currentPriceListId = pricelist.id;
var items = pricelist.additionalItems; var items = pricelist.additionalItems;
var insuranceItems = [], extraItems = []; var extraItems = [];
abroadItems = []; abroadItems = [];
selectedCountries = {}; selectedCountries = {};
if (Array.isArray(items)) { if (Array.isArray(items)) {
@@ -510,19 +614,20 @@
var code = (item.code || '').toUpperCase(); var code = (item.code || '').toUpperCase();
if (code.indexOf('BRAK') === 0 || code.indexOf('BRUD') === 0 || code.indexOf('KARA') === 0 || if (code.indexOf('BRAK') === 0 || code.indexOf('BRUD') === 0 || code.indexOf('KARA') === 0 ||
code.indexOf('MYCIE USŁU') === 0 || code === 'MYJ WEW') return; code.indexOf('MYCIE USŁU') === 0 || code === 'MYJ WEW') return;
// Drop Softra-insurance items — pakiety ochronne są zarządzane w panelu WP (SOFT/PREMIUM).
if (name.indexOf('ubezp') !== -1 || name.indexOf('ochrony') !== -1 ||
name.indexOf('zniesienie') !== -1 || name.indexOf('insurance') !== -1) {
return;
}
if (name.indexOf('wyjazd za granic') !== -1) { if (name.indexOf('wyjazd za granic') !== -1) {
item._countryName = parseCountryName(item.name); item._countryName = parseCountryName(item.name);
item._countryFlag = getCountryFlag(item._countryName); item._countryFlag = getCountryFlag(item._countryName);
abroadItems.push(item); abroadItems.push(item);
} else if (name.indexOf('ubezp') !== -1 || name.indexOf('ochrony') !== -1 ||
name.indexOf('zniesienie') !== -1 || name.indexOf('insurance') !== -1) {
insuranceItems.push(item);
} else { } else {
extraItems.push(item); extraItems.push(item);
} }
}); });
} }
if (insuranceContainer) { insuranceContainer.innerHTML = ''; insuranceItems.forEach(function (item) { insuranceContainer.appendChild(buildExtraCard(item)); }); }
if (extrasContainer) { extrasContainer.innerHTML = ''; extraItems.forEach(function (item) { extrasContainer.appendChild(buildExtraCard(item)); }); } if (extrasContainer) { extrasContainer.innerHTML = ''; extraItems.forEach(function (item) { extrasContainer.appendChild(buildExtraCard(item)); }); }
if (abroadSection) { abroadSection.style.display = abroadItems.length > 0 ? '' : 'none'; } if (abroadSection) { abroadSection.style.display = abroadItems.length > 0 ? '' : 'none'; }
renderAbroadSelected(); renderAbroadSelected();
@@ -533,8 +638,12 @@
var price = parseFloat(item.price || item.minPrice || 0); var price = parseFloat(item.price || item.minPrice || 0);
var maxPrice = parseFloat(item.maxPrice || 0); var maxPrice = parseFloat(item.maxPrice || 0);
var priceLabel = (maxPrice > 0 && maxPrice !== price) var priceLabel = (maxPrice > 0 && maxPrice !== price)
? 'od ' + price.toFixed(0) + ' do ' + maxPrice.toFixed(0) + ' zł' ? tFmt('priceRange', {min: price.toFixed(0), max: maxPrice.toFixed(0)}, 'od %min% do %max% zł')
: (price > 0 ? price.toFixed(0) + ' zł' + (item.unit === 'doba' ? '/doba' : '') : 'Gratis'); : (price > 0
? (item.unit === 'doba'
? tFmt('pricePerDay', {price: price.toFixed(0)}, '%price% zł/doba')
: tFmt('priceSimple', {price: price.toFixed(0)}, '%price% zł'))
: t('free', 'Gratis'));
var card = document.createElement('div'); var card = document.createElement('div');
card.className = 'carei-form__extra-card'; card.className = 'carei-form__extra-card';
card.innerHTML = card.innerHTML =
@@ -652,7 +761,7 @@
function buildCountryCard(item, isSelected) { function buildCountryCard(item, isSelected) {
var id = item.id || item.code; var id = item.id || item.code;
var price = parseFloat(item.price || item.minPrice || 0); var price = parseFloat(item.price || item.minPrice || 0);
var priceHtml = '<span class="carei-abroad__price-val">' + (price > 0 ? price.toFixed(0) + ' zł' : 'Gratis') + '</span>'; var priceHtml = '<span class="carei-abroad__price-val">' + (price > 0 ? tFmt('priceSimple', {price: price.toFixed(0)}, '%price% zł') : t('free', 'Gratis')) + '</span>';
var card = document.createElement('div'); var card = document.createElement('div');
card.className = 'carei-abroad__card' + (isSelected ? ' carei-abroad__card--selected' : ''); card.className = 'carei-abroad__card' + (isSelected ? ' carei-abroad__card--selected' : '');
@@ -660,7 +769,7 @@
'<span class="carei-abroad__flag">' + escHtml(item._countryFlag) + '</span>' + '<span class="carei-abroad__flag">' + escHtml(item._countryFlag) + '</span>' +
'<span class="carei-abroad__name">' + escHtml(item._countryName) + '</span>' + '<span class="carei-abroad__name">' + escHtml(item._countryName) + '</span>' +
'<span class="carei-abroad__price">' + priceHtml + '</span>' + '<span class="carei-abroad__price">' + priceHtml + '</span>' +
'<span role="button" tabindex="0" class="carei-abroad__action" data-abroad-id="' + escAttr(id) + '" title="' + (isSelected ? 'Usuń' : 'Dodaj') + '">' + '<span role="button" tabindex="0" class="carei-abroad__action" data-abroad-id="' + escAttr(id) + '" title="' + (isSelected ? t('btnRemove', 'Usuń') : t('btnAdd', 'Dodaj')) + '">' +
(isSelected (isSelected
? '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' ? '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'
: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v10M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' : '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v10M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'
@@ -694,7 +803,7 @@
if (!select) return; if (!select) return;
select.innerHTML = ''; select.innerHTML = '';
var ph = document.createElement('option'); var ph = document.createElement('option');
ph.value = ''; ph.disabled = true; ph.selected = true; ph.textContent = placeholder || 'Wybierz...'; ph.value = ''; ph.disabled = true; ph.selected = true; ph.textContent = placeholder || t('selectPlaceholder', 'Wybierz...');
select.appendChild(ph); select.appendChild(ph);
options.forEach(function (opt) { options.forEach(function (opt) {
var o = document.createElement('option'); var o = document.createElement('option');
@@ -720,15 +829,15 @@
// ─── Validation ─────────────────────────────────────────────── // ─── Validation ───────────────────────────────────────────────
var requiredFields = [ var requiredFields = [
{ id: 'carei-segment', type: 'select', msg: 'Wybierz segment pojazdu' }, { id: 'carei-segment', type: 'select', msgKey: 'selectSegment', msgFallback: 'Wybierz segment pojazdu' },
{ id: 'carei-date-from', type: 'input', msg: 'Podaj datę rozpoczęcia' }, { id: 'carei-date-from', type: 'input', msgKey: 'enterDateFrom', msgFallback: 'Podaj datę rozpoczęcia' },
{ id: 'carei-date-to', type: 'input', msg: 'Podaj datę zakończenia' }, { id: 'carei-date-to', type: 'input', msgKey: 'enterDateTo', msgFallback: 'Podaj datę zakończenia' },
{ id: 'carei-pickup-branch', type: 'select', msg: 'Wybierz miejsce odbioru' }, { id: 'carei-pickup-branch', type: 'select', msgKey: 'selectPickup', msgFallback: 'Wybierz miejsce odbioru' },
{ id: 'carei-firstname', type: 'input', msg: 'Podaj imię' }, { id: 'carei-firstname', type: 'input', msgKey: 'enterFirstName', msgFallback: 'Podaj imię' },
{ id: 'carei-lastname', type: 'input', msg: 'Podaj nazwisko' }, { id: 'carei-lastname', type: 'input', msgKey: 'enterLastName', msgFallback: 'Podaj nazwisko' },
{ id: 'carei-email', type: 'email', msg: 'Podaj poprawny adres e-mail' }, { id: 'carei-email', type: 'email', msgKey: 'enterEmail', msgFallback: 'Podaj poprawny adres e-mail' },
{ id: 'carei-phone', type: 'phone', msg: 'Podaj numer telefonu (min. 9 cyfr)' }, { id: 'carei-phone', type: 'phone', msgKey: 'enterPhone', msgFallback: 'Podaj numer telefonu (min. 9 cyfr)' },
{ id: 'carei-privacy', type: 'checkbox', msg: 'Wymagana zgoda na Politykę Prywatności' } { id: 'carei-privacy', type: 'checkbox', msgKey: 'privacyRequired', msgFallback: 'Wymagana zgoda na Politykę Prywatności' }
]; ];
function validateForm() { function validateForm() {
@@ -747,23 +856,23 @@
else if (f.type === 'pesel') { if (!/^\d{11}$/.test(el.value.trim())) hasError = true; } else if (f.type === 'pesel') { if (!/^\d{11}$/.test(el.value.trim())) hasError = true; }
else if (f.type === 'select') { if (!el.value) hasError = true; } else if (f.type === 'select') { if (!el.value) hasError = true; }
else { if (!el.value.trim()) hasError = true; } else { if (!el.value.trim()) hasError = true; }
if (hasError) { valid = false; markFieldError(el, f.msg, f.type); } if (hasError) { valid = false; markFieldError(el, t(f.msgKey, f.msgFallback), f.type); }
}); });
var now = new Date(); var now = new Date();
if (dateFrom && dateFrom.value && new Date(dateFrom.value) < now) { if (dateFrom && dateFrom.value && new Date(dateFrom.value) < now) {
valid = false; markFieldError(dateFrom, 'Data lub godzina rozpoczęcia już minęły', 'input'); valid = false; markFieldError(dateFrom, t('dateStartPast', 'Data lub godzina rozpoczęcia już minęły'), 'input');
} }
if (dateTo && dateTo.value && new Date(dateTo.value) < now) { if (dateTo && dateTo.value && new Date(dateTo.value) < now) {
valid = false; markFieldError(dateTo, 'Data lub godzina zakończenia już minęły', 'input'); valid = false; markFieldError(dateTo, t('dateEndPast', 'Data lub godzina zakończenia już minęły'), 'input');
} }
if (dateFrom && dateTo && dateFrom.value && dateTo.value) { if (dateFrom && dateTo && dateFrom.value && dateTo.value) {
if (new Date(dateTo.value) <= new Date(dateFrom.value)) { if (new Date(dateTo.value) <= new Date(dateFrom.value)) {
valid = false; markFieldError(dateTo, 'Data zakończenia musi być po dacie rozpoczęcia', 'input'); valid = false; markFieldError(dateTo, t('dateEndAfterStart', 'Data zakończenia musi być po dacie rozpoczęcia'), 'input');
} }
} }
if (sameReturnCheck && !sameReturnCheck.checked && returnSelect && !returnSelect.value) { if (sameReturnCheck && !sameReturnCheck.checked && returnSelect && !returnSelect.value) {
valid = false; markFieldError(returnSelect, 'Wybierz miejsce zwrotu', 'select'); valid = false; markFieldError(returnSelect, t('selectReturn', 'Wybierz miejsce zwrotu'), 'select');
} }
if (errorSummary) errorSummary.style.display = valid ? 'none' : 'block'; if (errorSummary) errorSummary.style.display = valid ? 'none' : 'block';
return valid; return valid;
@@ -826,11 +935,11 @@
if (state === 'loading') { if (state === 'loading') {
btn.disabled = true; btn.disabled = true;
btn.setAttribute('aria-busy', 'true'); btn.setAttribute('aria-busy', 'true');
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Przetwarzanie...'; btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> ' + escHtml(t('btnProcessing', 'Przetwarzanie...'));
} else { } else {
btn.disabled = false; btn.disabled = false;
btn.setAttribute('aria-busy', 'false'); btn.setAttribute('aria-busy', 'false');
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Wyślij'; btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> ' + escHtml(t('btnSubmit', 'Wyślij'));
} }
} }
@@ -859,7 +968,7 @@
function createCustomerAndShowSummary() { function createCustomerAndShowSummary() {
var fd = collectFormData(); var fd = collectFormData();
hideSummaryError(); hideSummaryError();
announce('Ładowanie podsumowania...'); announce(t('loadingSummary', 'Ładowanie podsumowania...'));
apiPost('customer', { apiPost('customer', {
firstName: fd.firstName, firstName: fd.firstName,
@@ -878,11 +987,11 @@
currentCustomerId = res.customerId; currentCustomerId = res.customerId;
return loadPricingSummary(fd); return loadPricingSummary(fd);
} }
throw new Error(res.rejectReason || 'Nie udało się utworzyć klienta'); throw new Error(res.rejectReason || t('errorCustomerCreate', 'Nie udało się utworzyć klienta'));
}).catch(function (err) { }).catch(function (err) {
console.error('Customer creation failed:', err); console.error('Customer creation failed:', err);
setSubmitState('ready'); setSubmitState('ready');
showFormError('Błąd tworzenia klienta: ' + err.message); showFormError(tFmt('errorCustomerCreatePrefix', {msg: err.message}, 'Błąd tworzenia klienta: %msg%'));
}); });
} }
@@ -905,7 +1014,7 @@
}).catch(function (err) { }).catch(function (err) {
console.error('Pricing summary failed:', err); console.error('Pricing summary failed:', err);
setSubmitState('ready'); setSubmitState('ready');
showFormError('Błąd pobierania podsumowania: ' + err.message); showFormError(tFmt('errorSummaryPrefix', {msg: err.message}, 'Błąd pobierania podsumowania: %msg%'));
}); });
} }
@@ -965,7 +1074,12 @@
function buildBookingComments(userMessage) { function buildBookingComments(userMessage) {
var pkg = getSelectedProtectionPayload(); var pkg = getSelectedProtectionPayload();
if (!pkg) return userMessage || ''; if (!pkg) return userMessage || '';
var pkgLine = 'Pakiet ochronny: ' + pkg.name + ' — ' + pkg.pricePerDay.toFixed(2) + ' zł/doba × ' + pkg.days + ' = ' + pkg.total.toFixed(2) + ' zł (do doliczenia poza systemem)'; var pkgLine = tFmt('protectionComment', {
name: pkg.name,
perDay: pkg.pricePerDay.toFixed(2),
days: pkg.days,
total: pkg.total.toFixed(2)
}, 'Pakiet ochronny: %name% — %perDay% zł/doba × %days% = %total% zł (do doliczenia poza systemem)');
return userMessage ? (pkgLine + '\n\n' + userMessage) : pkgLine; return userMessage ? (pkgLine + '\n\n' + userMessage) : pkgLine;
} }
@@ -1023,7 +1137,7 @@
// Animated transition: form → summary // Animated transition: form → summary
transitionStep(form, summaryOverlay, function () { transitionStep(form, summaryOverlay, function () {
announce('Podsumowanie rezerwacji'); announce(t('announceSummary', 'Podsumowanie rezerwacji'));
var title = summaryOverlay.querySelector('.carei-summary__title'); var title = summaryOverlay.querySelector('.carei-summary__title');
if (title) title.focus(); if (title) title.focus();
}); });
@@ -1039,40 +1153,40 @@
returnLabel = returnSelect.options[returnSelect.selectedIndex].text; returnLabel = returnSelect.options[returnSelect.selectedIndex].text;
} }
var html = var html =
'<div><strong>Segment:</strong> ' + escHtml(segLabel) + '</div>' + '<div><strong>' + escHtml(t('labelSegment', 'Segment')) + ':</strong> ' + escHtml(segLabel) + '</div>' +
'<div><strong>Od:</strong> ' + escHtml(fd.dateFrom.replace('T', ' ')) + '</div>' + '<div><strong>' + escHtml(t('labelFrom', 'Od')) + ':</strong> ' + escHtml(fd.dateFrom.replace('T', ' ')) + '</div>' +
'<div><strong>Do:</strong> ' + escHtml(fd.dateTo.replace('T', ' ')) + '</div>' + '<div><strong>' + escHtml(t('labelTo', 'Do')) + ':</strong> ' + escHtml(fd.dateTo.replace('T', ' ')) + '</div>' +
'<div><strong>Miejsce odbioru:</strong> ' + escHtml(pickupLabel) + '</div>'; '<div><strong>' + escHtml(t('labelPickup', 'Miejsce odbioru')) + ':</strong> ' + escHtml(pickupLabel) + '</div>';
if (returnLabel) { if (returnLabel) {
html += '<div><strong>Miejsce zwrotu:</strong> ' + escHtml(returnLabel) + '</div>'; html += '<div><strong>' + escHtml(t('labelReturn', 'Miejsce zwrotu')) + ':</strong> ' + escHtml(returnLabel) + '</div>';
} }
html += '<div><strong>Najemca:</strong> ' + escHtml(fd.firstName + ' ' + fd.lastName) + '</div>' + html += '<div><strong>' + escHtml(t('labelRenter', 'Najemca')) + ':</strong> ' + escHtml(fd.firstName + ' ' + fd.lastName) + '</div>' +
'<div><strong>Email:</strong> ' + escHtml(fd.email) + '</div>' + '<div><strong>' + escHtml(t('labelEmail', 'Email')) + ':</strong> ' + escHtml(fd.email) + '</div>' +
'<div><strong>Telefon:</strong> ' + escHtml(fd.phone) + '</div>'; '<div><strong>' + escHtml(t('labelPhone', 'Telefon')) + ':</strong> ' + escHtml(fd.phone) + '</div>';
// Selected extras // Selected extras
var selectedExtras = getSelectedExtrasForApi(); var selectedExtras = getSelectedExtrasForApi();
var pkgForDetails = getSelectedProtectionPayload(); var pkgForDetails = getSelectedProtectionPayload();
if (selectedExtras.length > 0 || pkgForDetails) { if (selectedExtras.length > 0 || pkgForDetails) {
html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">'; html += '<div style="margin-top:8px"><strong>' + escHtml(t('labelSelectedOptions', 'Wybrane opcje')) + ':</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">';
selectedExtras.forEach(function (ex) { selectedExtras.forEach(function (ex) {
var totalPrice = ex.priceAfterDiscount * (ex.amount || 1); var totalPrice = ex.priceAfterDiscount * (ex.amount || 1);
var priceInfo = ex.unit === 'doba' && ex.amount > 1 var priceInfo = ex.unit === 'doba' && ex.amount > 1
? fmtPrice(ex.priceAfterDiscount) + ' zł/doba × ' + ex.amount + ' = ' + fmtPrice(totalPrice) + ' zł' ? tFmt('priceLineDaily', {perDay: fmtPrice(ex.priceAfterDiscount), days: ex.amount, total: fmtPrice(totalPrice)}, '%perDay% zł/doba × %days% = %total% zł')
: fmtPrice(totalPrice) + ' zł'; : tFmt('priceSimpleFmt', {price: fmtPrice(totalPrice)}, '%price% zł');
html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>'; html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>';
}); });
if (pkgForDetails) { if (pkgForDetails) {
var pkgInfo = pkgForDetails.days > 1 var pkgInfo = pkgForDetails.days > 1
? fmtPrice(pkgForDetails.pricePerDay) + ' zł/doba × ' + pkgForDetails.days + ' = ' + fmtPrice(pkgForDetails.total) + ' zł' ? tFmt('priceLineDaily', {perDay: fmtPrice(pkgForDetails.pricePerDay), days: pkgForDetails.days, total: fmtPrice(pkgForDetails.total)}, '%perDay% zł/doba × %days% = %total% zł')
: fmtPrice(pkgForDetails.total) + ' zł'; : tFmt('priceSimpleFmt', {price: fmtPrice(pkgForDetails.total)}, '%price% zł');
html += '<li>' + escHtml(pkgForDetails.name) + ' — ' + pkgInfo + '</li>'; html += '<li>' + escHtml(pkgForDetails.name) + ' — ' + pkgInfo + '</li>';
} }
html += '</ul>'; html += '</ul>';
} }
if (fd.message) { if (fd.message) {
html += '<div style="margin-top:8px"><strong>Wiadomość:</strong> ' + escHtml(fd.message) + '</div>'; html += '<div style="margin-top:8px"><strong>' + escHtml(t('labelMessage', 'Wiadomość')) + ':</strong> ' + escHtml(fd.message) + '</div>';
} }
summaryDetails.innerHTML = html; summaryDetails.innerHTML = html;
@@ -1082,19 +1196,19 @@
// Price table // Price table
if (summaryTable && summary.pricelist) { if (summaryTable && summary.pricelist) {
var html = '<table><thead><tr><th>Nazwa</th><th>Ilość</th><th>Netto</th><th>Brutto</th></tr></thead><tbody>'; var html = '<table><thead><tr><th>' + escHtml(t('thName', 'Nazwa')) + '</th><th>' + escHtml(t('thQuantity', 'Ilość')) + '</th><th>' + escHtml(t('thNet', 'Netto')) + '</th><th>' + escHtml(t('thGross', 'Brutto')) + '</th></tr></thead><tbody>';
summary.pricelist.forEach(function (item) { summary.pricelist.forEach(function (item) {
var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : ''; var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : '';
html += '<tr' + rowClass + '>' + html += '<tr' + rowClass + '>' +
'<td>' + escHtml(toSentenceCase(item.name)) + (item.addedBySystem ? ' <small>(auto)</small>' : '') + '</td>' + '<td>' + escHtml(toSentenceCase(item.name)) + (item.addedBySystem ? ' <small>(' + escHtml(t('labelAuto', 'auto')) + ')</small>' : '') + '</td>' +
'<td>' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '</td>' + '<td>' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '</td>' +
'<td>' + fmtPrice(item.netValue) + '</td>' + '<td>' + fmtPrice(item.netValue) + '</td>' +
'<td>' + fmtPrice(item.grossValue) + '</td></tr>'; '<td>' + fmtPrice(item.grossValue) + '</td></tr>';
}); });
if (protectionPayload) { if (protectionPayload) {
html += '<tr class="carei-summary__protection-row">' + html += '<tr class="carei-summary__protection-row">' +
'<td>' + escHtml(protectionPayload.name) + ' <small>(do doliczenia)</small></td>' + '<td>' + escHtml(protectionPayload.name) + ' <small>(' + escHtml(t('labelExtraCharge', 'do doliczenia')) + ')</small></td>' +
'<td>' + protectionPayload.days + ' doba</td>' + '<td>' + protectionPayload.days + ' ' + escHtml(pluralPl(protectionPayload.days, t('dayOne', 'doba'), t('dayFew', 'doby'), t('dayMany', 'dób'))) + '</td>' +
'<td>—</td>' + '<td>—</td>' +
'<td>' + fmtPrice(protectionPayload.total) + '</td></tr>'; '<td>' + fmtPrice(protectionPayload.total) + '</td></tr>';
} }
@@ -1108,12 +1222,12 @@
var protectionTotal = protectionPayload ? protectionPayload.total : 0; var protectionTotal = protectionPayload ? protectionPayload.total : 0;
var grandGross = softraGross + protectionTotal; var grandGross = softraGross + protectionTotal;
var totalsHtml = var totalsHtml =
'<div class="carei-summary__total-row"><span class="carei-summary__total-label">Netto:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalNetValue) + '</span></div>' + '<div class="carei-summary__total-row"><span class="carei-summary__total-label">' + escHtml(t('thNet', 'Netto')) + ':</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalNetValue) + '</span></div>' +
'<div class="carei-summary__total-row"><span class="carei-summary__total-label">VAT:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalVatValue) + '</span></div>'; '<div class="carei-summary__total-row"><span class="carei-summary__total-label">' + escHtml(t('labelVat', 'VAT')) + ':</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalVatValue) + '</span></div>';
if (protectionPayload) { if (protectionPayload) {
totalsHtml += '<div class="carei-summary__total-row"><span class="carei-summary__total-label">Pakiet ochronny:</span><span class="carei-summary__total-value">' + fmtPrice(protectionPayload.total) + '</span></div>'; totalsHtml += '<div class="carei-summary__total-row"><span class="carei-summary__total-label">' + escHtml(t('labelProtectionPackage', 'Pakiet ochronny')) + ':</span><span class="carei-summary__total-value">' + fmtPrice(protectionPayload.total) + '</span></div>';
} }
totalsHtml += '<div class="carei-summary__total-row carei-summary__total-row--gross"><span class="carei-summary__total-label">Do zapłaty:</span><span class="carei-summary__total-value">' + fmtPrice(grandGross) + '</span></div>'; totalsHtml += '<div class="carei-summary__total-row carei-summary__total-row--gross"><span class="carei-summary__total-label">' + escHtml(t('labelToPay', 'Do zapłaty')) + ':</span><span class="carei-summary__total-value">' + tFmt('priceSimpleFmt', {price: fmtPrice(grandGross)}, '%price% zł') + '</span></div>';
summaryTotal.innerHTML = totalsHtml; summaryTotal.innerHTML = totalsHtml;
} }
} }
@@ -1136,7 +1250,7 @@
if (summaryConfirm) { if (summaryConfirm) {
summaryConfirm.disabled = true; summaryConfirm.disabled = true;
summaryConfirm.setAttribute('aria-busy', 'true'); summaryConfirm.setAttribute('aria-busy', 'true');
summaryConfirm.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Rezerwuję...'; summaryConfirm.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> ' + escHtml(t('btnBookingInProgress', 'Rezerwuję...'));
} }
hideSummaryError(); hideSummaryError();
@@ -1178,7 +1292,7 @@
showSuccessView(res.reservationNo || res.reservationId); showSuccessView(res.reservationNo || res.reservationId);
return; return;
} }
throw new Error(translateRejectReason(res.rejectReason) || 'Rezerwacja nie powiodła się'); throw new Error(translateRejectReason(res.rejectReason) || t('errorBookingFailed', 'Rezerwacja nie powiodła się'));
}).catch(function (err) { }).catch(function (err) {
console.error('Booking failed:', err); console.error('Booking failed:', err);
showSummaryError(err.message); showSummaryError(err.message);
@@ -1189,12 +1303,12 @@
function translateRejectReason(reason) { function translateRejectReason(reason) {
if (!reason) return null; if (!reason) return null;
var map = { var map = {
'CAR_NOT_FOUND': 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.', 'CAR_NOT_FOUND': t('rejectCarNotFound', 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.'),
'INVALID_DATE_RANGE': 'Nieprawidłowy zakres dat', 'INVALID_DATE_RANGE': t('rejectInvalidDateRange', 'Nieprawidłowy zakres dat'),
'BRANCH_NOT_FOUND': 'Nie znaleziono oddziału', 'BRANCH_NOT_FOUND': t('rejectBranchNotFound', 'Nie znaleziono oddziału'),
'CUSTOMER_ALREADY_EXISTS': 'Klient o tych danych już istnieje w systemie', 'CUSTOMER_ALREADY_EXISTS': t('rejectCustomerExists', 'Klient o tych danych już istnieje w systemie'),
'INVALID_PESEL': 'Nieprawidłowy numer PESEL', 'INVALID_PESEL': t('rejectInvalidPesel', 'Nieprawidłowy numer PESEL'),
'PRICE_LIST_EXPIRED': 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.' 'PRICE_LIST_EXPIRED': t('rejectPriceListExpired', 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.')
}; };
return map[reason] || reason; return map[reason] || reason;
} }
@@ -1203,16 +1317,16 @@
if (summaryConfirm) { if (summaryConfirm) {
summaryConfirm.disabled = false; summaryConfirm.disabled = false;
summaryConfirm.setAttribute('aria-busy', 'false'); summaryConfirm.setAttribute('aria-busy', 'false');
summaryConfirm.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Potwierdź rezerwację'; summaryConfirm.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> ' + escHtml(t('btnConfirmBooking', 'Potwierdź rezerwację'));
} }
} }
// ─── Success View ───────────────────────────────────────────── // ─── Success View ─────────────────────────────────────────────
function showSuccessView(reservationNo) { function showSuccessView(reservationNo) {
if (successNumber) successNumber.textContent = 'Nr zamówienia: ' + reservationNo; if (successNumber) successNumber.textContent = tFmt('orderNumber', {no: reservationNo}, 'Nr zamówienia: %no%');
transitionStep(summaryOverlay, successView, function () { transitionStep(summaryOverlay, successView, function () {
announce('Rezerwacja potwierdzona'); announce(t('announceBookingConfirmed', 'Rezerwacja potwierdzona'));
var title = successView.querySelector('.carei-success__title'); var title = successView.querySelector('.carei-success__title');
if (title) title.focus(); if (title) title.focus();
}); });
@@ -1226,7 +1340,7 @@
currentCustomerId = null; currentCustomerId = null;
currentReservationId = null; currentReservationId = null;
hideExtras(); hideExtras();
if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; } if (pickupSelect) { populateSelect(pickupSelect, [], t('pickupPlaceholder', 'Najpierw wybierz segment')); pickupSelect.disabled = true; }
closeModal(); closeModal();
} }
@@ -1324,13 +1438,13 @@
if (Array.isArray(classes) && classes.length > 0) { if (Array.isArray(classes) && classes.length > 0) {
var segments = classes.map(function (c) { var segments = classes.map(function (c) {
var val = typeof c === 'string' ? c : (c.name || c); var val = typeof c === 'string' ? c : (c.name || c);
var label = typeof c === 'string' ? ('Segment ' + c) : (c.description || c.name || c); var label = typeof c === 'string' ? tFmt('segmentLabel', {name: c}, 'Segment %name%') : (c.description || c.name || c);
return { value: val, label: label }; return { value: val, label: label };
}); });
populateSelect(searchSegment, segments, 'Wybierz segment'); populateSelect(searchSegment, segments, t('selectSegmentShort', 'Wybierz segment'));
} }
if (searchPickup) { if (searchPickup) {
populateSelect(searchPickup, [], 'Najpierw wybierz segment'); populateSelect(searchPickup, [], t('pickupPlaceholder', 'Najpierw wybierz segment'));
searchPickup.disabled = true; searchPickup.disabled = true;
} }
}).catch(function (err) { }).catch(function (err) {
@@ -1354,10 +1468,10 @@
} }
}); });
if (opts.length > 0) { if (opts.length > 0) {
populateSelect(searchPickup, opts, 'Miejsce odbioru'); populateSelect(searchPickup, opts, t('pickupLabel', 'Miejsce odbioru'));
searchPickup.disabled = false; searchPickup.disabled = false;
} else { } else {
populateSelect(searchPickup, [], 'Brak lokalizacji'); populateSelect(searchPickup, [], t('noLocations', 'Brak lokalizacji'));
searchPickup.disabled = true; searchPickup.disabled = true;
} }
}); });
@@ -1449,6 +1563,9 @@
// Inicjalizuj search form niezależnie od modala // Inicjalizuj search form niezależnie od modala
initSearchForm(); initSearchForm();
// Flatpickr on both modal + hero inputs — safe to call even if modal absent
initDatePickers();
if (!overlay || !form) return; if (!overlay || !form) return;
initModal(); initModal();

View File

@@ -52,6 +52,13 @@ require_once CAREI_RESERVATION_PATH . 'includes/class-softra-api.php';
require_once CAREI_RESERVATION_PATH . 'includes/class-rest-proxy.php'; require_once CAREI_RESERVATION_PATH . 'includes/class-rest-proxy.php';
require_once CAREI_RESERVATION_PATH . 'includes/class-admin-panel.php'; require_once CAREI_RESERVATION_PATH . 'includes/class-admin-panel.php';
/**
* Load plugin textdomain
*/
add_action( 'plugins_loaded', function () {
load_plugin_textdomain( 'carei-reservation', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
} );
/** /**
* Initialize plugin on plugins_loaded * Initialize plugin on plugins_loaded
*/ */
@@ -64,7 +71,7 @@ add_action( 'plugins_loaded', function () {
if ( empty( $api_url ) || empty( $username ) || empty( $password ) ) { if ( empty( $api_url ) || empty( $username ) || empty( $password ) ) {
add_action( 'admin_notices', function () { add_action( 'admin_notices', function () {
echo '<div class="notice notice-error"><p><strong>Carei Reservation:</strong> Brak konfiguracji API w pliku .env (url, username, password).</p></div>'; echo '<div class="notice notice-error"><p><strong>Carei Reservation:</strong> ' . esc_html__( 'Brak konfiguracji API w pliku .env (url, username, password).', 'carei-reservation' ) . '</p></div>';
} ); } );
return; return;
} }
@@ -103,16 +110,38 @@ add_action( 'elementor/widgets/register', function ( $widgets_manager ) {
* Enqueue frontend assets * Enqueue frontend assets
*/ */
add_action( 'wp_enqueue_scripts', function () { add_action( 'wp_enqueue_scripts', function () {
// Flatpickr — cross-browser, locale-aware date/time picker (CDN)
wp_register_style(
'carei-flatpickr-css',
'https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css',
array(),
'4.6.13'
);
wp_register_script(
'carei-flatpickr-js',
'https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js',
array(),
'4.6.13',
true
);
wp_register_script(
'carei-flatpickr-pl',
'https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/l10n/pl.js',
array( 'carei-flatpickr-js' ),
'4.6.13',
true
);
wp_register_style( wp_register_style(
'carei-reservation-css', 'carei-reservation-css',
CAREI_RESERVATION_URL . 'assets/css/carei-reservation.css', CAREI_RESERVATION_URL . 'assets/css/carei-reservation.css',
array(), array( 'carei-flatpickr-css' ),
CAREI_RESERVATION_VERSION CAREI_RESERVATION_VERSION
); );
wp_register_script( wp_register_script(
'carei-reservation-js', 'carei-reservation-js',
CAREI_RESERVATION_URL . 'assets/js/carei-reservation.js', CAREI_RESERVATION_URL . 'assets/js/carei-reservation.js',
array(), array( 'carei-flatpickr-js', 'carei-flatpickr-pl' ),
CAREI_RESERVATION_VERSION, CAREI_RESERVATION_VERSION,
true true
); );
@@ -120,4 +149,115 @@ add_action( 'wp_enqueue_scripts', function () {
'restUrl' => esc_url_raw( rest_url( 'carei/v1/' ) ), 'restUrl' => esc_url_raw( rest_url( 'carei/v1/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ), 'nonce' => wp_create_nonce( 'wp_rest' ),
) ); ) );
wp_localize_script( 'carei-reservation-js', 'careiI18n', array(
// API / sieć
'errorApiHttp' => esc_html__( 'Błąd API: HTTP %status%', 'carei-reservation' ),
'errorTimeout' => esc_html__( 'Przekroczono czas oczekiwania. Spróbuj ponownie.', 'carei-reservation' ),
'errorNetwork' => esc_html__( 'Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.', 'carei-reservation' ),
'errorLoading' => esc_html__( 'Błąd ładowania', 'carei-reservation' ),
// Selecty / placeholdery
'selectPlaceholder' => esc_html__( 'Wybierz...', 'carei-reservation' ),
'selectSegment' => esc_html__( 'Wybierz segment pojazdu', 'carei-reservation' ),
'selectSegmentShort' => esc_html__( 'Wybierz segment', 'carei-reservation' ),
'selectPickup' => esc_html__( 'Wybierz miejsce odbioru', 'carei-reservation' ),
'selectReturn' => esc_html__( 'Wybierz miejsce zwrotu', 'carei-reservation' ),
'segmentLabel' => esc_html__( 'Segment %name%', 'carei-reservation' ),
'noSegments' => esc_html__( 'Brak segmentów', 'carei-reservation' ),
'pickupPlaceholder' => esc_html__( 'Najpierw wybierz segment', 'carei-reservation' ),
'pickupLabel' => esc_html__( 'Miejsce odbioru', 'carei-reservation' ),
'returnLabel' => esc_html__( 'Miejsce zwrotu', 'carei-reservation' ),
'noPickupForSegment' => esc_html__( 'Brak lokalizacji dla tego segmentu', 'carei-reservation' ),
'noLocations' => esc_html__( 'Brak lokalizacji', 'carei-reservation' ),
// Daty
'dateStart' => esc_html__( 'Rozpoczęcie', 'carei-reservation' ),
'dateEnd' => esc_html__( 'Zakończenie', 'carei-reservation' ),
'warnPastDate' => esc_html__( '%label% — data lub godzina już minęły', 'carei-reservation' ),
'dateStartPast' => esc_html__( 'Data lub godzina rozpoczęcia już minęły', 'carei-reservation' ),
'dateEndPast' => esc_html__( 'Data lub godzina zakończenia już minęły', 'carei-reservation' ),
'dateEndAfterStart' => esc_html__( 'Data zakończenia musi być po dacie rozpoczęcia', 'carei-reservation' ),
'enterDateFrom' => esc_html__( 'Podaj datę rozpoczęcia', 'carei-reservation' ),
'enterDateTo' => esc_html__( 'Podaj datę zakończenia', 'carei-reservation' ),
// Dni/Doby (pluralizacja PL)
'daysCount' => wp_kses( __( 'Wybrano: <strong>%count% %unit%</strong>', 'carei-reservation' ), array( 'strong' => array() ) ),
'dayWord' => esc_html__( 'dzień', 'carei-reservation' ),
'daysWord' => esc_html__( 'dni', 'carei-reservation' ),
'dayOne' => esc_html__( 'doba', 'carei-reservation' ),
'dayFew' => esc_html__( 'doby', 'carei-reservation' ),
'dayMany' => esc_html__( 'dób', 'carei-reservation' ),
// Ceny
'pricePerDay' => esc_html__( '%price% zł/doba', 'carei-reservation' ),
'priceSimple' => esc_html__( '%price% zł', 'carei-reservation' ),
'priceSimpleFmt' => esc_html__( '%price% zł', 'carei-reservation' ),
'priceRange' => esc_html__( 'od %min% do %max% zł', 'carei-reservation' ),
'priceLineDaily' => esc_html__( '%perDay% zł/doba × %days% = %total% zł', 'carei-reservation' ),
'free' => esc_html__( 'Gratis', 'carei-reservation' ),
// Zagranica / akcje
'btnRemove' => esc_html__( 'Usuń', 'carei-reservation' ),
'btnAdd' => esc_html__( 'Dodaj', 'carei-reservation' ),
// Walidacja formularza
'enterFirstName' => esc_html__( 'Podaj imię', 'carei-reservation' ),
'enterLastName' => esc_html__( 'Podaj nazwisko', 'carei-reservation' ),
'enterEmail' => esc_html__( 'Podaj poprawny adres e-mail', 'carei-reservation' ),
'enterPhone' => esc_html__( 'Podaj numer telefonu (min. 9 cyfr)', 'carei-reservation' ),
'privacyRequired' => esc_html__( 'Wymagana zgoda na Politykę Prywatności', 'carei-reservation' ),
// Przyciski
'btnSubmit' => esc_html__( 'Wyślij', 'carei-reservation' ),
'btnProcessing' => esc_html__( 'Przetwarzanie...', 'carei-reservation' ),
'btnBookingInProgress' => esc_html__( 'Rezerwuję...', 'carei-reservation' ),
'btnConfirmBooking' => esc_html__( 'Potwierdź rezerwację', 'carei-reservation' ),
// Flow klienta / rezerwacji
'loadingSummary' => esc_html__( 'Ładowanie podsumowania...', 'carei-reservation' ),
'errorCustomerCreate' => esc_html__( 'Nie udało się utworzyć klienta', 'carei-reservation' ),
'errorCustomerCreatePrefix' => esc_html__( 'Błąd tworzenia klienta: %msg%', 'carei-reservation' ),
'errorSummaryPrefix' => esc_html__( 'Błąd pobierania podsumowania: %msg%', 'carei-reservation' ),
'errorBookingFailed' => esc_html__( 'Rezerwacja nie powiodła się', 'carei-reservation' ),
// Pakiet ochronny (komentarz booking)
'protectionComment' => esc_html__( 'Pakiet ochronny: %name% — %perDay% zł/doba × %days% = %total% zł (do doliczenia poza systemem)', 'carei-reservation' ),
// Podsumowanie — etykiety
'labelSegment' => esc_html__( 'Segment', 'carei-reservation' ),
'labelFrom' => esc_html__( 'Od', 'carei-reservation' ),
'labelTo' => esc_html__( 'Do', 'carei-reservation' ),
'labelPickup' => esc_html__( 'Miejsce odbioru', 'carei-reservation' ),
'labelReturn' => esc_html__( 'Miejsce zwrotu', 'carei-reservation' ),
'labelRenter' => esc_html__( 'Najemca', 'carei-reservation' ),
'labelEmail' => esc_html__( 'Email', 'carei-reservation' ),
'labelPhone' => esc_html__( 'Telefon', 'carei-reservation' ),
'labelSelectedOptions' => esc_html__( 'Wybrane opcje', 'carei-reservation' ),
'labelMessage' => esc_html__( 'Wiadomość', 'carei-reservation' ),
'labelAuto' => esc_html__( 'auto', 'carei-reservation' ),
'labelExtraCharge' => esc_html__( 'do doliczenia', 'carei-reservation' ),
'labelVat' => esc_html__( 'VAT', 'carei-reservation' ),
'labelProtectionPackage' => esc_html__( 'Pakiet ochronny', 'carei-reservation' ),
'labelToPay' => esc_html__( 'Do zapłaty', 'carei-reservation' ),
// Tabela
'thName' => esc_html__( 'Nazwa', 'carei-reservation' ),
'thQuantity' => esc_html__( 'Ilość', 'carei-reservation' ),
'thNet' => esc_html__( 'Netto', 'carei-reservation' ),
'thGross' => esc_html__( 'Brutto', 'carei-reservation' ),
// Reject reasons (Softra → PL fallback)
'rejectCarNotFound' => esc_html__( 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.', 'carei-reservation' ),
'rejectInvalidDateRange' => esc_html__( 'Nieprawidłowy zakres dat', 'carei-reservation' ),
'rejectBranchNotFound' => esc_html__( 'Nie znaleziono oddziału', 'carei-reservation' ),
'rejectCustomerExists' => esc_html__( 'Klient o tych danych już istnieje w systemie', 'carei-reservation' ),
'rejectInvalidPesel' => esc_html__( 'Nieprawidłowy numer PESEL', 'carei-reservation' ),
'rejectPriceListExpired' => esc_html__( 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.', 'carei-reservation' ),
// Success / aria
'orderNumber' => esc_html__( 'Nr zamówienia: %no%', 'carei-reservation' ),
'announceBookingConfirmed' => esc_html__( 'Rezerwacja potwierdzona', 'carei-reservation' ),
'announceSummary' => esc_html__( 'Podsumowanie rezerwacji', 'carei-reservation' ),
) );
} ); } );

View File

@@ -9,13 +9,27 @@ class Carei_Admin_Panel {
const META_PREFIX = '_carei_'; const META_PREFIX = '_carei_';
const PROTECTION_OPTION = 'carei_protection_packages'; const PROTECTION_OPTION = 'carei_protection_packages';
const EXTRAS_SEEN_OPTION = 'carei_extras_seen';
const EXTRAS_TRANSLATIONS_OPTION = 'carei_extras_translations';
private static $statuses = array( private static $statuses = array(
'nowe' => array( 'label' => 'Nowe', 'color' => '#2F2482' ), 'nowe' => array( 'color' => '#2F2482' ),
'przeczytane' => array( 'label' => 'Przeczytane', 'color' => '#f59e0b' ), 'przeczytane' => array( 'color' => '#f59e0b' ),
'zrealizowane' => array( 'label' => 'Zrealizowane', 'color' => '#22c55e' ), 'zrealizowane' => array( 'color' => '#22c55e' ),
); );
private static function get_status_label( $key ) {
switch ( $key ) {
case 'nowe':
return __( 'Nowe', 'carei-reservation' );
case 'przeczytane':
return __( 'Przeczytane', 'carei-reservation' );
case 'zrealizowane':
return __( 'Zrealizowane', 'carei-reservation' );
}
return $key;
}
public function __construct() { public function __construct() {
add_action( 'init', array( $this, 'register_post_type' ) ); add_action( 'init', array( $this, 'register_post_type' ) );
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( $this, 'admin_columns' ) ); add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( $this, 'admin_columns' ) );
@@ -28,6 +42,135 @@ class Carei_Admin_Panel {
add_action( 'admin_head', array( $this, 'admin_styles' ) ); add_action( 'admin_head', array( $this, 'admin_styles' ) );
add_action( 'admin_menu', array( $this, 'register_protection_packages_page' ) ); add_action( 'admin_menu', array( $this, 'register_protection_packages_page' ) );
add_action( 'admin_post_carei_save_protection_packages', array( $this, 'handle_protection_packages_save' ) ); add_action( 'admin_post_carei_save_protection_packages', array( $this, 'handle_protection_packages_save' ) );
add_action( 'admin_menu', array( $this, 'register_extras_translations_page' ) );
add_action( 'admin_post_carei_save_extras_translations', array( $this, 'handle_extras_translations_save' ) );
}
// ─── Extras Translations (dynamic Softra pricelist item names) ────
public static function get_extras_seen() {
$seen = get_option( self::EXTRAS_SEEN_OPTION, array() );
if ( ! is_array( $seen ) ) return array();
$seen = array_values( array_unique( array_filter( array_map( 'strval', $seen ), 'strlen' ) ) );
sort( $seen, SORT_NATURAL | SORT_FLAG_CASE );
return $seen;
}
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 = get_option( self::EXTRAS_SEEN_OPTION, array() );
if ( ! is_array( $seen ) ) $seen = array();
if ( ! in_array( $pl_name, $seen, true ) ) {
$seen[] = $pl_name;
update_option( self::EXTRAS_SEEN_OPTION, $seen, false );
}
}
public static function translate_extra_name( $pl_name, $locale = null ) {
if ( $locale === null ) {
$wp_locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
$locale = ( 0 === strpos( (string) $wp_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;
}
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' )
);
}
public function render_extras_translations_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Brak uprawnień.', 'carei-reservation' ) );
}
$seen = self::get_extras_seen();
$trans = self::get_extras_translations();
$saved = isset( $_GET['carei_saved'] ) && $_GET['carei_saved'] === '1';
?>
<div class="wrap">
<h1><?php esc_html_e( 'Tłumaczenia extras', 'carei-reservation' ); ?></h1>
<p><?php esc_html_e( 'Tłumaczenia nazw dodatkowych opcji (extras) zwracanych z API Softra. Puste pole = wersja polska jest używana także w wersji angielskiej (fallback).', 'carei-reservation' ); ?></p>
<?php if ( $saved ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Zapisano.', 'carei-reservation' ); ?></p></div>
<?php endif; ?>
<?php if ( empty( $seen ) ) : ?>
<div class="notice notice-info"><p><?php esc_html_e( 'Brak zebranych pozycji. Otwórz formularz rezerwacji i wybierz daty/oddział/klasę aby załadować pricelist — pozycje pojawią się tutaj automatycznie.', 'carei-reservation' ); ?></p></div>
<?php else : ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="carei-extras-form">
<input type="hidden" name="action" value="carei_save_extras_translations">
<?php wp_nonce_field( 'carei_extras_translations', 'carei_extras_nonce' ); ?>
<table class="form-table striped">
<thead>
<tr>
<th style="width: 40%;"><?php esc_html_e( 'Nazwa PL (z API Softra)', 'carei-reservation' ); ?></th>
<th><?php esc_html_e( 'Tłumaczenie EN', 'carei-reservation' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $seen as $pl_name ) :
$en = isset( $trans[ $pl_name ] ) ? (string) $trans[ $pl_name ] : '';
?>
<tr>
<td><strong><?php echo esc_html( $pl_name ); ?></strong></td>
<td>
<input type="text" name="translations[<?php echo esc_attr( $pl_name ); ?>]" value="<?php echo esc_attr( $en ); ?>" class="regular-text" placeholder="<?php echo esc_attr__( 'EN translation...', 'carei-reservation' ); ?>">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php submit_button( __( 'Zapisz tłumaczenia', 'carei-reservation' ) ); ?>
</form>
<?php endif; ?>
</div>
<?php
}
public function handle_extras_translations_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Brak uprawnień.', 'carei-reservation' ) );
}
if ( ! isset( $_POST['carei_extras_nonce'] ) || ! wp_verify_nonce( $_POST['carei_extras_nonce'], 'carei_extras_translations' ) ) {
wp_die( esc_html__( 'Nieprawidłowy token.', 'carei-reservation' ) );
}
$input = isset( $_POST['translations'] ) && is_array( $_POST['translations'] ) ? $_POST['translations'] : array();
$clean = array();
foreach ( $input as $pl_name => $en_value ) {
$pl = trim( (string) wp_unslash( $pl_name ) );
$en = sanitize_text_field( wp_unslash( (string) $en_value ) );
if ( $pl === '' ) continue;
$clean[ $pl ] = $en;
}
update_option( self::EXTRAS_TRANSLATIONS_OPTION, $clean, false );
$redirect = add_query_arg(
array(
'post_type' => self::POST_TYPE,
'page' => 'carei-extras-translations',
'carei_saved' => '1',
),
admin_url( 'edit.php' )
);
wp_safe_redirect( $redirect );
exit;
} }
// ─── Protection Packages (SOFT / PREMIUM) ──────────────────── // ─── Protection Packages (SOFT / PREMIUM) ────────────────────
@@ -35,16 +178,20 @@ class Carei_Admin_Panel {
public static function get_protection_packages_defaults() { public static function get_protection_packages_defaults() {
return array( return array(
'soft' => array( 'soft' => array(
'name' => 'Ubezpieczenie SOFT', 'name' => __( 'Ubezpieczenie SOFT', 'carei-reservation' ),
'name_en' => 'SOFT Protection',
'pricePerDay' => 0, 'pricePerDay' => 0,
'active' => true, 'active' => true,
'description' => '', 'description' => '',
'description_en' => '',
), ),
'premium' => array( 'premium' => array(
'name' => 'Ubezpieczenie PREMIUM', 'name' => __( 'Ubezpieczenie PREMIUM', 'carei-reservation' ),
'name_en' => 'PREMIUM Protection',
'pricePerDay' => 0, 'pricePerDay' => 0,
'active' => true, 'active' => true,
'description' => '', 'description' => '',
'description_en' => '',
), ),
); );
} }
@@ -60,9 +207,11 @@ class Carei_Admin_Panel {
$item = isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ? $stored[ $key ] : array(); $item = isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ? $stored[ $key ] : array();
$out[ $key ] = array( $out[ $key ] = array(
'name' => isset( $item['name'] ) && $item['name'] !== '' ? (string) $item['name'] : $def['name'], 'name' => isset( $item['name'] ) && $item['name'] !== '' ? (string) $item['name'] : $def['name'],
'name_en' => isset( $item['name_en'] ) ? (string) $item['name_en'] : '',
'pricePerDay' => isset( $item['pricePerDay'] ) ? (float) $item['pricePerDay'] : (float) $def['pricePerDay'], 'pricePerDay' => isset( $item['pricePerDay'] ) ? (float) $item['pricePerDay'] : (float) $def['pricePerDay'],
'active' => isset( $item['active'] ) ? (bool) $item['active'] : (bool) $def['active'], 'active' => isset( $item['active'] ) ? (bool) $item['active'] : (bool) $def['active'],
'description' => isset( $item['description'] ) ? (string) $item['description'] : $def['description'], 'description' => isset( $item['description'] ) ? (string) $item['description'] : $def['description'],
'description_en' => isset( $item['description_en'] ) ? (string) $item['description_en'] : '',
); );
} }
return $out; return $out;
@@ -71,8 +220,8 @@ class Carei_Admin_Panel {
public function register_protection_packages_page() { public function register_protection_packages_page() {
add_submenu_page( add_submenu_page(
'edit.php?post_type=' . self::POST_TYPE, 'edit.php?post_type=' . self::POST_TYPE,
'Pakiety ochronne', __( 'Pakiety ochronne', 'carei-reservation' ),
'Pakiety ochronne', __( 'Pakiety ochronne', 'carei-reservation' ),
'manage_options', 'manage_options',
'carei-protection-packages', 'carei-protection-packages',
array( $this, 'render_protection_packages_page' ) array( $this, 'render_protection_packages_page' )
@@ -81,16 +230,16 @@ class Carei_Admin_Panel {
public function render_protection_packages_page() { public function render_protection_packages_page() {
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' ); wp_die( esc_html__( 'Brak uprawnień.', 'carei-reservation' ) );
} }
$data = self::get_protection_packages(); $data = self::get_protection_packages();
$saved = isset( $_GET['carei_saved'] ) && $_GET['carei_saved'] === '1'; $saved = isset( $_GET['carei_saved'] ) && $_GET['carei_saved'] === '1';
?> ?>
<div class="wrap"> <div class="wrap">
<h1>Pakiety ochronne</h1> <h1><?php esc_html_e( 'Pakiety ochronne', 'carei-reservation' ); ?></h1>
<p>Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji.</p> <p><?php echo wp_kses( __( 'Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji.', 'carei-reservation' ), array( 'strong' => array() ) ); ?></p>
<?php if ( $saved ) : ?> <?php if ( $saved ) : ?>
<div class="notice notice-success is-dismissible"><p>Zapisano.</p></div> <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Zapisano.', 'carei-reservation' ); ?></p></div>
<?php endif; ?> <?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="carei-protection-form"> <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="carei-protection-form">
<input type="hidden" name="action" value="carei_save_protection_packages"> <input type="hidden" name="action" value="carei_save_protection_packages">
@@ -100,29 +249,43 @@ class Carei_Admin_Panel {
$pkg = $data[ $key ]; $pkg = $data[ $key ];
?> ?>
<div class="carei-protection-card"> <div class="carei-protection-card">
<h2>Pakiet <?php echo esc_html( $label ); ?></h2> <h2><?php echo esc_html( sprintf( __( 'Pakiet %s', 'carei-reservation' ), $label ) ); ?></h2>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_name">Nazwa wyświetlana</label></th> <th><label for="carei_<?php echo esc_attr( $key ); ?>_name"><?php esc_html_e( 'Nazwa wyświetlana (PL)', 'carei-reservation' ); ?></label></th>
<td><input type="text" id="carei_<?php echo esc_attr( $key ); ?>_name" name="packages[<?php echo esc_attr( $key ); ?>][name]" value="<?php echo esc_attr( $pkg['name'] ); ?>" class="regular-text" required></td> <td><input type="text" id="carei_<?php echo esc_attr( $key ); ?>_name" name="packages[<?php echo esc_attr( $key ); ?>][name]" value="<?php echo esc_attr( $pkg['name'] ); ?>" class="regular-text" required></td>
</tr> </tr>
<tr> <tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_price">Cena za dobę (zł)</label></th> <th><label for="carei_<?php echo esc_attr( $key ); ?>_name_en"><?php esc_html_e( 'Nazwa wyświetlana (EN)', 'carei-reservation' ); ?></label></th>
<td>
<input type="text" id="carei_<?php echo esc_attr( $key ); ?>_name_en" name="packages[<?php echo esc_attr( $key ); ?>][name_en]" value="<?php echo esc_attr( $pkg['name_en'] ); ?>" class="regular-text" placeholder="<?php echo esc_attr__( 'Np. SOFT Protection', 'carei-reservation' ); ?>">
<p class="description"><?php esc_html_e( 'Puste = fallback do wersji polskiej.', 'carei-reservation' ); ?></p>
</td>
</tr>
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_price"><?php esc_html_e( 'Cena za dobę (zł)', 'carei-reservation' ); ?></label></th>
<td><input type="number" id="carei_<?php echo esc_attr( $key ); ?>_price" name="packages[<?php echo esc_attr( $key ); ?>][pricePerDay]" value="<?php echo esc_attr( $pkg['pricePerDay'] ); ?>" min="0" step="0.01" class="small-text" required></td> <td><input type="number" id="carei_<?php echo esc_attr( $key ); ?>_price" name="packages[<?php echo esc_attr( $key ); ?>][pricePerDay]" value="<?php echo esc_attr( $pkg['pricePerDay'] ); ?>" min="0" step="0.01" class="small-text" required></td>
</tr> </tr>
<tr> <tr>
<th>Status</th> <th><?php esc_html_e( 'Status', 'carei-reservation' ); ?></th>
<td><label><input type="checkbox" name="packages[<?php echo esc_attr( $key ); ?>][active]" value="1" <?php checked( $pkg['active'] ); ?>> Aktywny (widoczny w modalu)</label></td> <td><label><input type="checkbox" name="packages[<?php echo esc_attr( $key ); ?>][active]" value="1" <?php checked( $pkg['active'] ); ?>> <?php esc_html_e( 'Aktywny (widoczny w modalu)', 'carei-reservation' ); ?></label></td>
</tr> </tr>
<tr> <tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_desc">Opis / zakres usług</label></th> <th><label for="carei_<?php echo esc_attr( $key ); ?>_desc"><?php esc_html_e( 'Opis / zakres usług (PL)', 'carei-reservation' ); ?></label></th>
<td><textarea id="carei_<?php echo esc_attr( $key ); ?>_desc" name="packages[<?php echo esc_attr( $key ); ?>][description]" rows="3" cols="60" class="large-text"><?php echo esc_textarea( $pkg['description'] ); ?></textarea></td> <td><textarea id="carei_<?php echo esc_attr( $key ); ?>_desc" name="packages[<?php echo esc_attr( $key ); ?>][description]" rows="3" cols="60" class="large-text"><?php echo esc_textarea( $pkg['description'] ); ?></textarea></td>
</tr> </tr>
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_desc_en"><?php esc_html_e( 'Opis / zakres usług (EN)', 'carei-reservation' ); ?></label></th>
<td>
<textarea id="carei_<?php echo esc_attr( $key ); ?>_desc_en" name="packages[<?php echo esc_attr( $key ); ?>][description_en]" rows="3" cols="60" class="large-text" placeholder="<?php echo esc_attr__( 'Basic protection package...', 'carei-reservation' ); ?>"><?php echo esc_textarea( $pkg['description_en'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Puste = fallback do wersji polskiej.', 'carei-reservation' ); ?></p>
</td>
</tr>
</table> </table>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php submit_button( 'Zapisz pakiety' ); ?> <?php submit_button( __( 'Zapisz pakiety', 'carei-reservation' ) ); ?>
</form> </form>
</div> </div>
<style> <style>
@@ -134,10 +297,10 @@ class Carei_Admin_Panel {
public function handle_protection_packages_save() { public function handle_protection_packages_save() {
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' ); wp_die( esc_html__( 'Brak uprawnień.', 'carei-reservation' ) );
} }
if ( ! isset( $_POST['carei_protection_nonce'] ) || ! wp_verify_nonce( $_POST['carei_protection_nonce'], 'carei_protection_packages' ) ) { if ( ! isset( $_POST['carei_protection_nonce'] ) || ! wp_verify_nonce( $_POST['carei_protection_nonce'], 'carei_protection_packages' ) ) {
wp_die( 'Nieprawidłowy token.' ); wp_die( esc_html__( 'Nieprawidłowy token.', 'carei-reservation' ) );
} }
$input = isset( $_POST['packages'] ) && is_array( $_POST['packages'] ) ? $_POST['packages'] : array(); $input = isset( $_POST['packages'] ) && is_array( $_POST['packages'] ) ? $_POST['packages'] : array();
@@ -146,15 +309,19 @@ class Carei_Admin_Panel {
foreach ( $defaults as $key => $def ) { foreach ( $defaults as $key => $def ) {
$raw = isset( $input[ $key ] ) && is_array( $input[ $key ] ) ? $input[ $key ] : array(); $raw = isset( $input[ $key ] ) && is_array( $input[ $key ] ) ? $input[ $key ] : array();
$name = isset( $raw['name'] ) ? sanitize_text_field( wp_unslash( $raw['name'] ) ) : $def['name']; $name = isset( $raw['name'] ) ? sanitize_text_field( wp_unslash( $raw['name'] ) ) : $def['name'];
$name_en = isset( $raw['name_en'] ) ? sanitize_text_field( wp_unslash( $raw['name_en'] ) ) : '';
$price = isset( $raw['pricePerDay'] ) ? (float) $raw['pricePerDay'] : 0; $price = isset( $raw['pricePerDay'] ) ? (float) $raw['pricePerDay'] : 0;
if ( $price < 0 ) { $price = 0; } if ( $price < 0 ) { $price = 0; }
$active = ! empty( $raw['active'] ); $active = ! empty( $raw['active'] );
$desc = isset( $raw['description'] ) ? sanitize_textarea_field( wp_unslash( $raw['description'] ) ) : ''; $desc = isset( $raw['description'] ) ? sanitize_textarea_field( wp_unslash( $raw['description'] ) ) : '';
$desc_en = isset( $raw['description_en'] ) ? sanitize_textarea_field( wp_unslash( $raw['description_en'] ) ) : '';
$clean[ $key ] = array( $clean[ $key ] = array(
'name' => $name !== '' ? $name : $def['name'], 'name' => $name !== '' ? $name : $def['name'],
'name_en' => $name_en,
'pricePerDay' => $price, 'pricePerDay' => $price,
'active' => $active, 'active' => $active,
'description' => $desc, 'description' => $desc,
'description_en' => $desc_en,
); );
} }
update_option( self::PROTECTION_OPTION, $clean ); update_option( self::PROTECTION_OPTION, $clean );
@@ -174,15 +341,15 @@ class Carei_Admin_Panel {
public function register_post_type() { public function register_post_type() {
register_post_type( self::POST_TYPE, array( register_post_type( self::POST_TYPE, array(
'labels' => array( 'labels' => array(
'name' => 'Rezerwacje', 'name' => __( 'Rezerwacje', 'carei-reservation' ),
'singular_name' => 'Rezerwacja', 'singular_name' => __( 'Rezerwacja', 'carei-reservation' ),
'menu_name' => 'Rezerwacje', 'menu_name' => __( 'Rezerwacje', 'carei-reservation' ),
'all_items' => 'Wszystkie rezerwacje', 'all_items' => __( 'Wszystkie rezerwacje', 'carei-reservation' ),
'view_item' => 'Zobacz rezerwację', 'view_item' => __( 'Zobacz rezerwację', 'carei-reservation' ),
'edit_item' => 'Szczegóły rezerwacji', 'edit_item' => __( 'Szczegóły rezerwacji', 'carei-reservation' ),
'search_items' => 'Szukaj rezerwacji', 'search_items' => __( 'Szukaj rezerwacji', 'carei-reservation' ),
'not_found' => 'Nie znaleziono rezerwacji', 'not_found' => __( 'Nie znaleziono rezerwacji', 'carei-reservation' ),
'not_found_in_trash' => 'Brak rezerwacji w koszu', 'not_found_in_trash' => __( 'Brak rezerwacji w koszu', 'carei-reservation' ),
), ),
'public' => false, 'public' => false,
'show_ui' => true, 'show_ui' => true,
@@ -202,13 +369,13 @@ class Carei_Admin_Panel {
public function admin_columns( $columns ) { public function admin_columns( $columns ) {
return array( return array(
'cb' => '<input type="checkbox" />', 'cb' => '<input type="checkbox" />',
'reservation_no' => 'Nr rezerwacji', 'reservation_no' => __( 'Nr rezerwacji', 'carei-reservation' ),
'client' => 'Klient', 'client' => __( 'Klient', 'carei-reservation' ),
'segment' => 'Segment', 'segment' => __( 'Segment', 'carei-reservation' ),
'dates' => 'Daty', 'dates' => __( 'Daty', 'carei-reservation' ),
'branch' => 'Oddział', 'branch' => __( 'Oddział', 'carei-reservation' ),
'carei_status' => 'Status', 'carei_status' => __( 'Status', 'carei-reservation' ),
'date' => 'Data', 'date' => __( 'Data', 'carei-reservation' ),
); );
} }
@@ -250,7 +417,7 @@ class Carei_Admin_Panel {
printf( printf(
'<span class="carei-status-badge" style="background:%s;">%s</span>', '<span class="carei-status-badge" style="background:%s;">%s</span>',
esc_attr( $info['color'] ), esc_attr( $info['color'] ),
esc_html( $info['label'] ) esc_html( self::get_status_label( $status ) )
); );
break; break;
} }
@@ -264,13 +431,13 @@ class Carei_Admin_Panel {
} }
$current = isset( $_GET['carei_status'] ) ? sanitize_text_field( $_GET['carei_status'] ) : ''; $current = isset( $_GET['carei_status'] ) ? sanitize_text_field( $_GET['carei_status'] ) : '';
echo '<select name="carei_status">'; echo '<select name="carei_status">';
echo '<option value="">Wszystkie statusy</option>'; echo '<option value="">' . esc_html__( 'Wszystkie statusy', 'carei-reservation' ) . '</option>';
foreach ( self::$statuses as $key => $info ) { foreach ( self::$statuses as $key => $info ) {
printf( printf(
'<option value="%s"%s>%s</option>', '<option value="%s"%s>%s</option>',
esc_attr( $key ), esc_attr( $key ),
selected( $current, $key, false ), selected( $current, $key, false ),
esc_html( $info['label'] ) esc_html( self::get_status_label( $key ) )
); );
} }
echo '</select>'; echo '</select>';
@@ -306,7 +473,7 @@ class Carei_Admin_Panel {
public function add_meta_boxes() { public function add_meta_boxes() {
add_meta_box( add_meta_box(
'carei_reservation_details', 'carei_reservation_details',
'Szczegóły rezerwacji', __( 'Szczegóły rezerwacji', 'carei-reservation' ),
array( $this, 'render_meta_box' ), array( $this, 'render_meta_box' ),
self::POST_TYPE, self::POST_TYPE,
'normal', 'normal',
@@ -345,7 +512,14 @@ class Carei_Admin_Panel {
$price = isset( $protection['pricePerDay'] ) ? (float) $protection['pricePerDay'] : 0; $price = isset( $protection['pricePerDay'] ) ? (float) $protection['pricePerDay'] : 0;
$days = isset( $protection['days'] ) ? (int) $protection['days'] : 0; $days = isset( $protection['days'] ) ? (int) $protection['days'] : 0;
$total = isset( $protection['total'] ) ? (float) $protection['total'] : ( $price * $days ); $total = isset( $protection['total'] ) ? (float) $protection['total'] : ( $price * $days );
$protection_str = sprintf( '%s — %s zł/doba × %d = %s zł', $name, number_format( $price, 2, ',', ' ' ), $days, number_format( $total, 2, ',', ' ' ) ); $protection_str = sprintf(
/* translators: 1: package name, 2: price per day, 3: number of days, 4: total */
__( '%1$s — %2$s zł/doba × %3$d = %4$s zł', 'carei-reservation' ),
$name,
number_format( $price, 2, ',', ' ' ),
$days,
number_format( $total, 2, ',', ' ' )
);
} }
$address = $meta['address'] ? json_decode( $meta['address'], true ) : null; $address = $meta['address'] ? json_decode( $meta['address'], true ) : null;
@@ -373,34 +547,34 @@ class Carei_Admin_Panel {
?> ?>
<table class="carei-meta-table"> <table class="carei-meta-table">
<tr><th>Nr rezerwacji</th><td><?php echo esc_html( $meta['reservation_no'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Nr rezerwacji', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['reservation_no'] ?: '—' ); ?></td></tr>
<tr><th>ID rezerwacji (Softra)</th><td><?php echo esc_html( $meta['reservation_id'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'ID rezerwacji (Softra)', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['reservation_id'] ?: '—' ); ?></td></tr>
<tr><th>ID klienta (Softra)</th><td><?php echo esc_html( $meta['customer_id'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'ID klienta (Softra)', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['customer_id'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr> <tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Segment</th><td><?php echo esc_html( $meta['segment'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Segment', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['segment'] ?: '—' ); ?></td></tr>
<tr><th>Data od</th><td><?php echo esc_html( $from_fmt ); ?></td></tr> <tr><th><?php esc_html_e( 'Data od', 'carei-reservation' ); ?></th><td><?php echo esc_html( $from_fmt ); ?></td></tr>
<tr><th>Data do</th><td><?php echo esc_html( $to_fmt ); ?></td></tr> <tr><th><?php esc_html_e( 'Data do', 'carei-reservation' ); ?></th><td><?php echo esc_html( $to_fmt ); ?></td></tr>
<tr><th>Oddział odbioru</th><td><?php echo esc_html( $meta['pickup_branch'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Oddział odbioru', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['pickup_branch'] ?: '—' ); ?></td></tr>
<tr><th>Oddział zwrotu</th><td><?php echo esc_html( $meta['return_branch'] ?: $meta['pickup_branch'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Oddział zwrotu', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['return_branch'] ?: $meta['pickup_branch'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr> <tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Imię</th><td><?php echo esc_html( $meta['first_name'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Imię', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['first_name'] ?: '—' ); ?></td></tr>
<tr><th>Nazwisko</th><td><?php echo esc_html( $meta['last_name'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Nazwisko', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['last_name'] ?: '—' ); ?></td></tr>
<tr><th>Email</th><td><?php echo esc_html( $meta['email'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Email', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['email'] ?: '—' ); ?></td></tr>
<tr><th>Telefon</th><td><?php echo esc_html( $meta['phone'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Telefon', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['phone'] ?: '—' ); ?></td></tr>
<tr><th>PESEL</th><td><?php echo esc_html( $meta['pesel'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'PESEL', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['pesel'] ?: '—' ); ?></td></tr>
<tr><th>Adres</th><td><?php echo esc_html( $address_str ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Adres', 'carei-reservation' ); ?></th><td><?php echo esc_html( $address_str ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr> <tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Opcje dodatkowe</th><td><?php echo esc_html( $extras_str ?: 'Brak' ); ?></td></tr> <tr><th><?php esc_html_e( 'Opcje dodatkowe', 'carei-reservation' ); ?></th><td><?php echo esc_html( $extras_str ?: __( 'Brak', 'carei-reservation' ) ); ?></td></tr>
<tr><th>Pakiet ochronny</th><td><?php echo esc_html( $protection_str ?: 'Brak' ); ?></td></tr> <tr><th><?php esc_html_e( 'Pakiet ochronny', 'carei-reservation' ); ?></th><td><?php echo esc_html( $protection_str ?: __( 'Brak', 'carei-reservation' ) ); ?></td></tr>
<tr><th>Wiadomość</th><td><?php echo esc_html( $meta['comments'] ?: '—' ); ?></td></tr> <tr><th><?php esc_html_e( 'Wiadomość', 'carei-reservation' ); ?></th><td><?php echo esc_html( $meta['comments'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr> <tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr> <tr>
<th>Status</th> <th><?php esc_html_e( 'Status', 'carei-reservation' ); ?></th>
<td> <td>
<select name="carei_status"> <select name="carei_status">
<?php foreach ( self::$statuses as $key => $info ) : ?> <?php foreach ( self::$statuses as $key => $info ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $meta['status'], $key ); ?>> <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $meta['status'], $key ); ?>>
<?php echo esc_html( $info['label'] ); ?> <?php echo esc_html( self::get_status_label( $key ) ); ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -513,7 +687,13 @@ class Carei_Admin_Panel {
$first_name = isset( $driver['firstName'] ) ? $driver['firstName'] : ''; $first_name = isset( $driver['firstName'] ) ? $driver['firstName'] : '';
$last_name = isset( $driver['lastName'] ) ? $driver['lastName'] : ''; $last_name = isset( $driver['lastName'] ) ? $driver['lastName'] : '';
$title = sprintf( 'Rezerwacja #%s — %s %s', $reservation_no ?: $reservation_id, $first_name, $last_name ); $title = sprintf(
/* translators: 1: reservation number or ID, 2: first name, 3: last name */
__( 'Rezerwacja #%1$s — %2$s %3$s', 'carei-reservation' ),
$reservation_no ?: $reservation_id,
$first_name,
$last_name
);
$post_id = wp_insert_post( array( $post_id = wp_insert_post( array(
'post_type' => self::POST_TYPE, 'post_type' => self::POST_TYPE,

View File

@@ -20,7 +20,7 @@ class Carei_Branches_Widget extends \Elementor\Widget_Base {
} }
public function get_title() { public function get_title() {
return 'Carei Branches'; return esc_html__( 'Carei Branches', 'carei-reservation' );
} }
public function get_icon() { public function get_icon() {
@@ -92,11 +92,11 @@ class Carei_Branches_Widget extends \Elementor\Widget_Base {
if ( $street ) { if ( $street ) {
$street_lower = mb_strtolower( $street, 'UTF-8' ); $street_lower = mb_strtolower( $street, 'UTF-8' );
$has_prefix = preg_match( '/^(ul\.|al\.|pl\.|os\.)/u', $street_lower ); $has_prefix = preg_match( '/^(ul\.|al\.|pl\.|os\.)/u', $street_lower );
$street = $has_prefix ? $street : 'ul. ' . $street; $street = $has_prefix ? $street : sprintf( /* translators: %s: street name */ __( 'ul. %s', 'carei-reservation' ), $street );
} }
$result[] = array( $result[] = array(
'name' => 'Oddział ' . $display_city, 'name' => sprintf( /* translators: %s: city name */ __( 'Oddział %s', 'carei-reservation' ), $display_city ),
'street' => $street, 'street' => $street,
'zipCity' => trim( $zip . ' ' . $api_city_tc ), 'zipCity' => trim( $zip . ' ' . $api_city_tc ),
); );

View File

@@ -13,7 +13,7 @@ class Carei_Cities_Widget extends \Elementor\Widget_Base {
} }
public function get_title() { public function get_title() {
return 'Carei Cities'; return esc_html__( 'Carei Cities', 'carei-reservation' );
} }
public function get_icon() { public function get_icon() {

View File

@@ -13,7 +13,7 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
} }
public function get_title() { public function get_title() {
return 'Carei Reservation'; return esc_html__( 'Carei Reservation', 'carei-reservation' );
} }
public function get_icon() { public function get_icon() {
@@ -34,14 +34,14 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
protected function register_controls() { protected function register_controls() {
$this->start_controls_section( 'content_section', array( $this->start_controls_section( 'content_section', array(
'label' => 'Przycisk rezerwacji', 'label' => esc_html__( 'Przycisk rezerwacji', 'carei-reservation' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT, 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
) ); ) );
$this->add_control( 'button_text', array( $this->add_control( 'button_text', array(
'label' => 'Tekst przycisku', 'label' => esc_html__( 'Tekst przycisku', 'carei-reservation' ),
'type' => \Elementor\Controls_Manager::TEXT, 'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'Złóż zapytanie o rezerwację', 'default' => esc_html__( 'Złóż zapytanie o rezerwację', 'carei-reservation' ),
) ); ) );
$this->end_controls_section(); $this->end_controls_section();
@@ -59,12 +59,12 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-modal-overlay" data-carei-modal role="dialog" aria-modal="true" aria-labelledby="carei-modal-title"> <div class="carei-modal-overlay" data-carei-modal role="dialog" aria-modal="true" aria-labelledby="carei-modal-title">
<div class="carei-modal"> <div class="carei-modal">
<div class="carei-modal__scroll"> <div class="carei-modal__scroll">
<span role="button" tabindex="0" class="carei-modal-close" data-carei-close-modal aria-label="Zamknij formularz"> <span role="button" tabindex="0" class="carei-modal-close" data-carei-close-modal aria-label="<?php echo esc_attr__( 'Zamknij formularz', 'carei-reservation' ); ?>">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/> <path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg> </svg>
</span> </span>
<h2 class="carei-modal-title" id="carei-modal-title">Wypełnij formularz rezerwacji<span>.</span></h2> <h2 class="carei-modal-title" id="carei-modal-title"><?php esc_html_e( 'Wypełnij formularz rezerwacji', 'carei-reservation' ); ?><span>.</span></h2>
<form class="carei-form" id="carei-reservation-form" novalidate> <form class="carei-form" id="carei-reservation-form" novalidate>
@@ -74,7 +74,7 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__field"> <div class="carei-form__field">
<div class="carei-form__select-wrap"> <div class="carei-form__select-wrap">
<select id="carei-segment" name="segment" required> <select id="carei-segment" name="segment" required>
<option value="" disabled selected>Wybierz segment pojazdu</option> <option value="" disabled selected><?php esc_html_e( 'Wybierz segment pojazdu', 'carei-reservation' ); ?></option>
</select> </select>
<svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg> <svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div> </div>
@@ -82,27 +82,27 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__field carei-form__field--date"> <div class="carei-form__field carei-form__field--date">
<div class="carei-form__date-wrap"> <div class="carei-form__date-wrap">
<svg class="carei-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg class="carei-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
<label class="carei-form__date-label" for="carei-date-from">Od kiedy?</label> <label class="carei-form__date-label" for="carei-date-from"><?php esc_html_e( 'Od kiedy?', 'carei-reservation' ); ?></label>
<input type="datetime-local" id="carei-date-from" name="dateFrom" class="carei-form__input carei-form__input--date" required> <input type="datetime-local" id="carei-date-from" name="dateFrom" class="carei-form__input carei-form__input--date" required>
</div> </div>
</div> </div>
<div class="carei-form__field carei-form__field--date"> <div class="carei-form__field carei-form__field--date">
<div class="carei-form__date-wrap"> <div class="carei-form__date-wrap">
<svg class="carei-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg class="carei-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
<label class="carei-form__date-label" for="carei-date-to">Do kiedy?</label> <label class="carei-form__date-label" for="carei-date-to"><?php esc_html_e( 'Do kiedy?', 'carei-reservation' ); ?></label>
<input type="datetime-local" id="carei-date-to" name="dateTo" class="carei-form__input carei-form__input--date" required> <input type="datetime-local" id="carei-date-to" name="dateTo" class="carei-form__input carei-form__input--date" required>
</div> </div>
</div> </div>
</div> </div>
<div class="carei-form__days-count" id="carei-days-count">Wybrano: <strong>0 dni</strong></div> <div class="carei-form__days-count" id="carei-days-count"><?php esc_html_e( 'Wybrano:', 'carei-reservation' ); ?> <strong><?php esc_html_e( '0 dni', 'carei-reservation' ); ?></strong></div>
<div class="carei-form__row carei-form__row--pickup"> <div class="carei-form__row carei-form__row--pickup">
<div class="carei-form__field"> <div class="carei-form__field">
<div class="carei-form__select-wrap carei-form__select-wrap--icon"> <div class="carei-form__select-wrap carei-form__select-wrap--icon">
<svg class="carei-form__icon-pin" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg> <svg class="carei-form__icon-pin" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg>
<select id="carei-pickup-branch" name="pickupBranch" required> <select id="carei-pickup-branch" name="pickupBranch" required>
<option value="" disabled selected>Miejsce odbioru</option> <option value="" disabled selected><?php esc_html_e( 'Miejsce odbioru', 'carei-reservation' ); ?></option>
</select> </select>
<svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg> <svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div> </div>
@@ -112,7 +112,7 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<span class="carei-form__checkbox-box"> <span class="carei-form__checkbox-box">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span> </span>
<span class="carei-form__checkbox-text">Zwrot w tej samej lokalizacji</span> <span class="carei-form__checkbox-text"><?php esc_html_e( 'Zwrot w tej samej lokalizacji', 'carei-reservation' ); ?></span>
</label> </label>
</div> </div>
@@ -120,7 +120,7 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__select-wrap carei-form__select-wrap--icon"> <div class="carei-form__select-wrap carei-form__select-wrap--icon">
<svg class="carei-form__icon-pin" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg> <svg class="carei-form__icon-pin" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg>
<select id="carei-return-branch" name="returnBranch"> <select id="carei-return-branch" name="returnBranch">
<option value="" disabled selected>Miejsce zwrotu</option> <option value="" disabled selected><?php esc_html_e( 'Miejsce zwrotu', 'carei-reservation' ); ?></option>
</select> </select>
<svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg> <svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div> </div>
@@ -130,18 +130,18 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<!-- Ubezpieczenie + Opcje dodatkowe (ukryte do wybrania segmentu i miejsca odbioru) --> <!-- Ubezpieczenie + Opcje dodatkowe (ukryte do wybrania segmentu i miejsca odbioru) -->
<div id="carei-extras-wrapper" class="carei-form__extras-wrapper" style="display:none;"> <div id="carei-extras-wrapper" class="carei-form__extras-wrapper" style="display:none;">
<div class="carei-form__divider"><span>Wyjazd zagraniczny</span></div> <div class="carei-form__divider"><span><?php esc_html_e( 'Wyjazd zagraniczny', 'carei-reservation' ); ?></span></div>
<div class="carei-form__section" id="carei-abroad-section" style="display:none;"> <div class="carei-form__section" id="carei-abroad-section" style="display:none;">
<label class="carei-form__checkbox-label carei-form__checkbox-label--abroad"> <label class="carei-form__checkbox-label carei-form__checkbox-label--abroad">
<input type="checkbox" id="carei-abroad-toggle" name="abroadToggle"> <input type="checkbox" id="carei-abroad-toggle" name="abroadToggle">
<span class="carei-form__checkbox-box"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span> <span class="carei-form__checkbox-box"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span>Wyjazd poza granicę Polski do:</span> <span><?php esc_html_e( 'Wyjazd poza granicę Polski do:', 'carei-reservation' ); ?></span>
</label> </label>
<div id="carei-abroad-search" class="carei-abroad" style="display:none;"> <div id="carei-abroad-search" class="carei-abroad" style="display:none;">
<div class="carei-abroad__input-wrap"> <div class="carei-abroad__input-wrap">
<svg class="carei-abroad__plus-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> <svg class="carei-abroad__plus-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input type="text" id="carei-abroad-input" class="carei-abroad__input" placeholder="Wyszukaj i dodaj kraj na trasie"> <input type="text" id="carei-abroad-input" class="carei-abroad__input" placeholder="<?php echo esc_attr__( 'Wyszukaj i dodaj kraj na trasie', 'carei-reservation' ); ?>">
<span role="button" tabindex="0" class="carei-abroad__clear" id="carei-abroad-clear" title="Wyczyść"> <span role="button" tabindex="0" class="carei-abroad__clear" id="carei-abroad-clear" title="<?php echo esc_attr__( 'Wyczyść', 'carei-reservation' ); ?>">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg> <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span> </span>
</div> </div>
@@ -150,18 +150,14 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
</div> </div>
</div> </div>
<div class="carei-form__divider"><span>Pakiety ochronne</span></div> <div class="carei-form__divider"><span><?php esc_html_e( 'Pakiety ochronne', 'carei-reservation' ); ?></span></div>
<div class="carei-form__section"> <div class="carei-form__section">
<div class="carei-form__row carei-form__row--protection-packages" id="carei-protection-packages-container"> <div class="carei-form__row carei-form__row--protection-packages" id="carei-protection-packages-container">
<!-- Dynamicznie z panelu WP (SOFT, PREMIUM) --> <!-- Dynamicznie z panelu WP (SOFT, PREMIUM) -->
</div> </div>
<div class="carei-form__protection-divider" aria-hidden="true"></div>
<div class="carei-form__row" id="carei-insurance-container">
<!-- Dynamicznie z API pricelist -->
</div>
</div> </div>
<div class="carei-form__divider"><span>Opcje dodatkowe</span></div> <div class="carei-form__divider"><span><?php esc_html_e( 'Opcje dodatkowe', 'carei-reservation' ); ?></span></div>
<div class="carei-form__section"> <div class="carei-form__section">
<div class="carei-form__row" id="carei-extras-container"> <div class="carei-form__row" id="carei-extras-container">
<!-- Dynamicznie z API pricelist --> <!-- Dynamicznie z API pricelist -->
@@ -171,20 +167,20 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
</div> </div>
<!-- Dane najemcy --> <!-- Dane najemcy -->
<div class="carei-form__divider"><span>Dane najemcy</span></div> <div class="carei-form__divider"><span><?php esc_html_e( 'Dane najemcy', 'carei-reservation' ); ?></span></div>
<div class="carei-form__section"> <div class="carei-form__section">
<div class="carei-form__row"> <div class="carei-form__row">
<div class="carei-form__field"> <div class="carei-form__field">
<div class="carei-form__float-wrap"> <div class="carei-form__float-wrap">
<input type="text" id="carei-firstname" name="firstName" class="carei-form__input carei-form__input--float" placeholder=" " required> <input type="text" id="carei-firstname" name="firstName" class="carei-form__input carei-form__input--float" placeholder=" " required>
<label class="carei-form__float-label" for="carei-firstname">Imię</label> <label class="carei-form__float-label" for="carei-firstname"><?php esc_html_e( 'Imię', 'carei-reservation' ); ?></label>
</div> </div>
</div> </div>
<div class="carei-form__field"> <div class="carei-form__field">
<div class="carei-form__float-wrap"> <div class="carei-form__float-wrap">
<input type="text" id="carei-lastname" name="lastName" class="carei-form__input carei-form__input--float" placeholder=" " required> <input type="text" id="carei-lastname" name="lastName" class="carei-form__input carei-form__input--float" placeholder=" " required>
<label class="carei-form__float-label" for="carei-lastname">Nazwisko</label> <label class="carei-form__float-label" for="carei-lastname"><?php esc_html_e( 'Nazwisko', 'carei-reservation' ); ?></label>
</div> </div>
</div> </div>
</div> </div>
@@ -193,12 +189,12 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__field"> <div class="carei-form__field">
<div class="carei-form__float-wrap"> <div class="carei-form__float-wrap">
<input type="email" id="carei-email" name="email" class="carei-form__input carei-form__input--float" placeholder=" " required> <input type="email" id="carei-email" name="email" class="carei-form__input carei-form__input--float" placeholder=" " required>
<label class="carei-form__float-label" for="carei-email">Adres e-mail</label> <label class="carei-form__float-label" for="carei-email"><?php esc_html_e( 'Adres e-mail', 'carei-reservation' ); ?></label>
</div> </div>
</div> </div>
<div class="carei-form__field"> <div class="carei-form__field">
<div class="carei-form__phone-wrap"> <div class="carei-form__phone-wrap">
<label class="carei-form__phone-label" for="carei-phone">Nr telefonu</label> <label class="carei-form__phone-label" for="carei-phone"><?php esc_html_e( 'Nr telefonu', 'carei-reservation' ); ?></label>
<div class="carei-form__phone-row"> <div class="carei-form__phone-row">
<div class="carei-form__phone-prefix"> <div class="carei-form__phone-prefix">
<span class="carei-form__phone-flag">🇵🇱</span> <span class="carei-form__phone-flag">🇵🇱</span>
@@ -212,7 +208,7 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__field carei-form__field--full"> <div class="carei-form__field carei-form__field--full">
<textarea id="carei-message" name="message" class="carei-form__textarea" placeholder="Twoja wiadomość dotycząca rezerwacji" rows="4"></textarea> <textarea id="carei-message" name="message" class="carei-form__textarea" placeholder="<?php echo esc_attr__( 'Twoja wiadomość dotycząca rezerwacji', 'carei-reservation' ); ?>" rows="4"></textarea>
</div> </div>
</div> </div>
@@ -223,23 +219,23 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<span class="carei-form__checkbox-box"> <span class="carei-form__checkbox-box">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span> </span>
<span class="carei-form__checkbox-text">Zgadzam się na <a href="/polityka-prywatnosci/" target="_blank">Politykę Prywatności</a></span> <span class="carei-form__checkbox-text"><?php echo wp_kses( __( 'Zgadzam się na <a href="/polityka-prywatnosci/" target="_blank">Politykę Prywatności</a>', 'carei-reservation' ), array( 'a' => array( 'href' => array(), 'target' => array() ) ) ); ?></span>
</label> </label>
<button type="submit" class="carei-form__submit" aria-busy="false"> <button type="submit" class="carei-form__submit" aria-busy="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
Wyślij <?php esc_html_e( 'Wyślij', 'carei-reservation' ); ?>
</button> </button>
</div> </div>
<div class="carei-form__error-summary" id="carei-error-summary" style="display:none;"> <div class="carei-form__error-summary" id="carei-error-summary" style="display:none;">
Uzupełnij wymagane pola zaznaczone na czerwono. <?php esc_html_e( 'Uzupełnij wymagane pola zaznaczone na czerwono.', 'carei-reservation' ); ?>
</div> </div>
</form> </form>
<!-- Podsumowanie kosztów (po submit) --> <!-- Podsumowanie kosztów (po submit) -->
<div id="carei-summary-overlay" class="carei-summary" style="display:none;"> <div id="carei-summary-overlay" class="carei-summary" style="display:none;">
<h3 class="carei-summary__title" tabindex="-1">Podsumowanie rezerwacji<span>.</span></h3> <h3 class="carei-summary__title" tabindex="-1"><?php esc_html_e( 'Podsumowanie rezerwacji', 'carei-reservation' ); ?><span>.</span></h3>
<div class="carei-summary__details" id="carei-summary-details"></div> <div class="carei-summary__details" id="carei-summary-details"></div>
<div class="carei-summary__table" id="carei-summary-table"></div> <div class="carei-summary__table" id="carei-summary-table"></div>
<div class="carei-summary__total" id="carei-summary-total"></div> <div class="carei-summary__total" id="carei-summary-total"></div>
@@ -247,11 +243,11 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-summary__actions"> <div class="carei-summary__actions">
<button type="button" class="carei-summary__btn carei-summary__btn--back" id="carei-summary-back"> <button type="button" class="carei-summary__btn carei-summary__btn--back" id="carei-summary-back">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Wróć do formularza <?php esc_html_e( 'Wróć do formularza', 'carei-reservation' ); ?>
</button> </button>
<button type="button" class="carei-summary__btn carei-summary__btn--confirm" id="carei-summary-confirm" aria-busy="false"> <button type="button" class="carei-summary__btn carei-summary__btn--confirm" id="carei-summary-confirm" aria-busy="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
Potwierdź rezerwację <?php esc_html_e( 'Potwierdź rezerwację', 'carei-reservation' ); ?>
</button> </button>
</div> </div>
</div> </div>
@@ -261,10 +257,10 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-success__icon"> <div class="carei-success__icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"><path d="M20 6L9 17l-5-5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="40" height="40" viewBox="0 0 24 24" fill="none"><path d="M20 6L9 17l-5-5" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div> </div>
<h3 class="carei-success__title" tabindex="-1">Zamówienie złożone!</h3> <h3 class="carei-success__title" tabindex="-1"><?php esc_html_e( 'Zamówienie złożone!', 'carei-reservation' ); ?></h3>
<p class="carei-success__number" id="carei-success-number"></p> <p class="carei-success__number" id="carei-success-number"></p>
<p class="carei-success__message">Oczekuj na kontakt z wypożyczalnią</p> <p class="carei-success__message"><?php esc_html_e( 'Oczekuj na kontakt z wypożyczalnią', 'carei-reservation' ); ?></p>
<button type="button" class="carei-success__close" id="carei-success-close">Zamknij</button> <button type="button" class="carei-success__close" id="carei-success-close"><?php esc_html_e( 'Zamknij', 'carei-reservation' ); ?></button>
</div> </div>
</div><!-- .carei-modal__scroll --> </div><!-- .carei-modal__scroll -->

View File

@@ -63,7 +63,7 @@ class Carei_Map_Widget extends \Elementor\Widget_Base {
} }
public function get_title() { public function get_title() {
return 'Carei Map'; return esc_html__( 'Carei Map', 'carei-reservation' );
} }
public function get_icon() { public function get_icon() {
@@ -169,11 +169,11 @@ class Carei_Map_Widget extends \Elementor\Widget_Base {
$api_city = isset( $b['city'] ) ? trim( $b['city'] ) : ''; $api_city = isset( $b['city'] ) ? trim( $b['city'] ) : '';
$api_city_title = mb_convert_case( $api_city, MB_CASE_TITLE, 'UTF-8' ); $api_city_title = mb_convert_case( $api_city, MB_CASE_TITLE, 'UTF-8' );
$address_lines = array( 'Oddział ' . $display_city ); $address_lines = array( sprintf( /* translators: %s: city name */ __( 'Oddział %s', 'carei-reservation' ), $display_city ) );
if ( $street ) { if ( $street ) {
$street_lower = mb_strtolower( $street, 'UTF-8' ); $street_lower = mb_strtolower( $street, 'UTF-8' );
$has_prefix = preg_match( '/^(ul\.|al\.|pl\.|os\.)/u', $street_lower ); $has_prefix = preg_match( '/^(ul\.|al\.|pl\.|os\.)/u', $street_lower );
$address_lines[] = $has_prefix ? $street : 'ul. ' . $street; $address_lines[] = $has_prefix ? $street : sprintf( /* translators: %s: street name */ __( 'ul. %s', 'carei-reservation' ), $street );
} }
$zip_city = trim( $zip . ' ' . $api_city_title ); $zip_city = trim( $zip . ' ' . $api_city_title );
if ( $zip_city ) { if ( $zip_city ) {

View File

@@ -145,7 +145,7 @@ class Carei_REST_Proxy {
public function check_nonce( WP_REST_Request $request ) { public function check_nonce( WP_REST_Request $request ) {
$nonce = $request->get_header( 'X-WP-Nonce' ); $nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error( 'rest_forbidden', 'Invalid nonce.', array( 'status' => 403 ) ); return new WP_Error( 'rest_forbidden', __( 'Invalid nonce.', 'carei-reservation' ), array( 'status' => 403 ) );
} }
return true; return true;
} }
@@ -156,7 +156,7 @@ class Carei_REST_Proxy {
private function api() { private function api() {
$api = Carei_Softra_API::get_instance(); $api = Carei_Softra_API::get_instance();
if ( null === $api ) { if ( null === $api ) {
return new WP_Error( 'carei_not_configured', 'Softra API not configured.', array( 'status' => 500 ) ); return new WP_Error( 'carei_not_configured', __( 'Softra API not configured.', 'carei-reservation' ), array( 'status' => 500 ) );
} }
return $api; return $api;
} }
@@ -227,12 +227,34 @@ class Carei_REST_Proxy {
if ( is_wp_error( $api ) ) { if ( is_wp_error( $api ) ) {
return $api; return $api;
} }
return $this->respond( $api->get_pricelist( $pricelists = $api->get_pricelist(
$request->get_param( 'category' ), $request->get_param( 'category' ),
$request->get_param( 'dateFrom' ), $request->get_param( 'dateFrom' ),
$request->get_param( 'dateTo' ), $request->get_param( 'dateTo' ),
$request->get_param( 'pickUpLocation' ) $request->get_param( 'pickUpLocation' )
) ); );
// Auto-collect PL extra names + per-locale translate (Phase 19).
if ( is_array( $pricelists ) ) {
$locale = $this->resolve_locale( $request );
$translations = Carei_Admin_Panel::get_extras_translations();
foreach ( $pricelists as &$pricelist ) {
if ( ! is_array( $pricelist ) || empty( $pricelist['additionalItems'] ) || ! is_array( $pricelist['additionalItems'] ) ) continue;
foreach ( $pricelist['additionalItems'] as &$item ) {
if ( ! is_array( $item ) || ! isset( $item['name'] ) || ! is_string( $item['name'] ) ) continue;
$pl_name = trim( $item['name'] );
if ( $pl_name === '' ) continue;
Carei_Admin_Panel::remember_extra_name( $pl_name );
if ( $locale === 'en' && isset( $translations[ $pl_name ] ) && $translations[ $pl_name ] !== '' ) {
$item['name'] = $translations[ $pl_name ];
}
}
unset( $item );
}
unset( $pricelist );
}
return $this->respond( $pricelists );
} }
public function get_pricing_summary( WP_REST_Request $request ) { public function get_pricing_summary( WP_REST_Request $request ) {
@@ -308,17 +330,35 @@ class Carei_REST_Proxy {
public function get_protection_packages( WP_REST_Request $request ) { public function get_protection_packages( WP_REST_Request $request ) {
$all = Carei_Admin_Panel::get_protection_packages(); $all = Carei_Admin_Panel::get_protection_packages();
$locale = $this->resolve_locale( $request );
$out = array( 'soft' => null, 'premium' => null ); $out = array( 'soft' => null, 'premium' => null );
foreach ( array( 'soft', 'premium' ) as $key ) { foreach ( array( 'soft', 'premium' ) as $key ) {
if ( isset( $all[ $key ] ) && ! empty( $all[ $key ]['active'] ) ) { if ( isset( $all[ $key ] ) && ! empty( $all[ $key ]['active'] ) ) {
$pkg = $all[ $key ];
if ( 'en' === $locale ) {
$name = ! empty( $pkg['name_en'] ) ? $pkg['name_en'] : $pkg['name'];
$desc = ! empty( $pkg['description_en'] ) ? $pkg['description_en'] : $pkg['description'];
} else {
$name = $pkg['name'];
$desc = $pkg['description'];
}
$out[ $key ] = array( $out[ $key ] = array(
'key' => $key, 'key' => $key,
'name' => $all[ $key ]['name'], 'name' => $name,
'pricePerDay' => (float) $all[ $key ]['pricePerDay'], 'pricePerDay' => (float) $pkg['pricePerDay'],
'description' => $all[ $key ]['description'], 'description' => $desc,
); );
} }
} }
return rest_ensure_response( $out ); return rest_ensure_response( $out );
} }
private function resolve_locale( WP_REST_Request $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( (string) $locale, 'en' ) ) ? 'en' : 'pl';
}
} }

View File

@@ -13,7 +13,7 @@ class Carei_Search_Widget extends \Elementor\Widget_Base {
} }
public function get_title() { public function get_title() {
return 'Carei Search Form'; return esc_html__( 'Carei Search Form', 'carei-reservation' );
} }
public function get_icon() { public function get_icon() {
@@ -37,14 +37,14 @@ class Carei_Search_Widget extends \Elementor\Widget_Base {
protected function render() { protected function render() {
?> ?>
<div class="carei-search-form"> <div class="carei-search-form">
<h2 class="carei-search-form__title">Wypełnij formularz rezerwacji<span>.</span></h2> <h2 class="carei-search-form__title"><?php esc_html_e( 'Wypełnij formularz rezerwacji', 'carei-reservation' ); ?><span>.</span></h2>
<div class="carei-search-form__fields"> <div class="carei-search-form__fields">
<!-- Segment --> <!-- Segment -->
<div class="carei-search-form__field carei-search-form__field--full"> <div class="carei-search-form__field carei-search-form__field--full">
<div class="carei-search-form__select-wrap"> <div class="carei-search-form__select-wrap">
<select id="carei-search-segment"> <select id="carei-search-segment">
<option value="" disabled selected>Wybierz segment</option> <option value="" disabled selected><?php esc_html_e( 'Wybierz segment', 'carei-reservation' ); ?></option>
</select> </select>
<svg class="carei-search-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg> <svg class="carei-search-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div> </div>
@@ -55,14 +55,14 @@ class Carei_Search_Widget extends \Elementor\Widget_Base {
<div class="carei-search-form__field"> <div class="carei-search-form__field">
<div class="carei-search-form__date-wrap"> <div class="carei-search-form__date-wrap">
<svg class="carei-search-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg class="carei-search-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
<label class="carei-search-form__date-label" for="carei-search-date-from">Od kiedy?</label> <label class="carei-search-form__date-label" for="carei-search-date-from"><?php esc_html_e( 'Od kiedy?', 'carei-reservation' ); ?></label>
<input type="datetime-local" id="carei-search-date-from" class="carei-search-form__input carei-search-form__input--date"> <input type="datetime-local" id="carei-search-date-from" class="carei-search-form__input carei-search-form__input--date">
</div> </div>
</div> </div>
<div class="carei-search-form__field"> <div class="carei-search-form__field">
<div class="carei-search-form__date-wrap"> <div class="carei-search-form__date-wrap">
<svg class="carei-search-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg class="carei-search-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
<label class="carei-search-form__date-label" for="carei-search-date-to">Do kiedy?</label> <label class="carei-search-form__date-label" for="carei-search-date-to"><?php esc_html_e( 'Do kiedy?', 'carei-reservation' ); ?></label>
<input type="datetime-local" id="carei-search-date-to" class="carei-search-form__input carei-search-form__input--date"> <input type="datetime-local" id="carei-search-date-to" class="carei-search-form__input carei-search-form__input--date">
</div> </div>
</div> </div>
@@ -73,7 +73,7 @@ class Carei_Search_Widget extends \Elementor\Widget_Base {
<div class="carei-search-form__select-wrap carei-search-form__select-wrap--icon"> <div class="carei-search-form__select-wrap carei-search-form__select-wrap--icon">
<svg class="carei-search-form__pin-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg> <svg class="carei-search-form__pin-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg>
<select id="carei-search-pickup"> <select id="carei-search-pickup">
<option value="" disabled selected>Miejsce odbioru</option> <option value="" disabled selected><?php esc_html_e( 'Miejsce odbioru', 'carei-reservation' ); ?></option>
</select> </select>
<svg class="carei-search-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg> <svg class="carei-search-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div> </div>
@@ -86,7 +86,7 @@ class Carei_Search_Widget extends \Elementor\Widget_Base {
<span class="carei-search-form__checkbox-box"> <span class="carei-search-form__checkbox-box">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span> </span>
<span class="carei-search-form__checkbox-text">Zwrot w tej samej lokalizacji</span> <span class="carei-search-form__checkbox-text"><?php esc_html_e( 'Zwrot w tej samej lokalizacji', 'carei-reservation' ); ?></span>
</label> </label>
</div> </div>
</div> </div>
@@ -94,7 +94,7 @@ class Carei_Search_Widget extends \Elementor\Widget_Base {
<!-- Przycisk --> <!-- Przycisk -->
<button type="button" class="carei-search-form__submit" id="carei-search-submit"> <button type="button" class="carei-search-form__submit" id="carei-search-submit">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
Złóż zapytanie o rezerwację <?php esc_html_e( 'Złóż zapytanie o rezerwację', 'carei-reservation' ); ?>
</button> </button>
</div> </div>
<?php <?php

View File

@@ -137,9 +137,12 @@ class Carei_Softra_API {
} }
if ( $res['status'] < 200 || $res['status'] >= 300 ) { if ( $res['status'] < 200 || $res['status'] >= 300 ) {
$raw_msg = self::extract_softra_message( $res['body'] );
$mapped = self::map_error_message( $raw_msg );
$message = $mapped !== '' ? $mapped : sprintf( 'Softra API error: HTTP %d', $res['status'] );
return new WP_Error( return new WP_Error(
'carei_api_error', 'carei_api_error',
'Softra API error: HTTP ' . $res['status'], $message,
array( 'status' => $res['status'], 'body' => $res['body'] ) array( 'status' => $res['status'], 'body' => $res['body'] )
); );
} }
@@ -147,6 +150,69 @@ class Carei_Softra_API {
return $res['body']; return $res['body'];
} }
/**
* Extract human-readable message from Softra response body.
* Softra returns either JSON with `message` / `error` / `details` field, or raw string.
*/
public static function extract_softra_message( $body ) {
if ( is_array( $body ) ) {
foreach ( array( 'message', 'error', 'details', 'description' ) as $field ) {
if ( ! empty( $body[ $field ] ) && is_string( $body[ $field ] ) ) {
return trim( $body[ $field ] );
}
}
return '';
}
if ( is_string( $body ) && $body !== '' ) {
$decoded = json_decode( $body, true );
if ( is_array( $decoded ) ) {
return self::extract_softra_message( $decoded );
}
return trim( $body );
}
return '';
}
/**
* Map typical Softra PL error messages to localized strings.
* Exact match first, then fuzzy prefix match. Unknown messages passthrough.
*/
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' ),
);
$trimmed = trim( $original_message );
if ( isset( $dict[ $trimmed ] ) ) {
return $dict[ $trimmed ];
}
foreach ( $dict as $pl_key => $translated ) {
if ( 0 === stripos( $trimmed, $pl_key ) ) {
return $translated;
}
}
return $original_message;
}
// ─── Public API Methods ─────────────────────────────────────── // ─── Public API Methods ───────────────────────────────────────
public function get_branches() { public function get_branches() {

View File

@@ -0,0 +1,659 @@
# 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.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_GB\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"
#: carei-reservation.php:133
msgid "Błąd API: HTTP %status%"
msgstr "API error: HTTP %status%"
#: carei-reservation.php:134
msgid "Przekroczono czas oczekiwania. Spróbuj ponownie."
msgstr "Request timed out. Please try again."
#: carei-reservation.php:135
msgid "Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie."
msgstr "No server connection. Check your internet and try again."
#: carei-reservation.php:136
msgid "Błąd ładowania"
msgstr "Loading error"
#: carei-reservation.php:139
msgid "Wybierz..."
msgstr "Select..."
#: carei-reservation.php:140 includes/class-elementor-widget.php:77
msgid "Wybierz segment pojazdu"
msgstr "Select a vehicle segment"
#: carei-reservation.php:141 includes/class-search-widget.php:47
msgid "Wybierz segment"
msgstr "Select segment"
#: carei-reservation.php:142
msgid "Wybierz miejsce odbioru"
msgstr "Select pickup location"
#: carei-reservation.php:143
msgid "Wybierz miejsce zwrotu"
msgstr "Select return location"
#: carei-reservation.php:144
msgid "Segment %name%"
msgstr "Segment %name%"
#: carei-reservation.php:145
msgid "Brak segmentów"
msgstr "No segments"
#: carei-reservation.php:146
msgid "Najpierw wybierz segment"
msgstr "Select segment first"
#: carei-reservation.php:147 carei-reservation.php:209
#: includes/class-elementor-widget.php:105
#: includes/class-search-widget.php:76
msgid "Miejsce odbioru"
msgstr "Pickup location"
#: carei-reservation.php:148 carei-reservation.php:210
#: includes/class-elementor-widget.php:123
msgid "Miejsce zwrotu"
msgstr "Return location"
#: carei-reservation.php:149
msgid "Brak lokalizacji dla tego segmentu"
msgstr "No locations for this segment"
#: carei-reservation.php:150
msgid "Brak lokalizacji"
msgstr "No locations"
#: carei-reservation.php:153
msgid "Rozpoczęcie"
msgstr "Start"
#: carei-reservation.php:154
msgid "Zakończenie"
msgstr "End"
#: carei-reservation.php:155
msgid "%label% — data lub godzina już minęły"
msgstr "%label% — date or time has already passed"
#: carei-reservation.php:156
msgid "Data lub godzina rozpoczęcia już minęły"
msgstr "Start date or time has already passed"
#: carei-reservation.php:157
msgid "Data lub godzina zakończenia już minęły"
msgstr "End date or time has already passed"
#: carei-reservation.php:158
msgid "Data zakończenia musi być po dacie rozpoczęcia"
msgstr "End date must be after start date"
#: carei-reservation.php:159
msgid "Podaj datę rozpoczęcia"
msgstr "Enter start date"
#: carei-reservation.php:160
msgid "Podaj datę zakończenia"
msgstr "Enter end date"
#: carei-reservation.php:163
msgid "Wybrano: <strong>%count% %unit%</strong>"
msgstr "Selected: <strong>%count% %unit%</strong>"
#: carei-reservation.php:164
msgid "dzień"
msgstr "day"
#: carei-reservation.php:165
msgid "dni"
msgstr "days"
#: carei-reservation.php:166
msgid "doba"
msgstr "day"
#: carei-reservation.php:167
msgid "doby"
msgstr "days"
#: carei-reservation.php:168
msgid "dób"
msgstr "days"
#: carei-reservation.php:171
msgid "%price% zł/doba"
msgstr "%price% PLN/day"
#: carei-reservation.php:172 carei-reservation.php:173
msgid "%price% zł"
msgstr "%price% PLN"
#: carei-reservation.php:174
msgid "od %min% do %max% zł"
msgstr "from %min% to %max% PLN"
#: carei-reservation.php:175
msgid "%perDay% zł/doba × %days% = %total% zł"
msgstr "%perDay% PLN/day × %days% = %total% PLN"
#: carei-reservation.php:176
msgid "Gratis"
msgstr "Free"
#: carei-reservation.php:179
msgid "Usuń"
msgstr "Remove"
#: carei-reservation.php:180
msgid "Dodaj"
msgstr "Add"
#: carei-reservation.php:183
msgid "Podaj imię"
msgstr "Enter first name"
#: carei-reservation.php:184
msgid "Podaj nazwisko"
msgstr "Enter last name"
#: carei-reservation.php:185
msgid "Podaj poprawny adres e-mail"
msgstr "Enter a valid email address"
#: carei-reservation.php:186
msgid "Podaj numer telefonu (min. 9 cyfr)"
msgstr "Enter phone number (min. 9 digits)"
#: carei-reservation.php:187
msgid "Wymagana zgoda na Politykę Prywatności"
msgstr "Privacy policy consent is required"
#: carei-reservation.php:190 includes/class-elementor-widget.php:226
msgid "Wyślij"
msgstr "Send"
#: carei-reservation.php:191
msgid "Przetwarzanie..."
msgstr "Processing..."
#: carei-reservation.php:192
msgid "Rezerwuję..."
msgstr "Reserving..."
#: carei-reservation.php:193 includes/class-elementor-widget.php:250
msgid "Potwierdź rezerwację"
msgstr "Confirm reservation"
#: carei-reservation.php:196
msgid "Ładowanie podsumowania..."
msgstr "Loading summary..."
#: carei-reservation.php:197
msgid "Nie udało się utworzyć klienta"
msgstr "Failed to create customer"
#: carei-reservation.php:198
msgid "Błąd tworzenia klienta: %msg%"
msgstr "Customer creation error: %msg%"
#: carei-reservation.php:199
msgid "Błąd pobierania podsumowania: %msg%"
msgstr "Error loading summary: %msg%"
#: carei-reservation.php:200
msgid "Rezerwacja nie powiodła się"
msgstr "Reservation failed"
#: carei-reservation.php:203
msgid "Pakiet ochronny: %name% — %perDay% zł/doba × %days% = %total% zł (do doliczenia poza systemem)"
msgstr "Protection package: %name% — %perDay% PLN/day × %days% = %total% PLN (to be paid separately)"
#: carei-reservation.php:206 includes/class-admin-panel.php:219
#: includes/class-admin-panel.php:399
msgid "Segment"
msgstr "Segment"
#: carei-reservation.php:207
msgid "Od"
msgstr "From"
#: carei-reservation.php:208
msgid "Do"
msgstr "To"
#: carei-reservation.php:211
msgid "Najemca"
msgstr "Renter"
#: carei-reservation.php:212 includes/class-admin-panel.php:407
msgid "Email"
msgstr "Email"
#: carei-reservation.php:213 includes/class-admin-panel.php:408
msgid "Telefon"
msgstr "Phone"
#: carei-reservation.php:214
msgid "Wybrane opcje"
msgstr "Selected options"
#: carei-reservation.php:215 includes/class-admin-panel.php:414
msgid "Wiadomość"
msgstr "Message"
#: carei-reservation.php:216
msgid "auto"
msgstr "car"
#: carei-reservation.php:217
msgid "do doliczenia"
msgstr "to be added"
#: carei-reservation.php:218
msgid "VAT"
msgstr "VAT"
#: carei-reservation.php:219 includes/class-admin-panel.php:413
msgid "Pakiet ochronny"
msgstr "Protection package"
#: carei-reservation.php:220
msgid "Do zapłaty"
msgstr "Total due"
#: carei-reservation.php:223
msgid "Nazwa"
msgstr "Name"
#: carei-reservation.php:224
msgid "Ilość"
msgstr "Quantity"
#: carei-reservation.php:225
msgid "Netto"
msgstr "Net"
#: carei-reservation.php:226
msgid "Brutto"
msgstr "Gross"
#: carei-reservation.php:229
msgid "Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment."
msgstr "No vehicle available for the selected dates. Change dates or segment."
#: carei-reservation.php:230
msgid "Nieprawidłowy zakres dat"
msgstr "Invalid date range"
#: carei-reservation.php:231
msgid "Nie znaleziono oddziału"
msgstr "Location not found"
#: carei-reservation.php:232
msgid "Klient o tych danych już istnieje w systemie"
msgstr "A customer with these details already exists in the system"
#: carei-reservation.php:233
msgid "Nieprawidłowy numer PESEL"
msgstr "Invalid PESEL number"
#: carei-reservation.php:234
msgid "Cennik wygasł. Odśwież formularz i spróbuj ponownie."
msgstr "Price list expired. Refresh the form and try again."
#: carei-reservation.php:237
msgid "Nr zamówienia: %no%"
msgstr "Order number: %no%"
#: carei-reservation.php:238
msgid "Rezerwacja potwierdzona"
msgstr "Reservation confirmed"
#: carei-reservation.php:239 includes/class-elementor-widget.php:238
msgid "Podsumowanie rezerwacji"
msgstr "Reservation summary"
#: carei-reservation.php:74
msgid "Brak konfiguracji API w pliku .env (url, username, password)."
msgstr "Missing API configuration in .env file (url, username, password)."
#: includes/class-admin-panel.php:103
msgid "Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji."
msgstr "Configuration of packages displayed in the <strong>Protection packages</strong> section of the reservation form. Price is per day — total = price × number of rental days."
#: includes/class-admin-panel.php:105
msgid "Zapisano."
msgstr "Saved."
#: includes/class-admin-panel.php:115
msgid "Pakiet %s"
msgstr "%s Package"
#: includes/class-admin-panel.php:118
msgid "Nazwa wyświetlana"
msgstr "Display name"
#: includes/class-admin-panel.php:122
msgid "Cena za dobę (zł)"
msgstr "Price per day (PLN)"
#: includes/class-admin-panel.php:126 includes/class-admin-panel.php:222
#: includes/class-admin-panel.php:417
msgid "Status"
msgstr "Status"
#: includes/class-admin-panel.php:127
msgid "Aktywny (widoczny w modalu)"
msgstr "Active (visible in modal)"
#: includes/class-admin-panel.php:130
msgid "Opis / zakres usług"
msgstr "Description / service scope"
#: includes/class-admin-panel.php:137
msgid "Zapisz pakiety"
msgstr "Save packages"
#: includes/class-admin-panel.php:152
msgid "Nieprawidłowy token."
msgstr "Invalid token."
#: includes/class-admin-panel.php:189 includes/class-admin-panel.php:191
msgid "Rezerwacje"
msgstr "Reservations"
#: includes/class-admin-panel.php:190
msgid "Rezerwacja"
msgstr "Reservation"
#: includes/class-admin-panel.php:192
msgid "Wszystkie rezerwacje"
msgstr "All reservations"
#: includes/class-admin-panel.php:193
msgid "Zobacz rezerwację"
msgstr "View reservation"
#: includes/class-admin-panel.php:194 includes/class-admin-panel.php:321
msgid "Szczegóły rezerwacji"
msgstr "Reservation details"
#: includes/class-admin-panel.php:195
msgid "Szukaj rezerwacji"
msgstr "Search reservations"
#: includes/class-admin-panel.php:196
msgid "Nie znaleziono rezerwacji"
msgstr "No reservations found"
#: includes/class-admin-panel.php:197
msgid "Brak rezerwacji w koszu"
msgstr "No reservations in trash"
#: includes/class-admin-panel.php:217 includes/class-admin-panel.php:395
msgid "Nr rezerwacji"
msgstr "Reservation No."
#: includes/class-admin-panel.php:218
msgid "Klient"
msgstr "Customer"
#: includes/class-admin-panel.php:22
msgid "Nowe"
msgstr "New"
#: includes/class-admin-panel.php:220
msgid "Daty"
msgstr "Dates"
#: includes/class-admin-panel.php:221
msgid "Oddział"
msgstr "Location"
#: includes/class-admin-panel.php:223
msgid "Data"
msgstr "Date"
#: includes/class-admin-panel.php:24
msgid "Przeczytane"
msgstr "Read"
#: includes/class-admin-panel.php:26
msgid "Zrealizowane"
msgstr "Completed"
#: includes/class-admin-panel.php:279
msgid "Wszystkie statusy"
msgstr "All statuses"
#: includes/class-admin-panel.php:362
msgid "%1$s — %2$s zł/doba × %3$d = %4$s zł"
msgstr "%1$s — %2$s PLN/day × %3$d = %4$s PLN"
#: includes/class-admin-panel.php:396
msgid "ID rezerwacji (Softra)"
msgstr "Reservation ID (Softra)"
#: includes/class-admin-panel.php:397
msgid "ID klienta (Softra)"
msgstr "Customer ID (Softra)"
#: includes/class-admin-panel.php:400
msgid "Data od"
msgstr "Date from"
#: includes/class-admin-panel.php:401
msgid "Data do"
msgstr "Date to"
#: includes/class-admin-panel.php:402
msgid "Oddział odbioru"
msgstr "Pickup location"
#: includes/class-admin-panel.php:403
msgid "Oddział zwrotu"
msgstr "Return location"
#: includes/class-admin-panel.php:409
msgid "PESEL"
msgstr "PESEL"
#: includes/class-admin-panel.php:410
msgid "Adres"
msgstr "Address"
#: includes/class-admin-panel.php:412 includes/class-admin-panel.php:413
msgid "Brak"
msgstr "None"
#: includes/class-admin-panel.php:50
msgid "Ubezpieczenie SOFT"
msgstr "SOFT Protection"
#: includes/class-admin-panel.php:537
msgid "Rezerwacja #%1$s — %2$s %3$s"
msgstr "Reservation #%1$s — %2$s %3$s"
#: includes/class-admin-panel.php:56
msgid "Ubezpieczenie PREMIUM"
msgstr "PREMIUM Protection"
#: includes/class-admin-panel.php:149 includes/class-admin-panel.php:96
msgid "Brak uprawnień."
msgstr "No permissions."
#: includes/class-branches-widget.php:23
msgid "Carei Branches"
msgstr "Carei Branches"
#: includes/class-cities-widget.php:16
msgid "Carei Cities"
msgstr "Carei Cities"
#: includes/class-elementor-widget.php:115
#: includes/class-search-widget.php:89
msgid "Zwrot w tej samej lokalizacji"
msgstr "Return at the same location"
#: includes/class-elementor-widget.php:133
msgid "Wyjazd zagraniczny"
msgstr "International travel"
#: includes/class-elementor-widget.php:138
msgid "Wyjazd poza granicę Polski do:"
msgstr "Travel outside Poland to:"
#: includes/class-elementor-widget.php:143
msgid "Wyszukaj i dodaj kraj na trasie"
msgstr "Search and add a country on the route"
#: includes/class-elementor-widget.php:144
msgid "Wyczyść"
msgstr "Clear"
#: includes/class-admin-panel.php:102 includes/class-admin-panel.php:86
#: includes/class-admin-panel.php:87
#: includes/class-elementor-widget.php:153
msgid "Pakiety ochronne"
msgstr "Protection packages"
#: includes/class-elementor-widget.php:16
msgid "Carei Reservation"
msgstr "Carei Reservation"
#: includes/class-admin-panel.php:412
#: includes/class-elementor-widget.php:160
msgid "Opcje dodatkowe"
msgstr "Additional options"
#: includes/class-elementor-widget.php:170
msgid "Dane najemcy"
msgstr "Renter details"
#: includes/class-admin-panel.php:405
#: includes/class-elementor-widget.php:177
msgid "Imię"
msgstr "First name"
#: includes/class-admin-panel.php:406
#: includes/class-elementor-widget.php:183
msgid "Nazwisko"
msgstr "Last name"
#: includes/class-elementor-widget.php:192
msgid "Adres e-mail"
msgstr "Email address"
#: includes/class-elementor-widget.php:197
msgid "Nr telefonu"
msgstr "Phone number"
#: includes/class-elementor-widget.php:211
msgid "Twoja wiadomość dotycząca rezerwacji"
msgstr "Your message regarding the reservation"
#: includes/class-elementor-widget.php:222
msgid "Zgadzam się na <a href=\"/polityka-prywatnosci/\" target=\"_blank\">Politykę Prywatności</a>"
msgstr "I agree to the <a href=\"/polityka-prywatnosci/\" target=\"_blank\">Privacy Policy</a>"
#: includes/class-elementor-widget.php:231
msgid "Uzupełnij wymagane pola zaznaczone na czerwono."
msgstr "Please complete the required fields marked in red."
#: includes/class-elementor-widget.php:246
msgid "Wróć do formularza"
msgstr "Back to form"
#: includes/class-elementor-widget.php:260
msgid "Zamówienie złożone!"
msgstr "Order submitted!"
#: includes/class-elementor-widget.php:262
msgid "Oczekuj na kontakt z wypożyczalnią"
msgstr "Please wait for the rental company to contact you"
#: includes/class-elementor-widget.php:263
msgid "Zamknij"
msgstr "Close"
#: includes/class-elementor-widget.php:37
msgid "Przycisk rezerwacji"
msgstr "Reservation button"
#: includes/class-elementor-widget.php:42
msgid "Tekst przycisku"
msgstr "Button text"
#: includes/class-elementor-widget.php:44
#: includes/class-search-widget.php:97
msgid "Złóż zapytanie o rezerwację"
msgstr "Request a reservation"
#: includes/class-elementor-widget.php:62
msgid "Zamknij formularz"
msgstr "Close form"
#: includes/class-elementor-widget.php:67
#: includes/class-search-widget.php:40
msgid "Wypełnij formularz rezerwacji"
msgstr "Fill out the reservation form"
#: includes/class-elementor-widget.php:85
#: includes/class-search-widget.php:58
msgid "Od kiedy?"
msgstr "From?"
#: includes/class-elementor-widget.php:92
#: includes/class-search-widget.php:65
msgid "Do kiedy?"
msgstr "Until?"
#: includes/class-elementor-widget.php:98
msgid "Wybrano:"
msgstr "Selected:"
#: includes/class-elementor-widget.php:98
msgid "0 dni"
msgstr "0 days"
#: includes/class-branches-widget.php:99 includes/class-map-widget.php:172
msgid "Oddział %s"
msgstr "Location %s"
#: includes/class-branches-widget.php:95 includes/class-map-widget.php:176
msgid "ul. %s"
msgstr "%s St."
#: includes/class-map-widget.php:66
msgid "Carei Map"
msgstr "Carei Map"
#: includes/class-rest-proxy.php:148
msgid "Invalid nonce."
msgstr "Invalid nonce."
#: includes/class-rest-proxy.php:159
msgid "Softra API not configured."
msgstr "Softra API not configured."
#: includes/class-search-widget.php:16
msgid "Carei Search Form"
msgstr "Carei Search Form"

View File

@@ -0,0 +1,659 @@
# 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.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"
#: carei-reservation.php:133
msgid "Błąd API: HTTP %status%"
msgstr "API error: HTTP %status%"
#: carei-reservation.php:134
msgid "Przekroczono czas oczekiwania. Spróbuj ponownie."
msgstr "Request timed out. Please try again."
#: carei-reservation.php:135
msgid "Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie."
msgstr "No server connection. Check your internet and try again."
#: carei-reservation.php:136
msgid "Błąd ładowania"
msgstr "Loading error"
#: carei-reservation.php:139
msgid "Wybierz..."
msgstr "Select..."
#: carei-reservation.php:140 includes/class-elementor-widget.php:77
msgid "Wybierz segment pojazdu"
msgstr "Select a vehicle segment"
#: carei-reservation.php:141 includes/class-search-widget.php:47
msgid "Wybierz segment"
msgstr "Select segment"
#: carei-reservation.php:142
msgid "Wybierz miejsce odbioru"
msgstr "Select pickup location"
#: carei-reservation.php:143
msgid "Wybierz miejsce zwrotu"
msgstr "Select return location"
#: carei-reservation.php:144
msgid "Segment %name%"
msgstr "Segment %name%"
#: carei-reservation.php:145
msgid "Brak segmentów"
msgstr "No segments"
#: carei-reservation.php:146
msgid "Najpierw wybierz segment"
msgstr "Select segment first"
#: carei-reservation.php:147 carei-reservation.php:209
#: includes/class-elementor-widget.php:105
#: includes/class-search-widget.php:76
msgid "Miejsce odbioru"
msgstr "Pickup location"
#: carei-reservation.php:148 carei-reservation.php:210
#: includes/class-elementor-widget.php:123
msgid "Miejsce zwrotu"
msgstr "Return location"
#: carei-reservation.php:149
msgid "Brak lokalizacji dla tego segmentu"
msgstr "No locations for this segment"
#: carei-reservation.php:150
msgid "Brak lokalizacji"
msgstr "No locations"
#: carei-reservation.php:153
msgid "Rozpoczęcie"
msgstr "Start"
#: carei-reservation.php:154
msgid "Zakończenie"
msgstr "End"
#: carei-reservation.php:155
msgid "%label% — data lub godzina już minęły"
msgstr "%label% — date or time has already passed"
#: carei-reservation.php:156
msgid "Data lub godzina rozpoczęcia już minęły"
msgstr "Start date or time has already passed"
#: carei-reservation.php:157
msgid "Data lub godzina zakończenia już minęły"
msgstr "End date or time has already passed"
#: carei-reservation.php:158
msgid "Data zakończenia musi być po dacie rozpoczęcia"
msgstr "End date must be after start date"
#: carei-reservation.php:159
msgid "Podaj datę rozpoczęcia"
msgstr "Enter start date"
#: carei-reservation.php:160
msgid "Podaj datę zakończenia"
msgstr "Enter end date"
#: carei-reservation.php:163
msgid "Wybrano: <strong>%count% %unit%</strong>"
msgstr "Selected: <strong>%count% %unit%</strong>"
#: carei-reservation.php:164
msgid "dzień"
msgstr "day"
#: carei-reservation.php:165
msgid "dni"
msgstr "days"
#: carei-reservation.php:166
msgid "doba"
msgstr "day"
#: carei-reservation.php:167
msgid "doby"
msgstr "days"
#: carei-reservation.php:168
msgid "dób"
msgstr "days"
#: carei-reservation.php:171
msgid "%price% zł/doba"
msgstr "%price% PLN/day"
#: carei-reservation.php:172 carei-reservation.php:173
msgid "%price% zł"
msgstr "%price% PLN"
#: carei-reservation.php:174
msgid "od %min% do %max% zł"
msgstr "from %min% to %max% PLN"
#: carei-reservation.php:175
msgid "%perDay% zł/doba × %days% = %total% zł"
msgstr "%perDay% PLN/day × %days% = %total% PLN"
#: carei-reservation.php:176
msgid "Gratis"
msgstr "Free"
#: carei-reservation.php:179
msgid "Usuń"
msgstr "Remove"
#: carei-reservation.php:180
msgid "Dodaj"
msgstr "Add"
#: carei-reservation.php:183
msgid "Podaj imię"
msgstr "Enter first name"
#: carei-reservation.php:184
msgid "Podaj nazwisko"
msgstr "Enter last name"
#: carei-reservation.php:185
msgid "Podaj poprawny adres e-mail"
msgstr "Enter a valid email address"
#: carei-reservation.php:186
msgid "Podaj numer telefonu (min. 9 cyfr)"
msgstr "Enter phone number (min. 9 digits)"
#: carei-reservation.php:187
msgid "Wymagana zgoda na Politykę Prywatności"
msgstr "Privacy policy consent is required"
#: carei-reservation.php:190 includes/class-elementor-widget.php:226
msgid "Wyślij"
msgstr "Send"
#: carei-reservation.php:191
msgid "Przetwarzanie..."
msgstr "Processing..."
#: carei-reservation.php:192
msgid "Rezerwuję..."
msgstr "Reserving..."
#: carei-reservation.php:193 includes/class-elementor-widget.php:250
msgid "Potwierdź rezerwację"
msgstr "Confirm reservation"
#: carei-reservation.php:196
msgid "Ładowanie podsumowania..."
msgstr "Loading summary..."
#: carei-reservation.php:197
msgid "Nie udało się utworzyć klienta"
msgstr "Failed to create customer"
#: carei-reservation.php:198
msgid "Błąd tworzenia klienta: %msg%"
msgstr "Customer creation error: %msg%"
#: carei-reservation.php:199
msgid "Błąd pobierania podsumowania: %msg%"
msgstr "Error loading summary: %msg%"
#: carei-reservation.php:200
msgid "Rezerwacja nie powiodła się"
msgstr "Reservation failed"
#: carei-reservation.php:203
msgid "Pakiet ochronny: %name% — %perDay% zł/doba × %days% = %total% zł (do doliczenia poza systemem)"
msgstr "Protection package: %name% — %perDay% PLN/day × %days% = %total% PLN (to be paid separately)"
#: carei-reservation.php:206 includes/class-admin-panel.php:219
#: includes/class-admin-panel.php:399
msgid "Segment"
msgstr "Segment"
#: carei-reservation.php:207
msgid "Od"
msgstr "From"
#: carei-reservation.php:208
msgid "Do"
msgstr "To"
#: carei-reservation.php:211
msgid "Najemca"
msgstr "Renter"
#: carei-reservation.php:212 includes/class-admin-panel.php:407
msgid "Email"
msgstr "Email"
#: carei-reservation.php:213 includes/class-admin-panel.php:408
msgid "Telefon"
msgstr "Phone"
#: carei-reservation.php:214
msgid "Wybrane opcje"
msgstr "Selected options"
#: carei-reservation.php:215 includes/class-admin-panel.php:414
msgid "Wiadomość"
msgstr "Message"
#: carei-reservation.php:216
msgid "auto"
msgstr "car"
#: carei-reservation.php:217
msgid "do doliczenia"
msgstr "to be added"
#: carei-reservation.php:218
msgid "VAT"
msgstr "VAT"
#: carei-reservation.php:219 includes/class-admin-panel.php:413
msgid "Pakiet ochronny"
msgstr "Protection package"
#: carei-reservation.php:220
msgid "Do zapłaty"
msgstr "Total due"
#: carei-reservation.php:223
msgid "Nazwa"
msgstr "Name"
#: carei-reservation.php:224
msgid "Ilość"
msgstr "Quantity"
#: carei-reservation.php:225
msgid "Netto"
msgstr "Net"
#: carei-reservation.php:226
msgid "Brutto"
msgstr "Gross"
#: carei-reservation.php:229
msgid "Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment."
msgstr "No vehicle available for the selected dates. Change dates or segment."
#: carei-reservation.php:230
msgid "Nieprawidłowy zakres dat"
msgstr "Invalid date range"
#: carei-reservation.php:231
msgid "Nie znaleziono oddziału"
msgstr "Location not found"
#: carei-reservation.php:232
msgid "Klient o tych danych już istnieje w systemie"
msgstr "A customer with these details already exists in the system"
#: carei-reservation.php:233
msgid "Nieprawidłowy numer PESEL"
msgstr "Invalid PESEL number"
#: carei-reservation.php:234
msgid "Cennik wygasł. Odśwież formularz i spróbuj ponownie."
msgstr "Price list expired. Refresh the form and try again."
#: carei-reservation.php:237
msgid "Nr zamówienia: %no%"
msgstr "Order number: %no%"
#: carei-reservation.php:238
msgid "Rezerwacja potwierdzona"
msgstr "Reservation confirmed"
#: carei-reservation.php:239 includes/class-elementor-widget.php:238
msgid "Podsumowanie rezerwacji"
msgstr "Reservation summary"
#: carei-reservation.php:74
msgid "Brak konfiguracji API w pliku .env (url, username, password)."
msgstr "Missing API configuration in .env file (url, username, password)."
#: includes/class-admin-panel.php:103
msgid "Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji."
msgstr "Configuration of packages displayed in the <strong>Protection packages</strong> section of the reservation form. Price is per day — total = price × number of rental days."
#: includes/class-admin-panel.php:105
msgid "Zapisano."
msgstr "Saved."
#: includes/class-admin-panel.php:115
msgid "Pakiet %s"
msgstr "%s Package"
#: includes/class-admin-panel.php:118
msgid "Nazwa wyświetlana"
msgstr "Display name"
#: includes/class-admin-panel.php:122
msgid "Cena za dobę (zł)"
msgstr "Price per day (PLN)"
#: includes/class-admin-panel.php:126 includes/class-admin-panel.php:222
#: includes/class-admin-panel.php:417
msgid "Status"
msgstr "Status"
#: includes/class-admin-panel.php:127
msgid "Aktywny (widoczny w modalu)"
msgstr "Active (visible in modal)"
#: includes/class-admin-panel.php:130
msgid "Opis / zakres usług"
msgstr "Description / service scope"
#: includes/class-admin-panel.php:137
msgid "Zapisz pakiety"
msgstr "Save packages"
#: includes/class-admin-panel.php:152
msgid "Nieprawidłowy token."
msgstr "Invalid token."
#: includes/class-admin-panel.php:189 includes/class-admin-panel.php:191
msgid "Rezerwacje"
msgstr "Reservations"
#: includes/class-admin-panel.php:190
msgid "Rezerwacja"
msgstr "Reservation"
#: includes/class-admin-panel.php:192
msgid "Wszystkie rezerwacje"
msgstr "All reservations"
#: includes/class-admin-panel.php:193
msgid "Zobacz rezerwację"
msgstr "View reservation"
#: includes/class-admin-panel.php:194 includes/class-admin-panel.php:321
msgid "Szczegóły rezerwacji"
msgstr "Reservation details"
#: includes/class-admin-panel.php:195
msgid "Szukaj rezerwacji"
msgstr "Search reservations"
#: includes/class-admin-panel.php:196
msgid "Nie znaleziono rezerwacji"
msgstr "No reservations found"
#: includes/class-admin-panel.php:197
msgid "Brak rezerwacji w koszu"
msgstr "No reservations in trash"
#: includes/class-admin-panel.php:217 includes/class-admin-panel.php:395
msgid "Nr rezerwacji"
msgstr "Reservation No."
#: includes/class-admin-panel.php:218
msgid "Klient"
msgstr "Customer"
#: includes/class-admin-panel.php:22
msgid "Nowe"
msgstr "New"
#: includes/class-admin-panel.php:220
msgid "Daty"
msgstr "Dates"
#: includes/class-admin-panel.php:221
msgid "Oddział"
msgstr "Location"
#: includes/class-admin-panel.php:223
msgid "Data"
msgstr "Date"
#: includes/class-admin-panel.php:24
msgid "Przeczytane"
msgstr "Read"
#: includes/class-admin-panel.php:26
msgid "Zrealizowane"
msgstr "Completed"
#: includes/class-admin-panel.php:279
msgid "Wszystkie statusy"
msgstr "All statuses"
#: includes/class-admin-panel.php:362
msgid "%1$s — %2$s zł/doba × %3$d = %4$s zł"
msgstr "%1$s — %2$s PLN/day × %3$d = %4$s PLN"
#: includes/class-admin-panel.php:396
msgid "ID rezerwacji (Softra)"
msgstr "Reservation ID (Softra)"
#: includes/class-admin-panel.php:397
msgid "ID klienta (Softra)"
msgstr "Customer ID (Softra)"
#: includes/class-admin-panel.php:400
msgid "Data od"
msgstr "Date from"
#: includes/class-admin-panel.php:401
msgid "Data do"
msgstr "Date to"
#: includes/class-admin-panel.php:402
msgid "Oddział odbioru"
msgstr "Pickup location"
#: includes/class-admin-panel.php:403
msgid "Oddział zwrotu"
msgstr "Return location"
#: includes/class-admin-panel.php:409
msgid "PESEL"
msgstr "PESEL"
#: includes/class-admin-panel.php:410
msgid "Adres"
msgstr "Address"
#: includes/class-admin-panel.php:412 includes/class-admin-panel.php:413
msgid "Brak"
msgstr "None"
#: includes/class-admin-panel.php:50
msgid "Ubezpieczenie SOFT"
msgstr "SOFT Protection"
#: includes/class-admin-panel.php:537
msgid "Rezerwacja #%1$s — %2$s %3$s"
msgstr "Reservation #%1$s — %2$s %3$s"
#: includes/class-admin-panel.php:56
msgid "Ubezpieczenie PREMIUM"
msgstr "PREMIUM Protection"
#: includes/class-admin-panel.php:149 includes/class-admin-panel.php:96
msgid "Brak uprawnień."
msgstr "No permissions."
#: includes/class-branches-widget.php:23
msgid "Carei Branches"
msgstr "Carei Branches"
#: includes/class-cities-widget.php:16
msgid "Carei Cities"
msgstr "Carei Cities"
#: includes/class-elementor-widget.php:115
#: includes/class-search-widget.php:89
msgid "Zwrot w tej samej lokalizacji"
msgstr "Return at the same location"
#: includes/class-elementor-widget.php:133
msgid "Wyjazd zagraniczny"
msgstr "International travel"
#: includes/class-elementor-widget.php:138
msgid "Wyjazd poza granicę Polski do:"
msgstr "Travel outside Poland to:"
#: includes/class-elementor-widget.php:143
msgid "Wyszukaj i dodaj kraj na trasie"
msgstr "Search and add a country on the route"
#: includes/class-elementor-widget.php:144
msgid "Wyczyść"
msgstr "Clear"
#: includes/class-admin-panel.php:102 includes/class-admin-panel.php:86
#: includes/class-admin-panel.php:87
#: includes/class-elementor-widget.php:153
msgid "Pakiety ochronne"
msgstr "Protection packages"
#: includes/class-elementor-widget.php:16
msgid "Carei Reservation"
msgstr "Carei Reservation"
#: includes/class-admin-panel.php:412
#: includes/class-elementor-widget.php:160
msgid "Opcje dodatkowe"
msgstr "Additional options"
#: includes/class-elementor-widget.php:170
msgid "Dane najemcy"
msgstr "Renter details"
#: includes/class-admin-panel.php:405
#: includes/class-elementor-widget.php:177
msgid "Imię"
msgstr "First name"
#: includes/class-admin-panel.php:406
#: includes/class-elementor-widget.php:183
msgid "Nazwisko"
msgstr "Last name"
#: includes/class-elementor-widget.php:192
msgid "Adres e-mail"
msgstr "Email address"
#: includes/class-elementor-widget.php:197
msgid "Nr telefonu"
msgstr "Phone number"
#: includes/class-elementor-widget.php:211
msgid "Twoja wiadomość dotycząca rezerwacji"
msgstr "Your message regarding the reservation"
#: includes/class-elementor-widget.php:222
msgid "Zgadzam się na <a href=\"/polityka-prywatnosci/\" target=\"_blank\">Politykę Prywatności</a>"
msgstr "I agree to the <a href=\"/polityka-prywatnosci/\" target=\"_blank\">Privacy Policy</a>"
#: includes/class-elementor-widget.php:231
msgid "Uzupełnij wymagane pola zaznaczone na czerwono."
msgstr "Please complete the required fields marked in red."
#: includes/class-elementor-widget.php:246
msgid "Wróć do formularza"
msgstr "Back to form"
#: includes/class-elementor-widget.php:260
msgid "Zamówienie złożone!"
msgstr "Order submitted!"
#: includes/class-elementor-widget.php:262
msgid "Oczekuj na kontakt z wypożyczalnią"
msgstr "Please wait for the rental company to contact you"
#: includes/class-elementor-widget.php:263
msgid "Zamknij"
msgstr "Close"
#: includes/class-elementor-widget.php:37
msgid "Przycisk rezerwacji"
msgstr "Reservation button"
#: includes/class-elementor-widget.php:42
msgid "Tekst przycisku"
msgstr "Button text"
#: includes/class-elementor-widget.php:44
#: includes/class-search-widget.php:97
msgid "Złóż zapytanie o rezerwację"
msgstr "Request a reservation"
#: includes/class-elementor-widget.php:62
msgid "Zamknij formularz"
msgstr "Close form"
#: includes/class-elementor-widget.php:67
#: includes/class-search-widget.php:40
msgid "Wypełnij formularz rezerwacji"
msgstr "Fill out the reservation form"
#: includes/class-elementor-widget.php:85
#: includes/class-search-widget.php:58
msgid "Od kiedy?"
msgstr "From?"
#: includes/class-elementor-widget.php:92
#: includes/class-search-widget.php:65
msgid "Do kiedy?"
msgstr "Until?"
#: includes/class-elementor-widget.php:98
msgid "Wybrano:"
msgstr "Selected:"
#: includes/class-elementor-widget.php:98
msgid "0 dni"
msgstr "0 days"
#: includes/class-branches-widget.php:99 includes/class-map-widget.php:172
msgid "Oddział %s"
msgstr "Location %s"
#: includes/class-branches-widget.php:95 includes/class-map-widget.php:176
msgid "ul. %s"
msgstr "%s St."
#: includes/class-map-widget.php:66
msgid "Carei Map"
msgstr "Carei Map"
#: includes/class-rest-proxy.php:148
msgid "Invalid nonce."
msgstr "Invalid nonce."
#: includes/class-rest-proxy.php:159
msgid "Softra API not configured."
msgstr "Softra API not configured."
#: includes/class-search-widget.php:16
msgid "Carei Search Form"
msgstr "Carei Search Form"

View File

@@ -0,0 +1,659 @@
# 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.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-22 12:00+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Domain: carei-reservation\n"
#: carei-reservation.php:133
msgid "Błąd API: HTTP %status%"
msgstr ""
#: carei-reservation.php:134
msgid "Przekroczono czas oczekiwania. Spróbuj ponownie."
msgstr ""
#: carei-reservation.php:135
msgid "Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie."
msgstr ""
#: carei-reservation.php:136
msgid "Błąd ładowania"
msgstr ""
#: carei-reservation.php:139
msgid "Wybierz..."
msgstr ""
#: carei-reservation.php:140 includes/class-elementor-widget.php:77
msgid "Wybierz segment pojazdu"
msgstr ""
#: carei-reservation.php:141 includes/class-search-widget.php:47
msgid "Wybierz segment"
msgstr ""
#: carei-reservation.php:142
msgid "Wybierz miejsce odbioru"
msgstr ""
#: carei-reservation.php:143
msgid "Wybierz miejsce zwrotu"
msgstr ""
#: carei-reservation.php:144
msgid "Segment %name%"
msgstr ""
#: carei-reservation.php:145
msgid "Brak segmentów"
msgstr ""
#: carei-reservation.php:146
msgid "Najpierw wybierz segment"
msgstr ""
#: carei-reservation.php:147 carei-reservation.php:209
#: includes/class-elementor-widget.php:105
#: includes/class-search-widget.php:76
msgid "Miejsce odbioru"
msgstr ""
#: carei-reservation.php:148 carei-reservation.php:210
#: includes/class-elementor-widget.php:123
msgid "Miejsce zwrotu"
msgstr ""
#: carei-reservation.php:149
msgid "Brak lokalizacji dla tego segmentu"
msgstr ""
#: carei-reservation.php:150
msgid "Brak lokalizacji"
msgstr ""
#: carei-reservation.php:153
msgid "Rozpoczęcie"
msgstr ""
#: carei-reservation.php:154
msgid "Zakończenie"
msgstr ""
#: carei-reservation.php:155
msgid "%label% — data lub godzina już minęły"
msgstr ""
#: carei-reservation.php:156
msgid "Data lub godzina rozpoczęcia już minęły"
msgstr ""
#: carei-reservation.php:157
msgid "Data lub godzina zakończenia już minęły"
msgstr ""
#: carei-reservation.php:158
msgid "Data zakończenia musi być po dacie rozpoczęcia"
msgstr ""
#: carei-reservation.php:159
msgid "Podaj datę rozpoczęcia"
msgstr ""
#: carei-reservation.php:160
msgid "Podaj datę zakończenia"
msgstr ""
#: carei-reservation.php:163
msgid "Wybrano: <strong>%count% %unit%</strong>"
msgstr ""
#: carei-reservation.php:164
msgid "dzień"
msgstr ""
#: carei-reservation.php:165
msgid "dni"
msgstr ""
#: carei-reservation.php:166
msgid "doba"
msgstr ""
#: carei-reservation.php:167
msgid "doby"
msgstr ""
#: carei-reservation.php:168
msgid "dób"
msgstr ""
#: carei-reservation.php:171
msgid "%price% zł/doba"
msgstr ""
#: carei-reservation.php:172 carei-reservation.php:173
msgid "%price% zł"
msgstr ""
#: carei-reservation.php:174
msgid "od %min% do %max% zł"
msgstr ""
#: carei-reservation.php:175
msgid "%perDay% zł/doba × %days% = %total% zł"
msgstr ""
#: carei-reservation.php:176
msgid "Gratis"
msgstr ""
#: carei-reservation.php:179
msgid "Usuń"
msgstr ""
#: carei-reservation.php:180
msgid "Dodaj"
msgstr ""
#: carei-reservation.php:183
msgid "Podaj imię"
msgstr ""
#: carei-reservation.php:184
msgid "Podaj nazwisko"
msgstr ""
#: carei-reservation.php:185
msgid "Podaj poprawny adres e-mail"
msgstr ""
#: carei-reservation.php:186
msgid "Podaj numer telefonu (min. 9 cyfr)"
msgstr ""
#: carei-reservation.php:187
msgid "Wymagana zgoda na Politykę Prywatności"
msgstr ""
#: carei-reservation.php:190 includes/class-elementor-widget.php:226
msgid "Wyślij"
msgstr ""
#: carei-reservation.php:191
msgid "Przetwarzanie..."
msgstr ""
#: carei-reservation.php:192
msgid "Rezerwuję..."
msgstr ""
#: carei-reservation.php:193 includes/class-elementor-widget.php:250
msgid "Potwierdź rezerwację"
msgstr ""
#: carei-reservation.php:196
msgid "Ładowanie podsumowania..."
msgstr ""
#: carei-reservation.php:197
msgid "Nie udało się utworzyć klienta"
msgstr ""
#: carei-reservation.php:198
msgid "Błąd tworzenia klienta: %msg%"
msgstr ""
#: carei-reservation.php:199
msgid "Błąd pobierania podsumowania: %msg%"
msgstr ""
#: carei-reservation.php:200
msgid "Rezerwacja nie powiodła się"
msgstr ""
#: carei-reservation.php:203
msgid "Pakiet ochronny: %name% — %perDay% zł/doba × %days% = %total% zł (do doliczenia poza systemem)"
msgstr ""
#: carei-reservation.php:206 includes/class-admin-panel.php:219
#: includes/class-admin-panel.php:399
msgid "Segment"
msgstr ""
#: carei-reservation.php:207
msgid "Od"
msgstr ""
#: carei-reservation.php:208
msgid "Do"
msgstr ""
#: carei-reservation.php:211
msgid "Najemca"
msgstr ""
#: carei-reservation.php:212 includes/class-admin-panel.php:407
msgid "Email"
msgstr ""
#: carei-reservation.php:213 includes/class-admin-panel.php:408
msgid "Telefon"
msgstr ""
#: carei-reservation.php:214
msgid "Wybrane opcje"
msgstr ""
#: carei-reservation.php:215 includes/class-admin-panel.php:414
msgid "Wiadomość"
msgstr ""
#: carei-reservation.php:216
msgid "auto"
msgstr ""
#: carei-reservation.php:217
msgid "do doliczenia"
msgstr ""
#: carei-reservation.php:218
msgid "VAT"
msgstr ""
#: carei-reservation.php:219 includes/class-admin-panel.php:413
msgid "Pakiet ochronny"
msgstr ""
#: carei-reservation.php:220
msgid "Do zapłaty"
msgstr ""
#: carei-reservation.php:223
msgid "Nazwa"
msgstr ""
#: carei-reservation.php:224
msgid "Ilość"
msgstr ""
#: carei-reservation.php:225
msgid "Netto"
msgstr ""
#: carei-reservation.php:226
msgid "Brutto"
msgstr ""
#: carei-reservation.php:229
msgid "Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment."
msgstr ""
#: carei-reservation.php:230
msgid "Nieprawidłowy zakres dat"
msgstr ""
#: carei-reservation.php:231
msgid "Nie znaleziono oddziału"
msgstr ""
#: carei-reservation.php:232
msgid "Klient o tych danych już istnieje w systemie"
msgstr ""
#: carei-reservation.php:233
msgid "Nieprawidłowy numer PESEL"
msgstr ""
#: carei-reservation.php:234
msgid "Cennik wygasł. Odśwież formularz i spróbuj ponownie."
msgstr ""
#: carei-reservation.php:237
msgid "Nr zamówienia: %no%"
msgstr ""
#: carei-reservation.php:238
msgid "Rezerwacja potwierdzona"
msgstr ""
#: carei-reservation.php:239 includes/class-elementor-widget.php:238
msgid "Podsumowanie rezerwacji"
msgstr ""
#: carei-reservation.php:74
msgid "Brak konfiguracji API w pliku .env (url, username, password)."
msgstr ""
#: includes/class-admin-panel.php:103
msgid "Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji."
msgstr ""
#: includes/class-admin-panel.php:105
msgid "Zapisano."
msgstr ""
#: includes/class-admin-panel.php:115
msgid "Pakiet %s"
msgstr ""
#: includes/class-admin-panel.php:118
msgid "Nazwa wyświetlana"
msgstr ""
#: includes/class-admin-panel.php:122
msgid "Cena za dobę (zł)"
msgstr ""
#: includes/class-admin-panel.php:126 includes/class-admin-panel.php:222
#: includes/class-admin-panel.php:417
msgid "Status"
msgstr ""
#: includes/class-admin-panel.php:127
msgid "Aktywny (widoczny w modalu)"
msgstr ""
#: includes/class-admin-panel.php:130
msgid "Opis / zakres usług"
msgstr ""
#: includes/class-admin-panel.php:137
msgid "Zapisz pakiety"
msgstr ""
#: includes/class-admin-panel.php:152
msgid "Nieprawidłowy token."
msgstr ""
#: includes/class-admin-panel.php:189 includes/class-admin-panel.php:191
msgid "Rezerwacje"
msgstr ""
#: includes/class-admin-panel.php:190
msgid "Rezerwacja"
msgstr ""
#: includes/class-admin-panel.php:192
msgid "Wszystkie rezerwacje"
msgstr ""
#: includes/class-admin-panel.php:193
msgid "Zobacz rezerwację"
msgstr ""
#: includes/class-admin-panel.php:194 includes/class-admin-panel.php:321
msgid "Szczegóły rezerwacji"
msgstr ""
#: includes/class-admin-panel.php:195
msgid "Szukaj rezerwacji"
msgstr ""
#: includes/class-admin-panel.php:196
msgid "Nie znaleziono rezerwacji"
msgstr ""
#: includes/class-admin-panel.php:197
msgid "Brak rezerwacji w koszu"
msgstr ""
#: includes/class-admin-panel.php:217 includes/class-admin-panel.php:395
msgid "Nr rezerwacji"
msgstr ""
#: includes/class-admin-panel.php:218
msgid "Klient"
msgstr ""
#: includes/class-admin-panel.php:22
msgid "Nowe"
msgstr ""
#: includes/class-admin-panel.php:220
msgid "Daty"
msgstr ""
#: includes/class-admin-panel.php:221
msgid "Oddział"
msgstr ""
#: includes/class-admin-panel.php:223
msgid "Data"
msgstr ""
#: includes/class-admin-panel.php:24
msgid "Przeczytane"
msgstr ""
#: includes/class-admin-panel.php:26
msgid "Zrealizowane"
msgstr ""
#: includes/class-admin-panel.php:279
msgid "Wszystkie statusy"
msgstr ""
#: includes/class-admin-panel.php:362
msgid "%1$s — %2$s zł/doba × %3$d = %4$s zł"
msgstr ""
#: includes/class-admin-panel.php:396
msgid "ID rezerwacji (Softra)"
msgstr ""
#: includes/class-admin-panel.php:397
msgid "ID klienta (Softra)"
msgstr ""
#: includes/class-admin-panel.php:400
msgid "Data od"
msgstr ""
#: includes/class-admin-panel.php:401
msgid "Data do"
msgstr ""
#: includes/class-admin-panel.php:402
msgid "Oddział odbioru"
msgstr ""
#: includes/class-admin-panel.php:403
msgid "Oddział zwrotu"
msgstr ""
#: includes/class-admin-panel.php:409
msgid "PESEL"
msgstr ""
#: includes/class-admin-panel.php:410
msgid "Adres"
msgstr ""
#: includes/class-admin-panel.php:412 includes/class-admin-panel.php:413
msgid "Brak"
msgstr ""
#: includes/class-admin-panel.php:50
msgid "Ubezpieczenie SOFT"
msgstr ""
#: includes/class-admin-panel.php:537
msgid "Rezerwacja #%1$s — %2$s %3$s"
msgstr ""
#: includes/class-admin-panel.php:56
msgid "Ubezpieczenie PREMIUM"
msgstr ""
#: includes/class-admin-panel.php:149 includes/class-admin-panel.php:96
msgid "Brak uprawnień."
msgstr ""
#: includes/class-branches-widget.php:23
msgid "Carei Branches"
msgstr ""
#: includes/class-cities-widget.php:16
msgid "Carei Cities"
msgstr ""
#: includes/class-elementor-widget.php:115
#: includes/class-search-widget.php:89
msgid "Zwrot w tej samej lokalizacji"
msgstr ""
#: includes/class-elementor-widget.php:133
msgid "Wyjazd zagraniczny"
msgstr ""
#: includes/class-elementor-widget.php:138
msgid "Wyjazd poza granicę Polski do:"
msgstr ""
#: includes/class-elementor-widget.php:143
msgid "Wyszukaj i dodaj kraj na trasie"
msgstr ""
#: includes/class-elementor-widget.php:144
msgid "Wyczyść"
msgstr ""
#: includes/class-admin-panel.php:102 includes/class-admin-panel.php:86
#: includes/class-admin-panel.php:87
#: includes/class-elementor-widget.php:153
msgid "Pakiety ochronne"
msgstr ""
#: includes/class-elementor-widget.php:16
msgid "Carei Reservation"
msgstr ""
#: includes/class-admin-panel.php:412
#: includes/class-elementor-widget.php:160
msgid "Opcje dodatkowe"
msgstr ""
#: includes/class-elementor-widget.php:170
msgid "Dane najemcy"
msgstr ""
#: includes/class-admin-panel.php:405
#: includes/class-elementor-widget.php:177
msgid "Imię"
msgstr ""
#: includes/class-admin-panel.php:406
#: includes/class-elementor-widget.php:183
msgid "Nazwisko"
msgstr ""
#: includes/class-elementor-widget.php:192
msgid "Adres e-mail"
msgstr ""
#: includes/class-elementor-widget.php:197
msgid "Nr telefonu"
msgstr ""
#: includes/class-elementor-widget.php:211
msgid "Twoja wiadomość dotycząca rezerwacji"
msgstr ""
#: includes/class-elementor-widget.php:222
msgid "Zgadzam się na <a href=\"/polityka-prywatnosci/\" target=\"_blank\">Politykę Prywatności</a>"
msgstr ""
#: includes/class-elementor-widget.php:231
msgid "Uzupełnij wymagane pola zaznaczone na czerwono."
msgstr ""
#: includes/class-elementor-widget.php:246
msgid "Wróć do formularza"
msgstr ""
#: includes/class-elementor-widget.php:260
msgid "Zamówienie złożone!"
msgstr ""
#: includes/class-elementor-widget.php:262
msgid "Oczekuj na kontakt z wypożyczalnią"
msgstr ""
#: includes/class-elementor-widget.php:263
msgid "Zamknij"
msgstr ""
#: includes/class-elementor-widget.php:37
msgid "Przycisk rezerwacji"
msgstr ""
#: includes/class-elementor-widget.php:42
msgid "Tekst przycisku"
msgstr ""
#: includes/class-elementor-widget.php:44
#: includes/class-search-widget.php:97
msgid "Złóż zapytanie o rezerwację"
msgstr ""
#: includes/class-elementor-widget.php:62
msgid "Zamknij formularz"
msgstr ""
#: includes/class-elementor-widget.php:67
#: includes/class-search-widget.php:40
msgid "Wypełnij formularz rezerwacji"
msgstr ""
#: includes/class-elementor-widget.php:85
#: includes/class-search-widget.php:58
msgid "Od kiedy?"
msgstr ""
#: includes/class-elementor-widget.php:92
#: includes/class-search-widget.php:65
msgid "Do kiedy?"
msgstr ""
#: includes/class-elementor-widget.php:98
msgid "Wybrano:"
msgstr ""
#: includes/class-elementor-widget.php:98
msgid "0 dni"
msgstr ""
#: includes/class-branches-widget.php:99 includes/class-map-widget.php:172
msgid "Oddział %s"
msgstr ""
#: includes/class-branches-widget.php:95 includes/class-map-widget.php:176
msgid "ul. %s"
msgstr ""
#: includes/class-map-widget.php:66
msgid "Carei Map"
msgstr ""
#: includes/class-rest-proxy.php:148
msgid "Invalid nonce."
msgstr ""
#: includes/class-rest-proxy.php:159
msgid "Softra API not configured."
msgstr ""
#: includes/class-search-widget.php:16
msgid "Carei Search Form"
msgstr ""

BIN
wp-content/themes/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/Requests/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/SimplePie/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/blocks/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/images/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/js/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/rest-api/.DS_Store vendored Normal file

Binary file not shown.

BIN
wp-includes/sodium_compat/.DS_Store vendored Normal file

Binary file not shown.