From 5b52f0fdad6edb27e66cf61ffa63a71716298e3f Mon Sep 17 00:00:00 2001 From: Wynne Pirini Date: Wed, 4 Nov 2026 08:38:55 +1300 Subject: [PATCH] feat(i18n): add Polish locale (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal/i18n/locales/pl.json, so Polish joins the booker-facing surfaces: booking page, manage page, embed widget, the four emails and the calendar invite. Nothing else is needed - init() globs the locale directory, and the switcher reads its display name from x/text's CLDR data, so the existing picker shows "Polski" automatically. The strings come from kybeq's proposal in #216. The JSON is complete against en.json (169/169 keys, no extras) and passes the same-keys and printf-verb guards unchanged. The 19 date abbreviations had to be corrected to match CLDR, which the TestDateTablesMatchCLDR guard checks against Intl: - month_short_* are lowercase in Polish CLDR (Sty -> sty, Gru -> gru, ...) - dow_short_* are lowercase with a trailing dot (Pon -> pon., Śr -> śr., ...) date_format ('%[1]s %[2]d %[3]s %[4]d') and clock_format ('24h') already match Polish convention, so the proposal is used as given there. Docs updated for the new count: README, ARCHITECTURE \u00a723 and CLAUDE.md. Credit: strings by kybeq (#216). --- CLAUDE.md | 2 +- README.md | 5 +- docs/ARCHITECTURE.md | 2 +- internal/i18n/locales/pl.json | 171 ++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 internal/i18n/locales/pl.json diff --git a/CLAUDE.md b/CLAUDE.md index cfa2a80b..bb5c85fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,7 @@ no string plumbing with the Go templates), and admin-authored content (event nam descriptions, questions, custom email copy). Locale is resolved per request from `Accept-Language` + a `?lang=` override + the operator's fallback setting (`internal/handler/i18n.go`), and the booker's locale is stored on the booking so later -reminders match. Ships `en es fr fr-CA de it pt nl sv`. Full detail: ARCHITECTURE §23. +reminders match. Ships `en es fr fr-CA de it pt nl sv pl`. Full detail: ARCHITECTURE §23. **Adding a locale = adding `internal/i18n/locales/.json`.** Nothing else. `init()` globs the directory; the switcher, the fallback dropdown and the public API payload all read diff --git a/README.md b/README.md index b9db471f..e4e46d6c 100644 --- a/README.md +++ b/README.md @@ -200,9 +200,10 @@ domains, Resend email, Google & Microsoft OAuth, Litestream backups, troubleshoo - **Zoom** — per-host OAuth; a Zoom-located booking mints a meeting under the assigned host's account - **Built-in video meetings (LiveKit)** — in-browser rooms as a booking location (no app or account for guests); host controls (end-for-all, hand-off **and reclaim** host, attendee screen-share toggle), **meeting recording** straight to your own **Litestream backup bucket** (the same one you already use for DB backup — no extra storage to provision) with in-app downloads, **recording consent** (notice + consent-or-leave), and an **AI notetaker** (Deepgram transcript → LLM notes). Headless-consumable: MCP `get_meeting_notes`/`get_transcript` + `recording.completed`/`transcript.ready`/`notes.ready` webhooks. BYO LiveKit endpoint (Cloud or self-hosted); configured in Settings → Video — see [docs/VIDEO.md](docs/VIDEO.md) -- **9 languages** on every booker-facing surface - booking page, manage/reschedule page, +- **10 languages** on every booker-facing surface - booking page, manage/reschedule page, embed widget, all four emails, and the calendar invite: **English · Spanish · French · - Canadian French · German · Italian · Portuguese · Dutch · Swedish**. Picked from + Canadian French · German · Italian · Portuguese · Dutch · Swedish · + Polish**. Picked from `Accept-Language` with a footer switcher and an operator-set fallback language; the booker's choice is stored on the booking, so reminders arrive in the language they booked in. Adding a language is adding one JSON file - no code change. *(The admin UI and the built-in video room are diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d8f2ba2a..079731c7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1229,7 +1229,7 @@ LLM summary) — the next build; consent-gated (§8.11/§15 of the PRD). ## 23. Languages (i18n) -Calnode ships **9 locales**: `en` (source) · `es` · `fr` · `fr-CA` · `de` · `it` · `pt` · `nl` · `sv`. +Calnode ships **10 locales**: `en` (source) · `es` · `fr` · `fr-CA` · `de` · `it` · `pt` · `nl` · `sv` · `pl`. `fr-CA` is the first **regional** locale, and it is a separate file rather than a fallback because the differences are real: `courriel` not `e-mail`, `reporter`/`report` not diff --git a/internal/i18n/locales/pl.json b/internal/i18n/locales/pl.json new file mode 100644 index 00000000..3f0d96ae --- /dev/null +++ b/internal/i18n/locales/pl.json @@ -0,0 +1,171 @@ +{ + "book_by_chat": "Zarezerwuj przez czat", + "close": "Zamknij", + "assistant_greeting": "Cześć! Powiedz mi w przybliżeniu, kiedy chcesz się spotkać — np. „w czwartek po południu” lub „w przyszłym tygodniu” — a znajdę termin. W każdej chwili możesz też użyć kalendarza.", + "assistant_input_placeholder": "Wpisz, kiedy masz czas…", + "assistant_input_aria": "Napisz do asystenta rezerwacji", + "send": "Wyślij", + "show_more": "Pokaż więcej", + "show_less": "Pokaż mniej", + "date_picker_aria": "Wybór daty", + "timezone_aria": "Strefa czasowa", + "select_day_hint": "Wybierz dzień, aby zobaczyć dostępne godziny.", + "back": "Wstecz", + "name_label": "Imię i nazwisko", + "name_placeholder": "Twoje imię i nazwisko", + "email_label": "E-mail", + "email_placeholder": "ty@example.com", + "choose_option": "Wybierz opcję", + "confirm_booking": "Potwierdź rezerwację", + "confirming": "Potwierdzanie…", + "booking_confirmed": "Rezerwacja potwierdzona", + "confirmation_email_sent": "E-mail z potwierdzeniem został wysłany.", + "required_fields_error": "Wypełnij wszystkie wymagane pola.", + "slot_taken_error": "Ten termin właśnie został zajęty. Wybierz inną godzinę.", + "booking_failed_error": "Rezerwacja nie powiodła się. Spróbuj ponownie.", + "network_error": "Błąd sieci. Sprawdź swoje połączenie z internetem.", + "loading": "Ładowanie…", + "no_available_times": "Brak dostępnych terminów w dniu %s. Wybierz inną datę.", + "no_available_times_host": "%[1]s nie ma dostępnych terminów w dniu %[2]s. Wybierz inną datę.", + "slot_taken": "już zarezerwowane", + "all_times_taken": "Wszystkie terminy w tym dniu są zajęte.", + "min_notice_hint": "Rezerwacji należy dokonać z co najmniej %s wyprzedzeniem.", + "could_not_load_times": "Nie udało się załadować terminów. Spróbuj ponownie.", + "times_shown_in": "Godziny wyświetlane w strefie ", + "payment_received": "Płatność otrzymana", + "payment_confirmed_sub": "Twoja rezerwacja została potwierdzona — e-mail z potwierdzeniem jest już w drodze.", + "assistant_error": "Przepraszamy — coś poszło nie tak. Użyj poniższego kalendarza.", + "assistant_disclosure": "Rozmawiasz z automatycznym asystentem, a nie z człowiekiem.", + "other": "inny", + "others": "inni", + "list_separator": ", ", + "list_conjunction": " i ", + "language_switcher_aria": "Język", + "cookie_consent_aria": "Zgoda na pliki cookie", + "cookie_text": "Używamy analitycznych plików cookie, aby zrozumieć, jak używana jest ta strona rezerwacji — zapisujemy je tylko za Twoją zgodą.", + "privacy_policy": "Polityka prywatności", + "decline": "Odrzuć", + "accept": "Akceptuj", + "privacy": "Prywatność", + "terms": "Regulamin", + "cookie_settings": "Ustawienia plików cookie", + "prev_month_aria": "Poprzedni miesiąc", + "next_month_aria": "Następny miesiąc", + "demo_public_label": "Demo publiczne", + "demo_public_text": "dane widoczne w tym miejscu są publiczne i resetują się automatycznie. Nie wprowadzaj poufnych informacji.", + "demo_view_source": "Zobacz kod źródłowy", + "powered_by": "Obsługiwane przez", + "duration_min": "%d min", + "duration_hour_one": "1 godzina", + "duration_hours": "%d godz.", + "duration_hr_min": "%d godz. %d min", + "location_video_meeting": "Spotkanie wideo", + "location_phone": "Rozmowa telefoniczna", + "location_in_person": "Osobiście", + "location_video_call": "Połączenie wideo", + "manage_booking_title": "Zarządzaj rezerwacją", + "link_expired_title": "Link wygasł lub jest nieprawidłowy", + "link_expired_text": "Ten link do zarządzania wygasł lub nie jest już aktywny. Sprawdź e-mail z potwierdzeniem, aby znaleźć aktualny link, lub skontaktuj się z organizatorem.", + "booking_already_cancelled_title": "Rezerwacja została już anulowana", + "booking_already_cancelled_text": "Ta rezerwacja została anulowana. Aby zarezerwować nowy termin, przejdź do strony wydarzenia.", + "scheduled_for": "Zaplanowano na", + "reschedule": "Zmień termin", + "cancel_booking": "Anuluj rezerwację", + "select_a_date": "Wybierz datę", + "pick_day_hint": "Wybierz dzień z kalendarza.", + "confirm_reschedule_question": "Potwierdzić zmianę terminu?", + "was_label": "Było", + "now_label": "Teraz", + "confirm_reschedule": "Potwierdź zmianę terminu", + "rescheduling": "Zmienianie terminu…", + "cancel_this_booking_question": "Anulować tę rezerwację?", + "cancel_booking_notice": "Tej operacji nie można cofnąć. Wiadomość o anulowaniu zostanie wysłana do Ciebie oraz do organizatora.", + "reason_optional_label": "Powód (opcjonalnie)", + "reason_placeholder": "np. konflikt w kalendarzu", + "yes_cancel_booking": "Tak, anuluj rezerwację", + "cancelling": "Anulowanie…", + "booking_rescheduled_title": "Zmieniono termin rezerwacji", + "booking_rescheduled_prefix": "Twoja rezerwacja została przeniesiona na ", + "booking_cancelled_title": "Rezerwacja anulowana", + "booking_cancelled_text": "Twoja rezerwacja została anulowana.", + "slot_no_longer_available_error": "Ten termin nie jest już dostępny. Wybierz inną godzinę.", + "reschedule_failed_error": "Zmiana terminu nie powiodła się. Spróbuj ponownie.", + "already_cancelled_error": "Ta rezerwacja jest już anulowana.", + "cancellation_failed_error": "Anulowanie nie powiodło się. Spróbuj ponownie.", + "dow_short_sun": "niedz.", + "dow_short_mon": "pon.", + "dow_short_tue": "wt.", + "dow_short_wed": "śr.", + "dow_short_thu": "czw.", + "dow_short_fri": "pt.", + "dow_short_sat": "sob.", + "month_short_jan": "sty", + "month_short_feb": "lut", + "month_short_mar": "mar", + "month_short_apr": "kwi", + "month_short_may": "maj", + "month_short_jun": "cze", + "month_short_jul": "lip", + "month_short_aug": "sie", + "month_short_sep": "wrz", + "month_short_oct": "paź", + "month_short_nov": "lis", + "month_short_dec": "gru", + "clock_format": "24h", + "date_format": "%[1]s %[2]d %[3]s %[4]d", + "email_greeting": "Cześć %s,", + "email_confirmed_lead": "Twoja rezerwacja została potwierdzona.", + "email_cancelled_lead": "Twoja rezerwacja została anulowana.", + "email_rescheduled_lead": "Termin Twojej rezerwacji został zmieniony.", + "email_reminder_lead": "Przypomnienie: zbliża się termin Twojej rezerwacji.", + "email_label_event": "Wydarzenie", + "email_label_with": "Z kim", + "email_label_start": "Początek", + "email_label_end": "Koniec", + "email_label_when": "Kiedy", + "email_label_location": "Lokalizacja", + "email_label_reason": "Powód", + "email_label_was": "Poprzedni termin", + "email_label_now": "Nowy termin", + "email_add_to_calendar": "Dodaj do kalendarza:", + "email_add_to_calendar_updated": "Dodaj do kalendarza (zaktualizowany termin):", + "email_calendar_google": "Kalendarz Google", + "email_calendar_outlook": "Outlook", + "email_booking_reference": "Numer rezerwacji: %s", + "email_manage_link_intro": "Aby zmienić termin lub anulować rezerwację, przejdź do:", + "email_manage_link_intro_reschedule": "Aby ponowanie zmienić termin lub anulować rezerwację, przejdź do:", + "email_cancel_link_intro": "Aby anulować, przejdź do:", + "email_rebook_intro": "Aby zarezerwować ponowanie, przejdź do:", + "email_manage_button": "Zarządzaj rezerwacją", + "email_rebook_button": "Zarezerwuj ponownie", + "email_subject_confirmed": "Potwierdzenie rezerwacji: %s", + "email_subject_cancelled": "Anulowanie rezerwacji: %s", + "email_subject_rescheduled": "Zmiana terminu rezerwacji: %s", + "email_subject_reminder": "Przypomnienie: zbliża się %s", + "email_booking_with": "Rezerwacja z %s", + "email_manage_this_booking": "Zarządzaj tą rezerwacją:", + "calendar_event_summary": "%s – %s", + "calendar_event_booking_id": "Identyfikator rezerwacji: %s", + "err_booking_limit": "Masz już maksymalną liczbę nadchodzących rezerwacji dla tego wydarzenia. Anuluj istniejącą rezerwację lub poczekaj na jej zakończenie przed kolejną rezerwacją.", + "err_email_throttled": "Z tego adresu e-mail dokonywano ostatnio zbyt wielu rezerwacji. Spróbuj ponownie później.", + "err_required_field": "wymagane pole %q nie zostało wypełnione", + "err_required_checkbox": "pole %q musi zostać zaznaczone", + "err_invalid_option": "nieprawidłowa opcja dla %[1]q: %[2]q nie jest dozwolonym wyborem", + "assistant_unavailable": "Asystent rezerwacji jest w tej chwili niedostępny — wybierz termin z poniższego kalendarza.", + "assistant_trouble": "Przepraszamy — występują trudności techniczne. Wybierz termin z poniższego kalendarza.", + "assistant_iteration_cap": "Wybierzmy prostszą drogę — zaznacz termin z poniższego kalendarza.", + "assistant_status_slots": "Sprawdzanie dostępności…", + "assistant_status_book": "Rezerwowanie…", + "assistant_status_working": "Przetwarzanie…", + "time_period": "Pora dnia", + "time_morning": "Rano", + "time_afternoon": "Po południu", + "time_evening": "Wieczór", + "phone_call_number": "Numer telefonu (opcjonalnie, w przypadku rozmowy telefonicznej)", + "calendar_degraded_notice": "Nie udało się załadować części danych kalendarza, dlatego informacje o dostępności mogą być niepełne.", + "directory_book": "Zarezerwuj", + "directory_empty": "Brak dostępnych wydarzeń do zarezerwowania.", + "directory_members": "Członkowie zespołu", + "directory_person_subtitle": "Zarezerwuj spotkanie z użytkownikiem %s", + "directory_team_subtitle": "Zarezerwuj spotkanie z zespołem %s" +}