Skip to content

refactor(templating): migrate Smarty templates to Twig - #1860

Open
lucs7 wants to merge 67 commits into
LibreBooking:refactor/twigfrom
lucs7:refactor/twig-migration
Open

lucs7 wants to merge 67 commits into
LibreBooking:refactor/twigfrom
lucs7:refactor/twig-migration

Conversation

@lucs7

@lucs7 lucs7 commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Migrates the Smarty templates to Twig, with Smarty kept as a fallback:

  • Adds twig/twig and a TemplateRenderer abstraction (SmartyRenderer,
    TwigRenderer). Page, EmailMessage and Control render through it.
  • An engine selector renders a template with Twig when a .twig file exists
    next to the .tpl, otherwise with Smarty. Pages still call
    Display('….tpl'); the .tpl files stay in place.
  • LibreBookingExtension ports the Smarty plugins (translate, form/validation,
    buttons, links, formatting, assets, datatable, pagination, controls) and
    modifiers to Twig functions and filters.
  • Migrates all page, control, Ajax, admin, install and email templates
    (including the localized email bodies) to Twig.
  • Adds twig-cs-fixer (composer twigcsfixer:lint / :fix).

Upstream template changes up to current develop (no-double-encode escaping,
restored server-side pagination, server settings versions, Google Calendar
URL, profile-data escaping in emails) are ported into the Twig templates.

Switching between Smarty and Twig (debug mode)

