---
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
---
## 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 ~80–150 wpisów gotowych do tłumaczenia
- Strona po polsku działa **identycznie jak przed zmianami** (żaden tekst się nie zmienia — tylko jest owinięty)
@.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
## 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)
```
Task 1: PHP i18n — wrap stringów + textdomain bootstrap
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
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.
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
AC-1 satysfakcjonowane.
Task 2: JS i18n — migracja stringów do careiI18n przez wp_localize_script
wp-content/plugins/carei-reservation/assets/js/carei-reservation.js,
wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
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
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)
AC-2 satysfakcjonowane.
Task 3: Generate .pot file w languages/
wp-content/plugins/carei-reservation/languages/carei-reservation.pot
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.
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 ""`
AC-3 satysfakcjonowane: .pot gotowy dla Phase 18 (tłumaczenie).
- 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
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.
Napisz "approved" aby zamknąć plan, albo opisz które miejsca pokazują nietłumaczone / regresyjne stringi.
## 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
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ę
- Wszystkie 3 auto tasks zakończone
- Checkpoint human-verify zatwierdzony ("approved")
- Brak regresji w języku polskim
- `.pot` gotowy do Phase 18