For comparing the engines on a running instance, with app.debug enabled:

  • ?template_engine=smarty renders with the Smarty templates;
    ?template_engine=twig switches back. The choice is stored in a
    template_engine cookie, so following pages and AJAX requests (e.g. the
    schedule's reservation load) use the same engine.
  • It covers full pages, AJAX fragments, controls, partials and emails.
  • The footer shows Template engine: Smarty (from globalfooter.tpl) or
    Template engine: Twig (from globalfooter.twig), and every page sends an
    X-Template-Engine: smarty|twig header.
  • The request is only read, never modified, so URLs rebuilt from the current
    query string (login redirects, pagination) and the route parameter
    validation on schedule/calendar pages keep working.
  • Without app.debug, the parameter and cookie are ignored and nothing is
    shown.

Escaping

Stored text is mixed: Server::GetForm() HTML-encodes web-form input, while
reservation title/description, the REST API, SSO/LDAP logins and some imports
store raw values. Smarty printed most values raw; Twig's default escaping
would double-encode the encoded ones (& on screen).

  • New escape_once filter (escape without re-encoding existing entities),
    equivalent to Smarty's |escape:'html':'UTF-8':false. It is used for user
    text in pages and emails, so both stored forms display correctly and raw
    markup is escaped.
  • Values that Smarty printed raw are now escaped in several places (for
    example reservation titles, resource and accessory names in emails, slot
    labels in the mobile/condensed/monitor views).
  • Helpers marked is_safe now escape what they embed (buildAttributes,
    object_html_options, read_only_attribute); json_encode uses the
    JSON_HEX_* flags.
  • Remaining |raw are app-built URLs, config values, sanitized rich text and
    email values already escaped in PHP. Moving to raw storage plus plain
    autoescaping (with a data migration) is a follow-up.
  • Customized email templates (*-custom.tpl) still render through Smarty.

Testing

  • Golden tests (composer phpunit -- --testsuite golden, 1114 tests) render
    each migrated template with Smarty and Twig using the same fixture variables
    and compare the results after normalization: whitespace collapsed, entity
    and attribute-quote spelling canonicalized, plus documented per-test
    exceptions (mainly the escaping changes above). The output is equivalent,
    not byte-identical (Twig files are reformatted by twig-cs-fixer).
  • Golden tests only compare the if/else branches their fixtures reach;
    several tests run multiple variants, but not every branch is covered.
  • TwigStoredEntityEscapingTest renders real page and email templates with
    stored-encoded and raw values; TemplateEngineOverrideTest and
    PageEngineSelectionTest cover the engine switch.
  • composer phpunit: all pass except
    ConfigTest::testHistoricalLegacyConfigDevelParsesCorrectly and
    PluginConfigLoadingTest::testPluginConfigValidation, which fail locally
    only (environment-specific).
  • PHPStan (base and next), composer phpcsfixer:lint,
    twig-cs-fixer lint tpl lang and commitlint are clean.

🤖 Generated with Claude Code

lucs7 added 16 commits November 7, 2026 21:23
Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-opus-4-8
Add TwigEnvironmentFactory (PSR-4, src/) that creates a Twig\Environment
with html autoescaping, and a TwigRenderer global class (legacy tree)
implementing TemplateRenderer. Widen PageValidators constructor hint to
accept SmartyPage|TwigRenderer. Register TwigRenderer in namespace.php
and add tpl_c/twig/ to .gitignore.

Assisted-by: Claude:claude-sonnet-4-6
Add an empty Twig AbstractExtension wired into TwigRenderer, exposing
getFunctions() and getFilters() stubs ready for Phase 1 tasks.

Assisted-by: Claude:claude-sonnet-4-6
Add protected $renderer (TemplateRenderer) to Page; wire it to a
SmartyRenderer in the constructor and keep $this->smarty pointing at
the inner SmartyPage for full BC. Zero behaviour change.

Assisted-by: Claude:claude-sonnet-4-6
Wire SmartyRenderer into EmailMessage so $this->renderer holds the
TemplateRenderer contract; $this->email kept as a BC alias to the
underlying SmartyPage. Zero behaviour change.

Assisted-by: Claude:claude-sonnet-4-6
Control.__construct now accepts TemplateRenderer|SmartyPage; raw
SmartyPage
is wrapped via SmartyRenderer::wrap() (preserves the existing page
instance).
$this->data changes from \Smarty\Data to a plain array; Set/Get/Display
updated.
All concrete controls' constructors updated to match the widened type.

Assisted-by: Claude:claude-sonnet-4-6
Strengthen ControlRendererTest beyond smoke-test construction: verify
Set/Get round-trip (including null for missing keys), Display output
via a fake TemplateRenderer recording call args and returning a
sentinel,
and that a raw SmartyPage is wrapped into a SmartyRenderer on
construction.

Assisted-by: Claude:claude-sonnet-4-6
Replace every new SmartyPage() constructor call inside
DashboardPresenter::Initialize() with new SmartyRenderer().
SmartyRenderer wraps SmartyPage and implements TemplateRenderer,
so behaviour is identical — this routes all dashboard controls
through the renderer abstraction layer.

Adds DashboardPresenterRendererTest as a guard test verifying
each control accepts and stores a SmartyRenderer instance.

Assisted-by: Claude:claude-sonnet-4-6
Assisted-by: Claude:claude-sonnet-4-6
Twig's CoreExtension already provides a constant() function that
resolves
PHP constants including class constants (e.g.
CustomAttributeTypes::CHECKBOX).
Our custom TwigFunction was a redundant YAGNI override with a weaker
implementation (no object-form, no sandbox awareness).

Remove the custom constant() TwigFunction from LibreBookingExtension and
update the test to prove Twig's native constant() suffices — rendering
{{ constant('CustomAttributeTypes::CHECKBOX') }} through an environment
with only LibreBookingExtension added passes without any custom
function.

Assisted-by: Claude:claude-sonnet-4-6
Register translate() in LibreBookingExtension.getFunctions(), matching
SmartyPage::SmartyTranslate semantics: no args → GetString(key, '');
string args → GetString(key, explode(',', args)); array args passed
directly. Marked is_safe=>html so callers get unescaped HTML strings,
consistent with the Smarty equivalent.

Assisted-by: Claude:claude-sonnet-4-6
Extract linkify logic from SmartyPage::CreateUrl into a PSR-4 static
helper (LibreBooking\Common\Text\LinkifyText) so both engines share one
implementation. Add custom Twig filters sanitize_rich_text, url2link,
escapequotes, html_entity_decode, intval to LibreBookingExtension.
Confirm Twig natives lower/url_encode/length cover the strtolower,
urlencode, count Smarty modifiers — no custom duplicates added.

Assisted-by: Claude:claude-sonnet-4-6
Add a custom |urlencode Twig filter backed by PHP urlencode() (space →
'+'),
matching SmartyPage::UrlEncode exactly. Twig's native |url_encode uses
rawurlencode (space → '%20') and is a different filter name, so both
coexist
without conflict. Tests confirm the space → '+' distinction and
byte-identical
output vs SmartyPage::UrlEncode.

Assisted-by: Claude:claude-sonnet-4-6
Copilot AI balanced review requested due to automatic review settings November 7, 2026 20:45
@lucs7
lucs7 marked this pull request as draft November 7, 2026 20:47
@lucs7
lucs7 force-pushed the refactor/twig-migration branch 2 times, most recently from 5beb615 to 885b4ca Compare November 7, 2026 20:50
@lucs7
lucs7 marked this pull request as ready for review November 7, 2026 20:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved template issues omit Thai reminder descriptions, corrupt subscription URLs through missing HTML escaping, and double-escape stored terms text.

4 open issues
What changed in this PR

Migrates LibreBooking’s templates to Twig while retaining Smarty fallback when no Twig counterpart exists. The migration covers page, control, Ajax, administrative, CSV, and localized email rendering.

Changes:

  • Adds a shared renderer abstraction, automatic engine selection, and Twig equivalents of Smarty functions and filters.
  • Ports templates and adapts page, validation, and control integration points.
  • Adds rendering-parity tests, golden fixtures, Twig linting, and editor/cache configuration.

This pull request requires changes. The 16 unresolved moderate findings concern double-escaping in ten resource-status email templates, terms, resource names, and group names; incorrect calendar subscription URL escaping; and a missing description in Thai start reminders.

File Description
tpl/​wait-box.twig Twig loading dialog.
tpl/​tos.twig Terms display; unresolved double-escaping at line 3.
tpl/​ResourceDisplay/​resource-display-not-enabled.twig Disabled resource-display message.
tpl/​ResourceDisplay/​resource-display-instructions.twig Resource-display setup guidance.
tpl/​Reservation/​private-participation.twig Private-participation placeholder.
tpl/​Reservation/​pdf.twig Reservation PDF configuration script.
tpl/​Reservation/​pdf_libraries.twig PDF library includes.
tpl/​Reservation/​attachment-error.twig Attachment error page.
tpl/​Reports/​error.twig Report-generation error message.
tpl/​Reports/​chart.twig Report chart container and assets.
tpl/​maintenance.twig Maintenance page.
tpl/​json_data.twig JSON response output.
tpl/​guest-participation.twig Guest invitation response page.
tpl/​ExternalAuth/​external-login-error.twig External authentication error page.
tpl/​Export/​embedded-calendar-container.twig Embedded calendar wrapper.
tpl/​error.twig General error page.
tpl/​Email/​emailheader.twig Shared email header.
tpl/​Email/​emailfooter.twig Shared email footer.
tpl/​Controls/​Checkbox.twig Checkbox control markup.
tpl/​Calendar/​mycalendar.twig Personal calendar entry template.
tpl/​Calendar/​mycalendar.subscription.twig Personal subscription controls; missing outer HTML escaping at line 9.
tpl/​Calendar/​calendar.twig Resource calendar entry template.
tpl/​Calendar/​calendar.subscription.twig Calendar subscription controls; missing outer HTML escaping at line 5.
tpl/​Ajax/​reservation/​waitlist_added.twig Waitlist confirmation.
tpl/​Ajax/​reservation/​update_successful.twig Reservation update confirmation.
tpl/​Ajax/​reservation/​reservation_error.twig Critical reservation error response.
tpl/​Ajax/​reservation/​reservation_attributes.twig Reservation attribute controls.
tpl/​Ajax/​reservation/​reservation_attributes_print.twig Printable attribute JSON output.
tpl/​Ajax/​reservation/​delete_successful.twig Reservation deletion confirmation.
tpl/​Ajax/​reservation/​delete_failed.twig Reservation deletion errors.
tpl/​Ajax/​reservation/​checkin_successful.twig Check-in/out confirmation.
tpl/​Ajax/​reservation/​checkin_failed.twig Check-in/out errors.
tpl/​Admin/​Users/​import_user_template_csv.twig User-import CSV headers.
tpl/​Admin/​Schedules/​manage_availability.twig Schedule availability display.
tpl/​Admin/​Resources/​show_resource_qr.twig Resource QR page; resource-name double-escaping at line 5.
tpl/​Admin/​Resources/​manage_resources_groups.twig Resource-group display; group-name double-escaping at line 6.
tpl/​Admin/​Resources/​manage_resources_credits.twig Resource credit usage display.
tpl/​Admin/​Resources/​manage_resources_capacity.twig Resource capacity display.
tpl/​Admin/​Resources/​import_resource_template_csv.twig Resource-import CSV headers.
tpl/​Admin/​Reservations/​import_reservations_template_csv.twig Reservation-import CSV headers.
tpl/​Admin/​Groups/​groups_csv.twig Group CSV export.
tpl/​Activation/​activation-sent.twig Activation-email confirmation page.
tpl/​Activation/​activation-error.twig Account activation error page.
tpl/​_render_fallback_probe.tpl Smarty-only fallback test fixture.
tests/​Web/​InstallEntryPointTest.php Accepts either HTML attribute quote style.
tests/​src/​Common/​Templating/​TwigEnvironmentFactoryTest.php Tests Twig autoescaping.
tests/​src/​Common/​Templating/​TemplateRendererTest.php Tests renderer interface methods.
tests/​Pages/​Reports/​CustomCsvTemplateTest.php Tests CSV exports through Twig.
tests/​Pages/​fixtures/​schedule-block-only/​Schedule/​schedule.twig Twig schedule test parent.
tests/​Pages/​fixtures/​schedule-block-only/​Schedule/​schedule.tpl Removes Smarty schedule test parent.
tests/​Infrastructure/​Common/​SmartyRendererTest.php Tests Smarty adapter compatibility.
tests/​Golden/​fixtures/​wait-box.php Loading-dialog parity fixture.
tests/​Golden/​fixtures/​tos.php Terms-page parity fixture.
tests/​Golden/​fixtures/​support-and-credits.php Support-page parity fixture.
tests/​Golden/​fixtures/​search-reservations.php Reservation-search parity fixture.
tests/​Golden/​fixtures/​password.php Password-page parity fixture.
tests/​Golden/​fixtures/​participation.php Participation-page parity fixture.
tests/​Golden/​fixtures/​notification-preferences.php Notification-preferences parity fixture.
tests/​Golden/​fixtures/​myaccount-base.php Shared account-page fixture variables.
tests/​Golden/​fixtures/​javascript-includes.php Basic JavaScript include fixture.
tests/​Golden/​fixtures/​javascript-includes_full.php Full JavaScript include fixture.
tests/​Golden/​fixtures/​guest-participation.php Guest-participation parity fixture.
tests/​Golden/​fixtures/​globalfooter.php Basic footer parity fixture.
tests/​Golden/​fixtures/​globalfooter_full.php Full footer parity fixture.
tests/​Golden/​fixtures/​forgot_pwd.php Password-recovery parity fixture.
tests/​Golden/​fixtures/​dashboard.php Dashboard parity fixture.
tests/​Golden/​fixtures/​.gitkeep Fixture-directory placeholder.
tests/​Golden/​baselines/​wait-box.html Normalized loading-dialog baseline.
tests/​Golden/​baselines/​.gitkeep Baseline-directory placeholder.
phpunit.xml.dist Registers the golden test suite.
Pages/​Ajax/​ResourceDetailsPage.php Uses engine-selecting page display.
Pages/​Admin/​ManageUsersPage.php Renames the page-specific rendering hook.
Pages/​Admin/​GroupAdminManageUsersPage.php Updates the overridden rendering hook.
lib/​Common/​Validators/​PageValidators.php Accepts Twig-backed validation owners.
lib/​Common/​namespace.php Loads renderer integration classes.
lang/​zh_cn/​StartReminderEmail.twig Localized start-reminder email.
lang/​zh_cn/​ResetPassword.twig Localized password-reset email.
lang/​zh_cn/​ReservationAvailable.twig Localized availability email.
lang/​zh_cn/​ReportEmail.twig Localized report email.
lang/​zh_cn/​InviteUser.twig Localized user invitation email.
lang/​zh_cn/​GuestAccountCreation.twig Localized guest-account email.
lang/​zh_cn/​EndReminderEmail.twig Localized end-reminder email.
lang/​zh_cn/​AnnouncementEmail.twig Localized announcement email.
lang/​zh_cn/​AccountCreationForUser.twig Localized account-details email.
lang/​zh_cn/​AccountCreation.twig Localized registration notification.
lang/​vn_vn/​StartReminderEmail.twig Localized start-reminder email.
lang/​vn_vn/​ResetPassword.twig Localized password-reset email.
lang/​vn_vn/​ReservationAvailable.twig Localized availability email.
lang/​vn_vn/​ReportEmail.twig Localized report email.
lang/​vn_vn/​GuestAccountCreation.twig Localized guest-account email.
lang/​vn_vn/​EndReminderEmail.twig Localized end-reminder email.
lang/​vn_vn/​AnnouncementEmail.twig Localized announcement email.
lang/​vn_vn/​AccountCreation.twig Localized registration notification.
lang/​vn_vn/​AccountActivation.twig Localized account activation email.
lang/​th_th/​StartReminderEmail.twig Thai start reminder; incorrect description variable at line 9.
lang/​th_th/​ResetPassword.twig Localized password-reset email.
lang/​th_th/​ReservationAvailable.twig Localized availability email.
lang/​th_th/​ReportEmail.twig Localized report email.
lang/​th_th/​MissedCheckinEmail.twig Localized missed-check-in email.
lang/​th_th/​InviteUser.twig Localized user invitation email.
lang/​th_th/​GuestAccountCreation.twig Localized guest-account email.
lang/​th_th/​EndReminderEmail.twig Localized end-reminder email.
lang/​th_th/​AnnouncementEmail.twig Localized announcement email.
lang/​th_th/​AccountCreationForUser.twig Localized account-details email.
lang/​th_th/​AccountCreation.twig Localized registration notification.
lang/​th_th/​AccountActivation.twig Localized account activation email.
lang/​sv_sv/​ResetPassword.twig Localized password-reset email.
lang/​sv_sv/​AccountActivation.twig Localized account activation email.
lang/​sr_sr/​StartReminderEmail.twig Localized start-reminder email.
lang/​sr_sr/​ResetPassword.twig Localized password-reset email.
lang/​sr_sr/​ReportEmail.twig Localized report email.
lang/​sr_sr/​EndReminderEmail.twig Localized end-reminder email.
lang/​sr_sr/​AccountCreation.twig Localized registration notification.
lang/​sr_sr/​AccountActivation.twig Localized account activation email.
lang/​sk/​StartReminderEmail.twig Localized start-reminder email.
lang/​sk/​ResetPassword.twig Localized password-reset email.
lang/​sk/​ReportEmail.twig Localized report email.
lang/​sk/​EndReminderEmail.twig Localized end-reminder email.
lang/​sk/​AccountActivation.twig Localized account activation email.
lang/​si_si/​StartReminderEmail.twig Localized start-reminder email.
lang/​si_si/​ResetPassword.twig Localized password-reset email.
lang/​si_si/​ReportEmail.twig Localized report email.
lang/​si_si/​EndReminderEmail.twig Localized end-reminder email.
lang/​si_si/​AccountCreation.twig Localized registration notification.
lang/​si_si/​AccountActivation.twig Localized account activation email.
lang/​pt_pt/​StartReminderEmail.twig Localized start-reminder email.
lang/​pt_pt/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​pt_pt/​ResetPassword.twig Localized password-reset email.
lang/​pt_pt/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​pt_pt/​ReservationAvailable.twig Localized availability email.
lang/​pt_pt/​ReportEmail.twig Localized report email.
lang/​pt_pt/​InviteUser.twig Localized user invitation email.
lang/​pt_pt/​GuestAccountCreation.twig Localized guest-account email.
lang/​pt_pt/​EndReminderEmail.twig Localized end-reminder email.
lang/​pt_pt/​AnnouncementEmail.twig Localized announcement email.
lang/​pt_pt/​AccountDeleted.twig Localized account-deletion email.
lang/​pt_pt/​AccountCreationForUser.twig Localized account-details email.
lang/​pt_pt/​AccountCreation.twig Localized registration notification.
lang/​pt_pt/​AccountActivation.twig Localized account activation email.
lang/​pt_br/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 6.
lang/​pt_br/​ResetPassword.twig Localized password-reset email.
lang/​pt_br/​ReservationAvailable.twig Localized availability email.
lang/​pt_br/​ReportEmail.twig Localized report email.
lang/​pt_br/​InviteUser.twig Localized user invitation email.
lang/​pt_br/​GuestAccountCreation.twig Localized guest-account email.
lang/​pt_br/​AnnouncementEmail.twig Localized announcement email.
lang/​pt_br/​AccountDeleted.twig Localized account-deletion email.
lang/​pt_br/​AccountCreationForUser.twig Localized account-details email.
lang/​pt_br/​AccountCreation.twig Localized registration notification.
lang/​pt_br/​AccountActivation.twig Localized account activation email.
lang/​pl/​StartReminderEmail.twig Localized start-reminder email.
lang/​pl/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​pl/​ResetPassword.twig Localized password-reset email.
lang/​pl/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​pl/​ReservationAvailable.twig Localized availability email.
lang/​pl/​ReportEmail.twig Localized report email.
lang/​pl/​InviteUser.twig Localized user invitation email.
lang/​pl/​GuestAccountCreation.twig Localized guest-account email.
lang/​pl/​EndReminderEmail.twig Localized end-reminder email.
lang/​pl/​AnnouncementEmail.twig Localized announcement email.
lang/​pl/​AccountDeleted.twig Localized account-deletion email.
lang/​pl/​AccountCreationForUser.twig Localized account-details email.
lang/​pl/​AccountCreation.twig Localized registration notification.
lang/​pl/​AccountActivation.twig Localized account activation email.
lang/​ja_jp/​StartReminderEmail.twig Localized start-reminder email.
lang/​ja_jp/​ResetPassword.twig Localized password-reset email.
lang/​ja_jp/​ReportEmail.twig Localized report email.
lang/​ja_jp/​EndReminderEmail.twig Localized end-reminder email.
lang/​ja_jp/​AccountCreation.twig Localized registration notification.
lang/​ja_jp/​AccountActivation.twig Localized account activation email.
lang/​it_it/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 2.
lang/​it_it/​ResetPassword.twig Localized password-reset email.
lang/​it_it/​ReservationAvailable.twig Localized availability email.
lang/​it_it/​ReportEmail.twig Localized report email.
lang/​it_it/​InviteUser.twig Localized user invitation email.
lang/​it_it/​GuestAccountCreation.twig Localized guest-account email.
lang/​it_it/​AnnouncementEmail.twig Localized announcement email.
lang/​it_it/​AccountDeleted.twig Localized account-deletion email.
lang/​it_it/​AccountCreationForUser.twig Localized account-details email.
lang/​it_it/​AccountCreation.twig Localized registration notification.
lang/​it_it/​AccountActivation.twig Localized account activation email.
lang/​id_id/​StartReminderEmail.twig Localized start-reminder email.
lang/​id_id/​ResetPassword.twig Localized password-reset email.
lang/​id_id/​ReportEmail.twig Localized report email.
lang/​id_id/​EndReminderEmail.twig Localized end-reminder email.
lang/​id_id/​AccountActivation.twig Localized account activation email.
lang/​hu_hu/​StartReminderEmail.twig Localized start-reminder email.
lang/​hu_hu/​ResetPassword.twig Localized password-reset email.
lang/​hu_hu/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​hu_hu/​ReservationAvailable.twig Localized availability email.
lang/​hu_hu/​ReportEmail.twig Localized report email.
lang/​hu_hu/​InviteUser.twig Localized user invitation email.
lang/​hu_hu/​GuestAccountCreation.twig Localized guest-account email.
lang/​hu_hu/​EndReminderEmail.twig Localized end-reminder email.
lang/​hu_hu/​AnnouncementEmail.twig Localized announcement email.
lang/​hu_hu/​AccountCreationForUser.twig Localized account-details email.
lang/​hu_hu/​AccountCreation.twig Localized registration notification.
lang/​hu_hu/​AccountActivation.twig Localized account activation email.
lang/​hr_hr/​StartReminderEmail.twig Localized start-reminder email.
lang/​hr_hr/​ResetPassword.twig Localized password-reset email.
lang/​hr_hr/​ReportEmail.twig Localized report email.
lang/​hr_hr/​EndReminderEmail.twig Localized end-reminder email.
lang/​hr_hr/​AccountCreation.twig Localized registration notification.
lang/​hr_hr/​AccountActivation.twig Localized account activation email.
lang/​he/​ReportEmail.twig Localized report email.
lang/​he/​AccountActivation.twig Localized account activation email.
lang/​fr_fr/​StartReminderEmail.twig Localized start-reminder email.
lang/​fr_fr/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​fr_fr/​ResetPassword.twig Localized password-reset email.
lang/​fr_fr/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​fr_fr/​ReservationAvailable.twig Localized availability email.
lang/​fr_fr/​ReportEmail.twig Localized report email.
lang/​fr_fr/​InviteUser.twig Localized user invitation email.
lang/​fr_fr/​GuestAccountCreation.twig Localized guest-account email.
lang/​fr_fr/​EndReminderEmail.twig Localized end-reminder email.
lang/​fr_fr/​AnnouncementEmail.twig Localized announcement email.
lang/​fr_fr/​AccountDeleted.twig Localized account-deletion email.
lang/​fr_fr/​AccountCreationForUser.twig Localized account-details email.
lang/​fr_fr/​AccountCreation.twig Localized registration notification.
lang/​fr_fr/​AccountActivation.twig Localized account activation email.
lang/​fi_fi/​StartReminderEmail.twig Localized start-reminder email.
lang/​fi_fi/​ResourceStatusChanged.twig Localized resource-status email.
lang/​fi_fi/​ResetPassword.twig Localized password-reset email.
lang/​fi_fi/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​fi_fi/​ReservationAvailable.twig Localized availability email.
lang/​fi_fi/​ReportEmail.twig Localized report email.
lang/​fi_fi/​InviteUser.twig Localized user invitation email.
lang/​fi_fi/​GuestAccountCreation.twig Localized guest-account email.
lang/​fi_fi/​AnnouncementEmail.twig Localized announcement email.
lang/​fi_fi/​AccountDeleted.twig Localized account-deletion email.
lang/​fi_fi/​AccountCreationForUser.twig Localized account-details email.
lang/​fi_fi/​AccountCreation.twig Localized registration notification.
lang/​fi_fi/​AccountActivation.twig Localized account activation email.
lang/​eu_es/​StartReminderEmail.twig Localized start-reminder email.
lang/​eu_es/​ResetPassword.twig Localized password-reset email.
lang/​eu_es/​ReportEmail.twig Localized report email.
lang/​eu_es/​EndReminderEmail.twig Localized end-reminder email.
lang/​eu_es/​AccountCreation.twig Localized registration notification.
lang/​eu_es/​AccountActivation.twig Localized account activation email.
lang/​es/​StartReminderEmail.twig Localized start-reminder email.
lang/​es/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​es/​ResetPassword.twig Localized password-reset email.
lang/​es/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​es/​ReservationAvailable.twig Localized availability email.
lang/​es/​ReportEmail.twig Localized report email.
lang/​es/​InviteUser.twig Localized user invitation email.
lang/​es/​GuestAccountCreation.twig Localized guest-account email.
lang/​es/​EndReminderEmail.twig Localized end-reminder email.
lang/​es/​AnnouncementEmail.twig Localized announcement email.
lang/​es/​AccountDeleted.twig Localized account-deletion email.
lang/​es/​AccountCreationForUser.twig Localized account-details email.
lang/​es/​AccountCreation.twig Localized registration notification.
lang/​es/​AccountActivation.twig Localized account activation email.
lang/​en_us/​StartReminderEmail.twig English start-reminder email.
lang/​en_us/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​en_us/​ResetPassword.twig English password-reset email.
lang/​en_us/​ReservationSeriesEnding.twig English recurring-series reminder.
lang/​en_us/​ReservationAvailable.twig English availability email.
lang/​en_us/​ReportEmail.twig English report email.
lang/​en_us/​InviteUser.twig English user invitation email.
lang/​en_us/​GuestAccountCreation.twig English guest-account email.
lang/​en_us/​EndReminderEmail.twig English end-reminder email.
lang/​en_us/​AnnouncementEmail.twig English announcement email.
lang/​en_us/​AccountDeleted.twig English account-deletion email.
lang/​en_us/​AccountCreationForUser.twig English account-details email.
lang/​en_us/​AccountCreation.twig English registration notification.
lang/​en_us/​AccountActivation.twig English account activation email.
lang/​el_gr/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​el_gr/​ResetPassword.twig Localized password-reset email.
lang/​el_gr/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​el_gr/​ReservationAvailable.twig Localized availability email.
lang/​el_gr/​ReportEmail.twig Localized report email.
lang/​el_gr/​InviteUser.twig Localized user invitation email.
lang/​el_gr/​GuestAccountCreation.twig Localized guest-account email.
lang/​el_gr/​AnnouncementEmail.twig Localized announcement email.
lang/​el_gr/​AccountDeleted.twig Localized account-deletion email.
lang/​el_gr/​AccountCreationForUser.twig Localized account-details email.
lang/​el_gr/​AccountCreation.twig Localized registration notification.
lang/​el_gr/​AccountActivation.twig Localized account activation email.
lang/​ee_ee/​StartReminderEmail.twig Localized start-reminder email.
lang/​ee_ee/​ResetPassword.twig Localized password-reset email.
lang/​ee_ee/​ReservationAvailable.twig Localized availability email.
lang/​ee_ee/​ReportEmail.twig Localized report email.
lang/​ee_ee/​InviteUser.twig Localized user invitation email.
lang/​ee_ee/​GuestAccountCreation.twig Localized guest-account email.
lang/​ee_ee/​EndReminderEmail.twig Localized end-reminder email.
lang/​ee_ee/​AnnouncementEmail.twig Localized announcement email.
lang/​ee_ee/​AccountCreation.twig Localized registration notification.
lang/​ee_ee/​AccountActivation.twig Localized account activation email.
lang/​du_nl/​StartReminderEmail.twig Localized start-reminder email.
lang/​du_nl/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 3.
lang/​du_nl/​ResetPassword.twig Localized password-reset email.
lang/​du_nl/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​du_nl/​ReservationAvailable.twig Localized availability email.
lang/​du_nl/​ReportEmail.twig Localized report email.
lang/​du_nl/​InviteUser.twig Localized user invitation email.
lang/​du_nl/​GuestAccountCreation.twig Localized guest-account email.
lang/​du_nl/​EndReminderEmail.twig Localized end-reminder email.
lang/​du_nl/​AccountDeleted.twig Localized account-deletion email.
lang/​du_nl/​AccountCreationForUser.twig Localized account-details email.
lang/​du_nl/​AccountCreation.twig Localized registration notification.
lang/​du_nl/​AccountActivation.twig Localized account activation email.
lang/​de_de/​ResourceStatusChanged.twig Resource-status email; message double-escaping at line 7.
lang/​de_de/​ResetPassword.twig Localized password-reset email.
lang/​de_de/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​de_de/​ReservationAvailable.twig Localized availability email.
lang/​de_de/​ReportEmail.twig Localized report email.
lang/​de_de/​InviteUser.twig Localized user invitation email.
lang/​de_de/​GuestAccountCreation.twig Localized guest-account email.
lang/​de_de/​EndReminderEmail.twig Localized end-reminder email.
lang/​de_de/​AccountDeleted.twig Localized account-deletion email.
lang/​de_de/​AccountCreationForUser.twig Localized account-details email.
lang/​de_de/​AccountCreation.twig Localized registration notification.
lang/​de_de/​AccountActivation.twig Localized account activation email.
lang/​da_da/​StartReminderEmail.twig Localized start-reminder email.
lang/​da_da/​ResetPassword.twig Localized password-reset email.
lang/​da_da/​ReservationSeriesEnding.twig Localized recurring-series reminder.
lang/​da_da/​ReservationAvailable.twig Localized availability email.
lang/​da_da/​ReportEmail.twig Localized report email.
lang/​da_da/​MissedCheckinEmail.twig Localized missed-check-in email.
lang/​da_da/​InviteUser.twig Localized user invitation email.
lang/​da_da/​GuestAccountCreation.twig Localized guest-account email.
lang/​da_da/​EndReminderEmail.twig Localized end-reminder email.
lang/​da_da/​AnnouncementEmail.twig Localized announcement email.
lang/​da_da/​AccountDeleted.twig Localized account-deletion email.
lang/​da_da/​AccountCreationForUser.twig Localized account-details email.
lang/​da_da/​AccountCreation.twig Localized registration notification.
lang/​da_da/​AccountActivation.twig Localized account activation email.
lang/​cz/​StartReminderEmail.twig Localized start-reminder email.
lang/​cz/​ResetPassword.twig Localized password-reset email.
lang/​cz/​ReportEmail.twig Localized report email.
lang/​cz/​EndReminderEmail.twig Localized end-reminder email.
lang/​cz/​AccountCreation.twig Localized registration notification.
lang/​cz/​AccountActivation.twig Localized account activation email.
lang/​bg_bg/​ResetPassword.twig Localized password-reset email.
lang/​bg_bg/​ReportEmail.twig Localized report email.
lang/​bg_bg/​AccountActivation.twig Localized account activation email.
Controls/​DatePickerSetupControl.php Accepts shared renderer instances.
Controls/​Dashboard/​ResourceAvailabilityControl.php Accepts renderer-backed dashboard rendering.
Controls/​Dashboard/​PastReservations.php Updates reservation dashboard constructor types.
Controls/​Dashboard/​DashboardItem.php Broadens the dashboard renderer contract.
Controls/​Dashboard/​AnnouncementsControl.php Accepts renderer-backed announcement rendering.
Controls/​AttributeControl.php Accepts renderer-backed attribute rendering.
.vscode/​extensions.json Recommends Twig editor support.
.gitignore Ignores Twig and formatter caches.

Review effort: Balanced

Comment thread lang/th_th/StartReminderEmail.twig Resolved
Comment thread tpl/Calendar/calendar.subscription.twig Resolved
Comment thread tpl/tos.twig Resolved
@lucs7
lucs7 force-pushed the refactor/twig-migration branch from 885b4ca to b3b6043 Compare November 7, 2026 21:03
lucs7 added 6 commits November 7, 2026 22:36
Add Group A HTML-emitting Twig functions to LibreBookingExtension:
cancel_button, update_button, add_button, delete_button, reset_button,
filter_button, ok_button, showhide_icon, indicator. Each function
accepts an explicit `attributes` array for pass-through HTML attributes,
reproducing SmartyPage::AppendAttributes output (insertion order,
key="value" format). Parity tests compare Twig output to live
SmartyPage method output via output-buffer capture.

Assisted-by: Claude:claude-sonnet-4-6
Add Group B Twig functions to LibreBookingExtension: validator,
async_validator, validation_group, textbox, object_html_options,
setfocus, formname, csrf_token, read_only_attribute.

Wire LibreBookingExtension to receive the TwigRenderer as an optional
third constructor param so validator/textbox can access live
PageValidators and template vars. TwigRenderer passes $this when
constructing the extension.

Assisted-by: Claude:claude-sonnet-4-6
Add Group C to LibreBookingExtension: html_link, add_querystring,
sort_column, resource_image, fullname, formatdate/format_date,
formatcurrency, js_array, linebreak, flush. All functions produce
byte-identical output to the corresponding SmartyPage methods. Echo-only
SmartyPage methods (sort_column, formatcurrency, flush) now return their
string instead. Group B ServiceLocator tests extracted to their own file
so PHPUnit discovers them correctly.

Assisted-by: Claude:claude-sonnet-4-6
Adds jsfile, cssfile, vendor_js, vendor_css, datatable and
datatablefilter to LibreBookingExtension (Group D). Reproduces
GetDefaultDataTablePageSize and BuildDataTableLengthMenu as private
helpers on the extension so output is byte-identical to SmartyPage.
23 parity tests in LibreBookingExtensionGroupDTest confirm this.

Assisted-by: Claude:claude-sonnet-4-6
Add EngineSelector (src/Common/Templating/EngineSelector.php) with a
static twigNameFor() that maps a .tpl name to .twig and probes the
given search dirs. Wire it into Page::Display(): when a .twig exists
the page constructs a TwigRenderer, transfers all assigned vars and the
live PageValidators, and renders via Twig; otherwise falls back to
Smarty unchanged. A protected getTemplateSearchDirs() hook makes the
logic testable without touching real template dirs.

Assisted-by: Claude:claude-sonnet-4-6
lucs7 added 19 commits November 7, 2026 23:27
Wire the email pipeline to TwigRenderer with Smarty fallback for
not-yet-migrated templates:

- TwigRenderer::fetch() now does engine-select: if a .twig counterpart
  of the requested .tpl exists in the Twig loader, render via Twig;
  otherwise delegate to the smartyFallback.

- TwigRenderer::assign() now propagates variables to smartyFallback so
  Smarty fallback paths see the same data as the Twig path.

- TwigRenderer::fetchLocalized() implements the full
  SmartyPage::FetchLocalized
  resolution logic (language code resolution, -custom override,
  enforce-custom
  default-language fallback, en_us fallback), then engine-selects: .twig
  at the
  resolved path uses Twig; otherwise delegates to smartyFallback.

- EmailMessage::__construct() builds a TwigRenderer as the primary
  renderer
  instead of SmartyRenderer. The $this->email BC alias is preserved (a
  separate
  SmartyPage backed by the same Resources) for subclasses that call it
  directly
  (e.g. ReportEmailMessage::__construct).

- Set(), FetchTemplate(), Translate(), Charset() now route through
  $this->renderer
  (TwigRenderer). Translate() uses Resources::GetString() directly,
  removing the
  SmartyTranslate dependency.

- Migrate tpl/Email/emailheader.tpl and emailfooter.tpl to .twig
  counterparts
  (original .tpl files retained). The header/footer are now rendered via
  Twig
  while email body templates (lang/xx/*.tpl) continue to fall through to
  Smarty
  until individually migrated.

Tests:
- TwigRendererTest: add fetch() engine-select and fetchLocalized()
  fallback tests
- EmailMessageRendererTest: verify TwigRenderer is primary, BC alias is
  SmartyPage,
  variable propagation and Charset() delegation
- tests/Golden/EmailGoldenTest.php: parity tests for header/footer and
  smoke test
  for unmigrated body fallback

Assisted-by: Claude:claude-sonnet-4-6
Add .twig counterparts for all 20 lang/en_us/*.tpl email bodies so that
TwigRenderer::fetchLocalized now selects the Twig engine for en_us.
Original .tpl files are kept; the 27 other language directories continue
using the Smarty fallback (Phase-5 backlog).

Key conversion decisions:
- User-data fields emitted raw in Smarty kept |raw for 1:1 parity
  (security-backlog: review XSS hardening post-migration)
- Password (was |escape:'html' in Smarty) uses Twig autoescape naturally
- Rich-text fields (AnnouncementText, resource.description,
  resource.notes,
  Message) go through |sanitize_rich_text|url2link|nl2br|raw pipeline
- constant('InvitationAction::Decline') replaces bare PHP constant refs
- loop.last replaces $smarty.foreach.*.last
- control('AttributeControl', {...}) replaces {control type="..."} tag

Replace vacuous testFetchLocalizedSelectsTwigWhenTwigFileExists in
TwigRendererTest with a genuine test that calls fetchLocalized on a real
on-disk .twig file and asserts Twig output matches Smarty output.

Add EmailBodiesGoldenTest: live same-process Smarty-vs-Twig comparison
for all 20 bodies covering every conditional branch (39 test methods).
323 golden tests pass; 2243 full-suite tests pass; phpstan +
phpstan_next
both clean.

Assisted-by: Claude:claude-sonnet-4-6
Create attribute-list.twig and manage_attributes.twig alongside their
kept .tpl counterparts. Add AdminAttributesGoldenTest.php covering both
templates with 11 tests (attribute-list: 8 branches; manage_attributes:
full-page parity + 2 structural checks). Divergences noted:
- JS array joins (entityIds, entityDescriptions) use |raw in script
  context to match Smarty's unescaped join separator behaviour.
- adminOnly rendered via {{ attribute.AdminOnly() }} (returns int 0/1)
  to match Smarty's { $attribute->AdminOnly() } output.
- datatable() called with tableId conditional to reproduce Smarty's
  unset-variable empty-string behaviour when Attributes list is empty.

Assisted-by: Claude:claude-sonnet-4-6
Add Twig counterparts for all three Admin/Import templates:
- import.twig: hub page with ICS and Quartzy import links (full parity)
- ics_import.twig: ICS upload form with CSRF, async validators, JS
  handler
- quartzy_import.twig: Quartzy ZIP upload form with bookings checkbox;
  adds rel="noopener noreferrer" to target="_blank" external link
  per security policy (structural divergence from Smarty, intentional)

Add AdminImportGoldenTest covering all three templates with FakeServer
CSRF pinning; import/ics_import assert full parity; quartzy_import uses
structural assertions for the noopener divergence.

Assisted-by: Claude:claude-sonnet-4-6
Translate manage_payments.tpl and transaction_log.tpl to Twig, keeping
the original Smarty templates in place.

Accepted structural divergence (documented in golden tests):
  - {update_button submit=true} in Smarty emits a stray `submit="1"`
    HTML attribute via GetButtonAttributes() → AppendAttributes(); the
    Twig update_button() function treats `submit` as a named boolean and
    does not forward it as an HTML attribute. Twig output is
    authoritative.

Golden test strategy:
  - transaction_log: assertParity (Smarty ≡ Twig, no buttons → no
    divergence)
  - manage_payments: assertTwigBaseline (Twig output stored as
    authoritative
    baseline, divergence documented in each test's docblock)

Assisted-by: Claude:claude-sonnet-4-6
Replace stored-baseline and structural-only tests with LIVE
Smarty-vs-Twig
assertSame comparisons for the Admin Import, Payments, and Blackouts
templates.

ch2 Import (quartzy_import):
- Remove stray rel="noopener noreferrer" from quartzy_import.twig
  (Smarty .tpl
  does not have it; rel-hardening is a BACKLOG item, not applied
  inline).
- Replace 4 structural assertTwigContains tests with a single
  assertParity.

ch3 Payments (manage_payments):
- Replace all 6 assertTwigBaseline tests with live assertPaymentsParity.
- Strip stray submit="1" (Smarty AppendAttributes leak) from Smarty
  output before
  comparison; strip DataTable <script> block from both outputs to handle
  the
  Smarty undefined-$tableId path when PaymentsEnabled=false.
- Delete the 6 stored baseline HTML files.

ch4 Blackouts:
- manage_blackouts_response (5 tests) and manage_blackouts_edit (2
  tests):
  full assertParity, no accepted divergences.
- manage_blackouts main page (3 tests): assertParity after stripping
  data-default="H:00" from both outputs (clock-based, not honoured by
  Date::_SetNow() in either engine).
- Fix manage_blackouts_edit.twig checked='checked' quote style to match
  Smarty .tpl output (both are spec-identical HTML).

Assisted-by: Claude:claude-sonnet-4-6
Add groups_csv.twig and manage_groups.twig alongside their Smarty
originals.  Covers all template variables, control flow branches,
formname/button/datatable/html_link helpers, and PHP class constants.

Add AdminGroupsGoldenTest: 17 live Smarty-vs-Twig parity tests using
identical fixtures, HtmlNormalizer normalization, and assertSame.

Accepted divergence (a): Smarty {update_button submit="true"} emits
a stray submit="..." HTML attribute; Twig omits it.  Stripped from
both outputs before comparison (3 occurrences: permissionsForm,
resourceAdminForm, scheduleAdminForm) — rest of each button is
Smarty-parity-verified.

Assisted-by: Claude:claude-sonnet-4-6
Adds 4 Twig templates alongside the existing .tpl files plus a
golden test confirming Smarty/Twig output parity:

- import_reservations_template_csv.twig: full parity
- reservations_csv.twig: full parity
- manage_reservation_colors.twig: full parity (duplicates the
  harmless duplicate method="post" attribute from the .tpl source)
- manage_reservations.twig: parity after stripping the spurious
  submit="1" attribute that Smarty emits via AppendAttributes when
  {update_button submit=true} is used

Accepted divergences documented in test:
- update_button(submit=true): Twig does not forward the `submit`
  param as an HTML attribute; Smarty AppendAttributes does.
- escapequotes vs escape:'quotes': Twig escapes both " (&quot;)
  and ' (&LibreBooking#39;); Smarty escapes only ' with backslash. Tests use
  quote-free labels to stay in the parity zone.

Assisted-by: Claude:claude-sonnet-4-6
Migrate credit_log, import_user_template_csv, manage_users,
user-update, and users_csv templates to Twig alongside existing
Smarty .tpl files. Add AdminUsersGoldenTest with live Smarty-vs-Twig
parity assertions.

Assisted-by: Claude:claude-sonnet-4-6
Migrate four Admin root-level Smarty templates to Twig (additive only;
original .tpl files kept intact):

- tpl/Admin/InlineAttributeEdit.twig — reusable partial for inline
  editable custom attributes (text/textarea/select/checkbox/datetime).
  Includes full inline JS flatpickr integration for datetime type.
- tpl/Admin/manage_accessories.twig — CRUD page with add/edit/delete
  modals and resource-assignment modal with checkbox table.
- tpl/Admin/manage_announcements.twig — Announcements CRUD with
  Trumbowyg rich-text editor and Select2 multi-select for groups.
  Pages::NameFromId() static call replaced with an inline lookup hash
  (produces identical output for all valid DisplayPage values 1 and 5).
- tpl/Admin/manage_quotas.twig — Quota CRUD with {capture} → {% set %}
  HTML-snippet args passed to translate('QuotaConfiguration', [...]).

Golden test (tests/Golden/AdminRootGoldenTest.php):
- 18 live Smarty-vs-Twig parity assertions via
  HtmlNormalizer::normalize.
- InlineAttributeEdit: all 5 attribute types + not-applicable branch.
- Accessories: empty + rows (null quantity / unlimited symbol &infin;).
- Announcements: empty + with/without canEmail rows.
- Quotas: empty + all-day/everyday + timed/specific-days variants.
- Structural assertTwigContains checks for key elements.
- Accepted divergences documented:
  (a) update_button submit="true" stray attr — stripped from both;
      form="accessoryResourcesForm" attr added to Twig to match Smarty.
  (b) InlineAttributeEdit CHECKBOX trailing space before > — matched.
  (c) data-default clock values stripped from both in quotas parity.

Security gaps noted (not fixed — additive only):
- manage_accessories.twig: resource checkbox values use raw
  user-supplied
  IDs in HTML attributes; no rel=noopener on any external link.
- manage_announcements.twig: rich-text via sanitize_rich_text (existing
  policy); no additional hardening added.

Assisted-by: Claude:claude-sonnet-4-6
Add Twig counterparts for all 4 Admin/Schedules templates
(manage_availability, manage_peak_times, manage_schedules,
view_schedules) and validate them with 25 live Smarty-vs-Twig
golden tests.

Bug fixes found during testing:
- manage_peak_times.twig: boolean data attributes used `? 1 : 0`
  instead of `? 1 : ''`, causing false to render as "0" vs ""
- manage_schedules.twig: `{capture}` vs `{assign}` scope —
  daysVisible modal title input rendered wrong var reference
- manage_schedules.twig: `indicator()` missing the id argument
- manage_schedules.twig: `.row` div ids in changeLayout modal
  must reference `{{ id }}` (not a literal day index) to match
  Smarty {function} scope inheritance behaviour
- manage_schedules.twig: `|e('js')` replaced with `|escape_js`
  throughout (14 occurrences) — built-in escaper Unicode-escapes
  spaces, custom filter matches Smarty's output

Accepted divergences handled in the golden strip function:
- submit="1" stray attribute from {update_button}
- data-default clock-based timepicker values
- flatpickr random id/for attributes
- .row div ids inside changeLayout modal
- Whitespace before comma in LayoutDescription translate args
  (Smarty {capture} carries trailing whitespace that normalises
  to a space before the comma separator)

Assisted-by: Claude:claude-sonnet-4-6
Add Twig counterparts alongside the kept Smarty originals for the
Admin/Resources sub-panel partials (manage_resources_{access,capacity,
credits,duration,group_permissions,groups,public,user_permissions}) and
for view_resources, manage_resource_{groups,menu,status,types},
show_resource_qr, resources_csv and import_resource_template_csv.

Key conversion patterns:
- CSV templates use {% autoescape false %} + |replace({("'"): "\\'"})
  to replicate Smarty's |escape:'quotes' backslash escaping
- manage_resource_status: {function}/{call} -> {% macro %}/{{ _self.* }}
- view_resources: Twig whitespace control ({%- -%}) for the
  descriptionValue div to match Smarty's {strip} block

Golden tests: AdminResourcesSubPanelsGoldenTest and
AdminResourcesListGoldenTest. Translate args with integer 0 values are
passed as array [value] to bypass PHP empty() short-circuit. The public
panel href & vs &amp; is an accepted divergence stripped in
assertPublicParity; assertParityHrefAmp also normalises &LibreBooking#39; and
&LibreBooking#39;.

Closes: LibreBooking#58

Assisted-by: Claude:claude-sonnet-4-6
Migrate tpl/Admin/Resources/manage_resources.tpl (2432 lines) to Twig.
Add 21-test golden parity suite covering empty list, filter form, single
and multiple resources, admin groups, custom attributes, status reasons,
credits, resource types, contact-is-user, bulk dialogs, and JS block
structural coverage.

Bugs fixed in .twig:
- update_button submit='1' was inside attributes dict instead of as the
  named $submit parameter; fixed for bulk-update and resource-group
  dialogs.
- Description/notes editable divs collapsed to single line to match
  Smarty
  {strip} whitespace removal between tag attributes.

Test fixture makeAdminGroup updated to expose public $Id/$Name
properties
mirroring GroupItemView, so Smarty property access matches Twig method
calls.

Assisted-by: Claude:claude-sonnet-4-6
Add a .twig sibling for every translated email .tpl (391 files in
all non-en_us languages), using the same conversions as the existing
en_us templates. TwigRenderer::fetchLocalized() already prefers a
.twig next to the resolved .tpl, so the Smarty files stay as the
anchor for language resolution and custom-template lookup.

Assisted-by: Claude:claude-sonnet-5-5
The rebase onto develop brought .tpl changes that the Twig
templates did not have, because Twig is selected whenever a .twig
exists.

- add the escape_csv filter and the fullname csv option to the Twig
  extension, matching SmartyPage
- escape CSV values with escape_csv in the users, groups,
  reservations, resources and report CSV templates
- drop the trailing comma from report CSV rows
- move the comma outside the quotes for user CSV attribute headers
- add the max concurrent reservations column to the resources CSV
- skip days without periods in the wide schedule view
- render the subscribe-to-calendar control as a button
- use the SubscribeToCalendar title on the public resource links

Render the CSV and wide schedule template tests through Twig, as
production does, and accept either quote style for the install
page marker.

Assisted-by: Claude:claude-sonnet-5-5
EmailLocalizedGoldenTest renders every lang/<code>/<Name>.tpl that has
a .twig next to it through Smarty and Twig with six fixture variants
(populated, empty and each invitation action) and compares the
normalized output. The fixtures contain <, & and " so escaping is
exercised.

Twig HTML-escapes user-typed text that Smarty prints raw. That is the
only accepted difference and it is an explicit allowlist: Description,
DeleteReason and Message where the template uses |nl2br, and
participant/invitee names. Any other difference fails.

The Thai StartReminderEmail template cannot compile in Smarty (its
variable name is not a valid Smarty identifier), so only the Twig
render is asserted for it, and a second test fails once Smarty can
compile it.

GoldenCoverageTest fails when a .tpl with a .twig counterpart is not
mentioned in any tests/Golden test.

Assisted-by: Claude:claude-sonnet-5-5
@lucs7
lucs7 force-pushed the refactor/twig-migration branch from 28e077e to 74fb92b Compare November 7, 2026 22:28
@lucs7
lucs7 marked this pull request as draft November 7, 2026 22:55
In debug mode, ?template_engine=smarty renders pages, AJAX fragments,
controls, partials and emails with the Smarty templates, and
?template_engine=twig returns to Twig. The choice is stored in a
cookie so follow-up and AJAX requests use the same engine. Request
data is only read, never modified, so URLs rebuilt from the current
query string and the route parameter validation keep working.

The footer of each engine names itself ("Template engine: Smarty" in
globalfooter.tpl, "Twig" in globalfooter.twig), and a
X-Template-Engine response header names the engine that rendered the
page. All of this is ignored unless app.debug is enabled.

Assisted-by: Claude:claude-opus-5-5

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The cross-engine rendering and escaping migration needs representative browser and email validation, plus end-to-end checks of interactive flows and customized-template fallback, beyond golden-fixture parity to assess compatibility.

No open issues 🎉

4 resolved since last review

Review effort: Balanced

@lucs7
lucs7 marked this pull request as ready for review November 8, 2026 10:35
@lucs7
lucs7 changed the base branch from develop to refactor/twig November 8, 2026 10:35
@lucs7

lucs7 commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

I addressed the earlier copilot review and added the engine switch option

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants