[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-commits
Subject:    [kcalutils] /: Port part of KCalUtils to Grantlee templates
From:       Dan_Vrátil <dvratil () redhat ! com>
Date:       2015-10-06 10:36:09
Message-ID: E1ZjPbN-0003Cm-In () scm ! kde ! org
[Download RAW message or body]

Git commit c0c318942bc70714091b59fa078ee62332e8a60f by Dan Vrátil.
Committed on 06/10/2015 at 10:34.
Pushed by dvratil into branch 'master'.

Port part of KCalUtils to Grantlee templates

Instead of assembling the HTML by hand in C++ code, we use Grantlee templates
for presentation, and only prepare the data in C++ code. This commit only ports
the event, tasks, journal and freebusy views to Grantlee templates. The ITIP
HTML code is still assembled in C++, I want to wait with that until Sandro is
finished with his work on ObjectTreeParser refactoring in kdepim.git to see what
the requierements will be.

This code also includes a bunch of useful Grantlee plugins and tools that we
 might want to eventually move to some other repository so it can be used
from other projects as well.

REVIEW: 125331

M  +2    -0    CMakeLists.txt
A  +6    -0    Messages.sh
M  +27   -3    autotests/CMakeLists.txt
A  +4    -0    autotests/data/broken-template.html
A  +74   -0    autotests/data/event-1.html
A  +22   -0    autotests/data/event-1.ical
A  +116  -0    autotests/data/event-2.html
A  +102  -0    autotests/data/event-2.ical
A  +65   -0    autotests/data/event-allday-multiday.html
A  +19   -0    autotests/data/event-allday-multiday.ical
A  +65   -0    autotests/data/event-allday.html
A  +19   -0    autotests/data/event-allday.ical
A  +73   -0    autotests/data/event-exception-single.html
A  +19   -0    autotests/data/event-exception-single.ical
A  +73   -0    autotests/data/event-exception-thisandfuture.html
A  +19   -0    autotests/data/event-exception-thisandfuture.ical
A  +74   -0    autotests/data/event-multiday.html
A  +20   -0    autotests/data/event-multiday.ical
A  +73   -0    autotests/data/event-recurrence-single.out.html
A  +73   -0    autotests/data/event-recurrence-thisandfuture.out.html
A  +31   -0    autotests/data/freebusy-1.html
A  +17   -0    autotests/data/freebusy-1.ical
A  +58   -0    autotests/data/journal-1.html
A  +82   -0    autotests/data/journal-1.ical
A  +113  -0    autotests/data/todo-1.html
A  +30   -0    autotests/data/todo-1.ical
A  +5    -0    autotests/test_config.h.cmake
M  +234  -0    autotests/testincidenceformatter.cpp
M  +26   -0    autotests/testincidenceformatter.h
A  +54   -0    scripts/extract_strings_ki18n.py
A  +365  -0    scripts/grantlee_strings_extractor.py
M  +5    -0    src/CMakeLists.txt
D  +0    -3    src/Messages.sh
A  +19   -0    src/grantlee_plugin/CMakeLists.txt
A  +124  -0    src/grantlee_plugin/datetimefilters.cpp     [License: LGPL (v2.1+)]
A  +58   -0    src/grantlee_plugin/datetimefilters.h     [License: LGPL (v2.1+)]
A  +144  -0    src/grantlee_plugin/icon.cpp     [License: LGPL (v2.1+)]
A  +79   -0    src/grantlee_plugin/icon.h     [License: LGPL (v2.1+)]
A  +54   -0    src/grantlee_plugin/kcalendargrantleeplugin.cpp     [License: LGPL \
(v2.1+)] A  +40   -0    src/grantlee_plugin/kcalendargrantleeplugin.h     [License: \
LGPL (v2.1+)] A  +112  -0    src/grantleeki18nlocalizer.cpp     [License: LGPL \
(v2.1+)] A  +54   -0    src/grantleeki18nlocalizer_p.h     [License: LGPL (v2.1+)]
A  +126  -0    src/grantleetemplatemanager.cpp     [License: LGPL (v2.1+)]
A  +63   -0    src/grantleetemplatemanager_p.h     [License: LGPL (v2.1+)]
M  +214  -488  src/incidenceformatter.cpp
A  +10   -0    templates/CMakeLists.txt
A  +28   -0    templates/attendee_row.html
A  +198  -0    templates/event.html
A  +28   -0    templates/freebusy.html
A  +24   -0    templates/incidence_header.html
A  +48   -0    templates/journal.html
A  +22   -0    templates/template_base.html
A  +203  -0    templates/todo.html

http://commits.kde.org/kcalutils/c0c318942bc70714091b59fa078ee62332e8a60f

diff --git a/CMakeLists.txt b/CMakeLists.txt
index d145b6a..bd4109e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -32,6 +32,7 @@ find_package(KF5Config ${KF5_VERSION} CONFIG REQUIRED)
 find_package(KF5I18n ${KF5_VERSION} CONFIG REQUIRED)
 find_package(KF5KDELibs4Support ${KF5_VERSION} CONFIG REQUIRED)
 find_package(KF5Codecs ${KF5_VERSION} CONFIG REQUIRED)
+find_package(Grantlee5 "5.0" CONFIG REQUIRED)
 
 find_package(KF5CalendarCore ${CALENDARCORE_LIB_VERSION} CONFIG REQUIRED)
 find_package(KF5IdentityManagement ${IDENTITYMANAGER_LIB_VERSION} CONFIG REQUIRED)
@@ -41,6 +42,7 @@ add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII")
 
 ########### Targets ###########
 add_subdirectory(src)
+add_subdirectory(templates)
 
 if(BUILD_TESTING)
     add_subdirectory(autotests)
diff --git a/Messages.sh b/Messages.sh
new file mode 100644
index 0000000..8864580
--- /dev/null
+++ b/Messages.sh
@@ -0,0 +1,6 @@
+#! /bin/sh
+scripts/extract_strings_ki18n.py `find templates -name \*.html` >> html.cpp
+$EXTRACTRC *.kcfg *.ui >> rc.cpp
+$XGETTEXT rc.cpp html.cpp src/*.cpp src/*.h -o $podir/libkcalutils5.pot
+rm -f rc.cpp html.cpp
+
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt
index a7ce2f1..6fa5c28 100644
--- a/autotests/CMakeLists.txt
+++ b/autotests/CMakeLists.txt
@@ -5,6 +5,30 @@ include(ECMAddTests)
 set(QT_REQUIRED_VERSION "5.2.0")
 find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED COMPONENTS Test)
 
-ecm_add_tests(testdndfactory.cpp testincidenceformatter.cpp teststringify.cpp 
-              NAME_PREFIX "kcalutils-"
-              LINK_LIBRARIES KF5CalendarUtils Qt5::Test)
+set(TEST_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data")
+set(TEST_TEMPLATE_PATH "${CMAKE_SOURCE_DIR}/templates")
+set(TEST_PLUGIN_PATH "${CMAKE_BINARY_DIR}/grantlee")
+configure_file(test_config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/test_config.h @ONLY)
+
+include_directories(${CMAKE_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}/src)
+
+ecm_add_tests(testdndfactory.cpp teststringify.cpp
+    NAME_PREFIX "kcalutils-"
+    LINK_LIBRARIES KF5CalendarUtils Qt5::Test
+)
+
+
+ecm_add_test(testincidenceformatter.cpp
+        ${CMAKE_SOURCE_DIR}/src/incidenceformatter.cpp
+        ${CMAKE_SOURCE_DIR}/src/grantleetemplatemanager.cpp
+        ${CMAKE_SOURCE_DIR}/src/grantleeki18nlocalizer.cpp
+        ${CMAKE_SOURCE_DIR}/src/stringify.cpp
+        ${CMAKE_BINARY_DIR}/src/kcalutils_debug.cpp
+    TEST_NAME "testincidenceformatter"
+    NAME_PREFIX "kcalutils-"
+    LINK_LIBRARIES Qt5::Core Qt5::Test KF5::CalendarCore KF5::I18n \
KF5::IdentityManagement Grantlee5::Templates +    
+)
+
+# Make sure that dates are formatted in C locale
+set_tests_properties(kcalutils-testincidenceformatter PROPERTIES ENVIRONMENT \
                "LC_ALL=C")
diff --git a/autotests/data/broken-template.html \
b/autotests/data/broken-template.html new file mode 100644
index 0000000..be733eb
--- /dev/null
+++ b/autotests/data/broken-template.html
@@ -0,0 +1,4 @@
+<b>This is a template, and it's broken</b>
+
+{% if shouldBreak %}
+    <p>Why, you ask?</p>
diff --git a/autotests/data/event-1.html b/autotests/data/event-1.html
new file mode 100644
index 0000000..70fb30e
--- /dev/null
+++ b/autotests/data/event-1.html
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +          <img src="file:edit-redo.svg" align="top" \
height="16" width="16" alt="Recurring incidence" title="Recurring incidence" /> +     \
</td> +        <td>
+          <b>
+            <u>20. Mai 2005, 19-20 Uhr, alle 3 Monate am -2. Fr, 17 mal</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Friday, 20 May 2005</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>17:00:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>1 hour </td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Recurs every 3 months on the 2nd Last Friday until 2009-05-22 17:00 (17 \
occurrences)</td> +      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Friday, 20 May 2005 10:58:56 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-1.ical b/autotests/data/event-1.ical
new file mode 100644
index 0000000..4ba587c
--- /dev/null
+++ b/autotests/data/event-1.ical
@@ -0,0 +1,22 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+X-LibKCal-Testsuite-OutTZ:Europe/Vienna
+BEGIN:VEVENT
+DTSTAMP:20050520T105856Z
+ORGANIZER;CN=Reinhold Kainhofer:MAILTO:reinhold@kainhofer.com
+CREATED:20050520T105219Z
+UID:KOrganizer-45214176.303
+SEQUENCE:2
+LAST-MODIFIED:20050520T105815Z
+SUMMARY:20. Mai 2005\, 19-20 Uhr\, alle 3 Monate am -2. Fr\, 17 mal
+CLASS:PUBLIC
+PRIORITY:5
+RRULE:FREQ=MONTHLY;COUNT=17;INTERVAL=3;BYDAY=-2FR
+DTSTART:20050520T170000Z
+DTEND:20050520T180000Z
+TRANSP:OPAQUE
+END:VEVENT
+
+END:VCALENDAR
+
diff --git a/autotests/data/event-2.html b/autotests/data/event-2.html
new file mode 100644
index 0000000..500b38a
--- /dev/null
+++ b/autotests/data/event-2.html
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +          <img \
src="file:preferences-desktop-notification-bell.svg" align="top" height="16" \
width="16" alt="Incidence with a reminder" title="Incidence with a reminder" /> +     \
<img src="file:edit-redo.svg" align="top" height="16" width="16" alt="Recurring \
incidence" title="Recurring incidence" /> +        </td>
+        <td>
+          <b>
+            <u>Plánovací meeting</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+      <tr>
+        <th>Location:</th>
+        <td>Zasedačka číslo 3</td>
+      </tr>
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Wednesday, 30 September 2015</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>03:00:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>1 hour 30 minutes</td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Recurs every 2 weeks on Wed (excluding 1 day)</td>
+      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+      <tr>
+        <th>Reminder:</th>
+        <td>15 minutes before the start</td>
+      </tr>
+<!-- Organizer -->
+      <tr>
+        <th>Organizer:</th>
+        <td><img src="file:meeting-organizer.png" align="top" height="16" width="16" \
alt="meeting-organizer" title="" /> +Daniel Vrátil
+    <a href="mailto:Daniel Vrátil %3Cdvratil@kde.org%3E"><img \
src="file:mail-message-new.svg" align="top" height="16" width="16" alt="Send email" \
title="Send email" /></a> +
+        </td>
+      </tr>
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+      <tr>
+        <th>Required Participants:</th>
+        <td>
+          <img src="file:dialog-ok-apply.svg" align="top" height="16" width="16" \
alt="Accepted" title="Accepted" /> +          <a href="uid:62645952">Volker Krause
+    </a>
+          <a href="mailto:Volker Krause %3Cvkrause@kde.org%3E">
+            <img src="file:mail-message-new.svg" align="top" height="16" width="16" \
alt="Send email" title="Send email" /> +          </a>
+          <br />
+          <img src="file:help-about.svg" align="top" height="16" width="16" \
alt="Needs action" title="Needs action" /> +          <a \
href="uid:35926528">Christian Mollekopf +    </a>
+          <a href="mailto:Christian Mollekopf %3Ccmollekopf@gmail.com%3E">
+            <img src="file:mail-message-new.svg" align="top" height="16" width="16" \
alt="Send email" title="Send email" /> +          </a>
+          <br />
+          <img src="file:mail-forward.svg" align="top" height="16" width="16" \
alt="Delegated" title="Delegated" /> +          <a href="uid:64325328">Sandro Knauß
+    </a>
+          <a href="mailto:Sandro Knauß %3Csknauss@kde.org%3E">
+            <img src="file:mail-message-new.svg" align="top" height="16" width="16" \
alt="Send email" title="Send email" /> +          </a>
+        </td>
+      </tr>
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Wednesday, 30 September 2015 09:03:51 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-2.ical b/autotests/data/event-2.ical
new file mode 100644
index 0000000..62971ee
--- /dev/null
+++ b/autotests/data/event-2.ical
@@ -0,0 +1,102 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 5.0.44 pre//EN
+VERSION:2.0
+X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Prague
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+005744
+TZOFFSETTO:+0100
+DTSTART:19011213T214336
+RDATE;VALUE=DATE-TIME:19011213T214336
+END:STANDARD
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19160430T230000
+RDATE;VALUE=DATE-TIME:19160430T230000
+RDATE;VALUE=DATE-TIME:19170416T020000
+RDATE;VALUE=DATE-TIME:19180415T020000
+RDATE;VALUE=DATE-TIME:19400401T020000
+RDATE;VALUE=DATE-TIME:19430329T020000
+RDATE;VALUE=DATE-TIME:19440403T020000
+RDATE;VALUE=DATE-TIME:19450408T020000
+RDATE;VALUE=DATE-TIME:19460506T020000
+RDATE;VALUE=DATE-TIME:19470420T020000
+RDATE;VALUE=DATE-TIME:19480418T020000
+RDATE;VALUE=DATE-TIME:19490409T020000
+RDATE;VALUE=DATE-TIME:19790401T020000
+RDATE;VALUE=DATE-TIME:19800406T020000
+END:DAYLIGHT
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19790930T030000
+RRULE:FREQ=YEARLY;COUNT=17;BYDAY=-1SU;BYMONTH=9
+END:STANDARD
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19161001T010000
+RDATE;VALUE=DATE-TIME:19161001T010000
+RDATE;VALUE=DATE-TIME:19170917T030000
+RDATE;VALUE=DATE-TIME:19180916T030000
+RDATE;VALUE=DATE-TIME:19421102T030000
+RDATE;VALUE=DATE-TIME:19431004T030000
+RDATE;VALUE=DATE-TIME:19440917T030000
+RDATE;VALUE=DATE-TIME:19451118T030000
+RDATE;VALUE=DATE-TIME:19461006T030000
+RDATE;VALUE=DATE-TIME:19471005T030000
+RDATE;VALUE=DATE-TIME:19481003T030000
+RDATE;VALUE=DATE-TIME:19491002T030000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN="Daniel Vrátil":MAILTO:dvratil@kde.org
+DTSTAMP:20150930T090704Z
+ATTENDEE;CN="Volker Krause";RSVP=FALSE;PARTSTAT=ACCEPTED;
+ ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL;X-UID=62645952:mailto:
+ vkrause@kde.org
+ATTENDEE;CN="Christian Mollekopf";RSVP=FALSE;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL;X-UID=35926528:mailto:
+ cmollekopf@gmail.com
+ATTENDEE;CN="Sandro Knauß";RSVP=FALSE;PARTSTAT=DELEGATED;
+ ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL;X-UID=64325328:mailto:
+ sknauss@kde.org
+CREATED:20150930T090351Z
+UID:15ea20f1-33b4-4d4a-a746-482f09cb5287
+LAST-MODIFIED:20150930T090629Z
+SUMMARY:Plánovací meeting
+LOCATION:Zasedačka číslo 3
+RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=WE
+EXDATE;VALUE=DATE:20151118
+DTSTART;TZID=Europe/Prague:20150930T030000
+DTEND;TZID=Europe/Prague:20150930T043000
+TRANSP:OPAQUE
+BEGIN:VALARM
+DESCRIPTION:
+ACTION:DISPLAY
+TRIGGER;VALUE=DURATION:-PT15M
+X-KDE-KCALCORE-ENABLED:TRUE
+END:VALARM
+END:VEVENT
+END:VCALENDAR
diff --git a/autotests/data/event-allday-multiday.html \
b/autotests/data/event-allday-multiday.html new file mode 100644
index 0000000..6ca77a4
--- /dev/null
+++ b/autotests/data/event-allday-multiday.html
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +        </td>
+        <td>
+          <b>
+            <u>20. May 2005, allday, multiday</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Friday, 20 May 2005 - Saturday, 21 May 2005</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>2 days</td>
+      </tr>
+<!-- Recurrence -->
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Friday, 20 May 2005 10:58:56 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-allday-multiday.ical \
b/autotests/data/event-allday-multiday.ical new file mode 100644
index 0000000..bbc9864
--- /dev/null
+++ b/autotests/data/event-allday-multiday.ical
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+X-LibKCal-Testsuite-OutTZ:Europe/Vienna
+BEGIN:VEVENT
+DTSTAMP:20050520T105856Z
+ORGANIZER;CN=Reinhold Kainhofer:MAILTO:reinhold@kainhofer.com
+CREATED:20050520T105219Z
+UID:KOrganizer-45214176.303
+SEQUENCE:2
+LAST-MODIFIED:20050520T105815Z
+SUMMARY:20. May 2005\, allday\, multiday
+CLASS:PUBLIC
+PRIORITY:5
+DTSTART:20050520
+DTEND:20050522
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/autotests/data/event-allday.html b/autotests/data/event-allday.html
new file mode 100644
index 0000000..47012c6
--- /dev/null
+++ b/autotests/data/event-allday.html
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +        </td>
+        <td>
+          <b>
+            <u>20. May 2005, allday</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Friday, 20 May 2005</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>1 day</td>
+      </tr>
+<!-- Recurrence -->
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Friday, 20 May 2005 10:58:56 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-allday.ical b/autotests/data/event-allday.ical
new file mode 100644
index 0000000..173917a
--- /dev/null
+++ b/autotests/data/event-allday.ical
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+X-LibKCal-Testsuite-OutTZ:Europe/Vienna
+BEGIN:VEVENT
+DTSTAMP:20050520T105856Z
+ORGANIZER;CN=Reinhold Kainhofer:MAILTO:reinhold@kainhofer.com
+CREATED:20050520T105219Z
+UID:KOrganizer-45214176.303
+SEQUENCE:2
+LAST-MODIFIED:20050520T105815Z
+SUMMARY:20. May 2005\, allday
+CLASS:PUBLIC
+PRIORITY:5
+DTSTART:20050520
+DTEND:20050520
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/autotests/data/event-exception-single.html \
b/autotests/data/event-exception-single.html new file mode 100644
index 0000000..f1e7932
--- /dev/null
+++ b/autotests/data/event-exception-single.html
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +        </td>
+        <td>
+          <b>
+            <u>summaryException</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Thursday, 19 May 2005</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>09:45:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>3 hours 30 minutes</td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Exception</td>
+      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Sunday, 22 May 2005 20:12:07 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-exception-single.ical \
b/autotests/data/event-exception-single.ical new file mode 100644
index 0000000..a1c2910
--- /dev/null
+++ b/autotests/data/event-exception-single.ical
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20050522T201207Z
+ORGANIZER;CN=Reinhold Kainhofer:MAILTO:reinhold@kainhofer.com
+CREATED:20050522T201119Z
+UID:KOrganizer-557711714.436
+SEQUENCE:0
+LAST-MODIFIED:20050522T201119Z
+SUMMARY:summaryException
+CLASS:PUBLIC
+PRIORITY:5
+RECURRENCE-ID:20050519T084500Z
+DTSTART:20050519T094500Z
+DTEND:20050519T131500Z
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/autotests/data/event-exception-thisandfuture.html \
b/autotests/data/event-exception-thisandfuture.html new file mode 100644
index 0000000..f1e7932
--- /dev/null
+++ b/autotests/data/event-exception-thisandfuture.html
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +        </td>
+        <td>
+          <b>
+            <u>summaryException</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Thursday, 19 May 2005</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>09:45:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>3 hours 30 minutes</td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Exception</td>
+      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Sunday, 22 May 2005 20:12:07 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-exception-thisandfuture.ical \
b/autotests/data/event-exception-thisandfuture.ical new file mode 100644
index 0000000..e02dab8
--- /dev/null
+++ b/autotests/data/event-exception-thisandfuture.ical
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20050522T201207Z
+ORGANIZER;CN=Reinhold Kainhofer:MAILTO:reinhold@kainhofer.com
+CREATED:20050522T201119Z
+UID:KOrganizer-557711714.436
+SEQUENCE:0
+LAST-MODIFIED:20050522T201119Z
+SUMMARY:summaryException
+CLASS:PUBLIC
+PRIORITY:5
+RECURRENCE-ID;RANGE=THISANDFUTURE:20050519T084500Z
+DTSTART:20050519T094500Z
+DTEND:20050519T131500Z
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/autotests/data/event-multiday.html b/autotests/data/event-multiday.html
new file mode 100644
index 0000000..e14da33
--- /dev/null
+++ b/autotests/data/event-multiday.html
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
alt="Event" title="Event" /> +          <img src="file:edit-redo.svg" align="top" \
height="16" width="16" alt="Recurring incidence" title="Recurring incidence" /> +     \
</td> +        <td>
+          <b>
+            <u>20 May 2005 - 21 May 2015</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Friday, 20 May 2005</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>17:00:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>1 day 1 hour </td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Recurs every 3 months on the 2nd Last Friday until 2009-05-22 17:00 (17 \
occurrences)</td> +      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Friday, 20 May 2005 10:58:56 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-multiday.ical b/autotests/data/event-multiday.ical
new file mode 100644
index 0000000..2a7826b
--- /dev/null
+++ b/autotests/data/event-multiday.ical
@@ -0,0 +1,20 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+X-LibKCal-Testsuite-OutTZ:Europe/Vienna
+BEGIN:VEVENT
+DTSTAMP:20050520T105856Z
+ORGANIZER;CN=Reinhold Kainhofer:MAILTO:reinhold@kainhofer.com
+CREATED:20050520T105219Z
+UID:KOrganizer-45214176.303
+SEQUENCE:2
+LAST-MODIFIED:20050520T105815Z
+SUMMARY:20 May 2005 - 21 May 2015
+CLASS:PUBLIC
+PRIORITY:5
+RRULE:FREQ=MONTHLY;COUNT=17;INTERVAL=3;BYDAY=-2FR
+DTSTART:20050520T170000Z
+DTEND:20050521T180000Z
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/autotests/data/event-recurrence-single.out.html \
b/autotests/data/event-recurrence-single.out.html new file mode 100644
index 0000000..dd5d0fa
--- /dev/null
+++ b/autotests/data/event-recurrence-single.out.html
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
/> +        </td>
+        <td>
+          <b>
+            <u>summaryException</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Thursday, 19 May 2005</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>09:45:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>3 hours 30 minutes</td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Exception</td>
+      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Sunday, 22 May 2005 20:12:07 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/event-recurrence-thisandfuture.out.html \
b/autotests/data/event-recurrence-thisandfuture.out.html new file mode 100644
index 0000000..dd5d0fa
--- /dev/null
+++ b/autotests/data/event-recurrence-thisandfuture.out.html
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-day.svg" align="top" height="16" width="16" \
/> +        </td>
+        <td>
+          <b>
+            <u>summaryException</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar name -->
+<!-- Location -->
+<!-- Start/end -->
+      <tr>
+        <th>Date:</th>
+        <td>Thursday, 19 May 2005</td>
+      </tr>
+      <tr>
+        <th>Time:</th>
+        <td>09:45:00</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>3 hours 30 minutes</td>
+      </tr>
+<!-- Recurrence -->
+      <tr>
+        <th>Recurrence:</th>
+        <td>Exception</td>
+      </tr>
+<!-- Birthday -->
+<!-- Anniversary -->
+<!-- Description -->
+<!-- Alarms -->
+<!-- Organizer -->
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Attachments -->
+    </table>
+<!-- Creation -->
+    <p>
+      <em>Creation date: Sunday, 22 May 2005 20:12:07 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/freebusy-1.html b/autotests/data/freebusy-1.html
new file mode 100644
index 0000000..b0b963f
--- /dev/null
+++ b/autotests/data/freebusy-1.html
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <h2>Free/Busy information for dvratil@kde.org</h2>
+    <h4>Busy times in date range 30 Sep 2015 - 31 Oct 2015:</h4>
+    <p><em><b>Busy:</b></em>
+30 Sep 2015, 12:00:00 - 14:00:00
+        <br />5 Oct 2015, 09:30:00 - 10:00:00
+        <br />12 Oct 2015, 09:30:00 - 10:00:00
+        <br />20 Oct 2015 00:00:00 - 27 Oct 2015 00:00:00
+</p>
+  </body>
+</html>
diff --git a/autotests/data/freebusy-1.ical b/autotests/data/freebusy-1.ical
new file mode 100644
index 0000000..c637562
--- /dev/null
+++ b/autotests/data/freebusy-1.ical
@@ -0,0 +1,17 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 3.4//EN
+VERSION:2.0
+X-LibKCal-Testsuite-OutTZ:Europe/Vienna
+BEGIN:VFREEBUSY
+UID:xyz
+ORGANIZER:MAILTO:dvratil@kde.org
+ATTENDEE:MAILTO:dvratil@kde.org
+DTSTART:20150930T000000Z
+DTEND:20151031T235959Z
+DTSTAMP:20150930T083000Z
+FREEBUSY:20150930T120000Z/20150930T140000Z
+FREEBUSY:20151005T093000Z/20151005T100000Z
+FREEBUSY:20151012T093000Z/20151012T100000Z
+FREEBUSY:20151020T000000Z/20151027T000000Z
+END:VFREEBUSY
+END:VCALENDAR
diff --git a/autotests/data/journal-1.html b/autotests/data/journal-1.html
new file mode 100644
index 0000000..38a7a89
--- /dev/null
+++ b/autotests/data/journal-1.html
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-pim-journal.svg" align="top" height="16" width="16" \
alt="Journal" title="Journal" /> +        </td>
+        <td>
+          <b>
+            <u>Dear diary</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table>
+<!-- Calendar -->
+<!-- Date -->
+      <tr>
+        <th>Date:</th>
+        <td>Monday, 21 September 2015</td>
+      </tr>
+<!-- Description -->
+      <tr>
+        <th>Description:</th>
+        <td>Dear diary,<br />
+<br />
+today I went to Prague and it was amazing.<br />
+<br />
+End of story.<br />
+</td>
+      </tr>
+<!-- Categories -->
+    </table>
+<!-- Creation date -->
+    <p>
+      <em>Creation date: Wednesday, 30 September 2015 09:50:37 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/journal-1.ical b/autotests/data/journal-1.ical
new file mode 100644
index 0000000..b1d1eeb
--- /dev/null
+++ b/autotests/data/journal-1.ical
@@ -0,0 +1,82 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN
+VERSION:2.0
+X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0
+BEGIN:VJOURNAL
+DTSTAMP:20150930T095037Z
+CREATED:20150930T095037Z
+UID:2fdb08af-bf2a-4643-a6d7-47b03724e314
+LAST-MODIFIED:20150930T095037Z
+DESCRIPTION:Dear diary\,\n\ntoday I went to Prague and it was 
+ amazing.\n\nEnd of story.\n
+SUMMARY:Dear diary
+DTSTART;TZID=Europe/Prague:20150921T080000
+END:VJOURNAL
+BEGIN:VTIMEZONE
+TZID:Europe/Prague
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+005744
+TZOFFSETTO:+0100
+DTSTART:19011213T214336
+RDATE;VALUE=DATE-TIME:19011213T214336
+END:STANDARD
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+DTSTART:19160430T230000
+RDATE;VALUE=DATE-TIME:19160430T230000
+RDATE;VALUE=DATE-TIME:19170416T020000
+RDATE;VALUE=DATE-TIME:19180415T020000
+RDATE;VALUE=DATE-TIME:19400401T020000
+RDATE;VALUE=DATE-TIME:19430329T020000
+RDATE;VALUE=DATE-TIME:19440403T020000
+RDATE;VALUE=DATE-TIME:19450408T020000
+RDATE;VALUE=DATE-TIME:19460506T020000
+RDATE;VALUE=DATE-TIME:19470420T020000
+RDATE;VALUE=DATE-TIME:19480418T020000
+RDATE;VALUE=DATE-TIME:19490409T020000
+RDATE;VALUE=DATE-TIME:19790401T020000
+RDATE;VALUE=DATE-TIME:19800406T020000
+END:DAYLIGHT
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19790930T030000
+RRULE:FREQ=YEARLY;COUNT=17;BYDAY=-1SU;BYMONTH=9
+END:STANDARD
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:STANDARD
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+DTSTART:19161001T010000
+RDATE;VALUE=DATE-TIME:19161001T010000
+RDATE;VALUE=DATE-TIME:19170917T030000
+RDATE;VALUE=DATE-TIME:19180916T030000
+RDATE;VALUE=DATE-TIME:19421102T030000
+RDATE;VALUE=DATE-TIME:19431004T030000
+RDATE;VALUE=DATE-TIME:19440917T030000
+RDATE;VALUE=DATE-TIME:19451118T030000
+RDATE;VALUE=DATE-TIME:19461006T030000
+RDATE;VALUE=DATE-TIME:19471005T030000
+RDATE;VALUE=DATE-TIME:19481003T030000
+RDATE;VALUE=DATE-TIME:19491002T030000
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
diff --git a/autotests/data/todo-1.html b/autotests/data/todo-1.html
new file mode 100644
index 0000000..3a98c05
--- /dev/null
+++ b/autotests/data/todo-1.html
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" \
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html \
xmlns="http://www.w3.org/1999/xhtml"> +  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF8" />
+    <title></title>
+    <style></style>
+  </head>
+  <body>
+    <style type="text/css">
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+</style>
+    <table class="header">
+      <tr>
+        <td>
+          <img src="file:view-calendar-tasks.svg" align="top" height="16" width="16" \
alt="Todo" title="Todo" /> +          <img \
src="file:preferences-desktop-notification-bell.svg" align="top" height="16" \
width="16" alt="Incidence with a reminder" title="Incidence with a reminder" /> +     \
</td> +        <td>
+          <b>
+            <u>Buy Milk</u>
+          </b>
+        </td>
+      </tr>
+    </table>
+    <table class="main">
+<!-- Calendar -->
+<!-- Location -->
+      <tr>
+        <th>Location:</th>
+        <td>Grocery</td>
+      </tr>
+<!-- Start date -->
+      <tr>
+        <th>Start:</th>
+        <td>Wednesday, 30 September 2015 08:00:00 UTC</td>
+      </tr>
+<!-- Due date -->
+      <tr>
+        <th>Due:</th>
+        <td>Thursday, 1 October 2015 08:00:00 UTC</td>
+      </tr>
+<!-- Duration -->
+      <tr>
+        <th>Duration:</th>
+        <td>1 day </td>
+      </tr>
+<!-- Recurrence -->
+<!-- Organizer -->
+<!-- Attendee -->
+<!-- Description -->
+      <tr>
+        <th>Description:</th>
+        <td>We need milk <img align="center" title=":)" alt=":)" \
src="/home/dvratil/.local/share/emoticons/Ubuntu/icon_smile.png" width="16" \
height="15" /></td> +      </tr>
+<!-- Comments -->
+<!-- Reminders -->
+      <tr>
+        <th>Reminder:</th>
+        <td>15 minutes before the to-do is due</td>
+      </tr>
+<!-- Organizer -->
+      <tr>
+        <th>Organizer:</th>
+        <td><img src="file:meeting-organizer.png" align="top" height="16" width="16" \
alt="meeting-organizer" title="" /> +Daniel Vrátil
+    <a href="mailto:Daniel Vrátil %3Cme@dvratil.cz%3E"><img \
src="file:mail-message-new.svg" align="top" height="16" width="16" alt="Send email" \
title="Send email" /></a> +
+        </td>
+      </tr>
+<!-- Attendees - Chair -->
+<!-- Attendees - Required Participants -->
+      <tr>
+        <th>Required Participants:</th>
+        <td>
+          <img src="file:help-about.svg" align="top" height="16" width="16" \
alt="Needs action" title="Needs action" /> +          <a href="uid:68225424">Flatmate
+    </a>
+          <a href="mailto:Flatmate %3Cmy@flat.mate%3E">
+            <img src="file:mail-message-new.svg" align="top" height="16" width="16" \
alt="Send email" title="Send email" /> +          </a>
+        </td>
+      </tr>
+<!-- Attendees - Optional Participants -->
+<!-- Attendees - Observers -->
+<!-- Categories -->
+<!-- Priority -->
+      <tr>
+        <th>Priority:</th>
+        <td>3</td>
+      </tr>
+<!-- Completed -->
+      <tr>
+        <th>Percent done:</th>
+        <td>50%</td>
+      </tr>
+<!-- Attachments -->
+    </table>
+<!-- Creation date -->
+    <p>
+      <em>Creation date: Wednesday, 30 September 2015 09:43:11 UTC</em>
+    </p>
+  </body>
+</html>
diff --git a/autotests/data/todo-1.ical b/autotests/data/todo-1.ical
new file mode 100644
index 0000000..59f557d
--- /dev/null
+++ b/autotests/data/todo-1.ical
@@ -0,0 +1,30 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 5.0.44 pre//EN
+VERSION:2.0
+X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0
+METHOD:REQUEST
+BEGIN:VTODO
+ORGANIZER;CN="Daniel Vrátil":MAILTO:me@dvratil.cz
+DTSTAMP:20150930T094648Z
+ATTENDEE;CN="Flatmate";RSVP=FALSE;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL;X-UID=68225424:mailto:
+ my@flat.mate
+CREATED:20150930T094311Z
+UID:c8e304fa-f103-4693-bb7a-897bedf4552e
+LAST-MODIFIED:20150930T094525Z
+DESCRIPTION:We need milk :)
+SUMMARY:Buy Milk
+LOCATION:Grocery
+STATUS:IN-PROCESS
+PRIORITY:3
+DUE:20151001T080000Z
+DTSTART:20150930T080000Z
+PERCENT-COMPLETE:50
+BEGIN:VALARM
+DESCRIPTION:
+ACTION:DISPLAY
+TRIGGER;VALUE=DURATION;RELATED=END:-PT15M
+X-KDE-KCALCORE-ENABLED:TRUE
+END:VALARM
+END:VTODO
+END:VCALENDAR
diff --git a/autotests/test_config.h.cmake b/autotests/test_config.h.cmake
new file mode 100644
index 0000000..0947c1a
--- /dev/null
+++ b/autotests/test_config.h.cmake
@@ -0,0 +1,5 @@
+#define TEST_DATA_DIR "@TEST_DATA_DIR@"
+
+#define TEST_TEMPLATE_PATH "@TEST_TEMPLATE_PATH@"
+
+#define TEST_PLUGIN_PATH "@TEST_PLUGIN_PATH@"
\ No newline at end of file
diff --git a/autotests/testincidenceformatter.cpp \
b/autotests/testincidenceformatter.cpp index 28dd952..ce9f750 100644
--- a/autotests/testincidenceformatter.cpp
+++ b/autotests/testincidenceformatter.cpp
@@ -20,21 +20,37 @@
 */
 
 #include "testincidenceformatter.h"
+#include "test_config.h"
+
 #include "incidenceformatter.h"
+#include "grantleetemplatemanager_p.h"
 
 #include <kcalcore/event.h>
+#include <kcalcore/icalformat.h>
+#include <kcalcore/todo.h>
+#include <kcalcore/journal.h>
+#include <kcalcore/freebusy.h>
+#include <kcalcore/memorycalendar.h>
 
 #include <KDateTime>
 #include <KLocalizedString>
 #include <KLocale>
 
 #include <QDebug>
+#include <QProcess>
 #include <qtest.h>
+
 QTEST_MAIN(IncidenceFormatterTest)
 
 using namespace KCalCore;
 using namespace KCalUtils;
 
+void IncidenceFormatterTest::initTestCase()
+{
+    GrantleeTemplateManager::instance()->setTemplatePath(QStringLiteral(TEST_TEMPLATE_PATH));
 +    GrantleeTemplateManager::instance()->setPluginPath(QStringLiteral(TEST_PLUGIN_PATH));
 +}
+
 void IncidenceFormatterTest::testRecurrenceString()
 {
     // TEST: A daily recurrence with date exclusions //
@@ -127,3 +143,221 @@ void IncidenceFormatterTest::testRecurrenceString()
 
 //  qDebug() << "recurrenceString=" << IncidenceFormatter::recurrenceString( e3 );
 }
+
+
+KCalCore::Calendar::Ptr IncidenceFormatterTest::loadCalendar(const QString &name)
+{
+    auto calendar = KCalCore::MemoryCalendar::Ptr::create(KDateTime::UTC);
+    KCalCore::ICalFormat format;
+
+    if (!format.load(calendar, QStringLiteral(TEST_DATA_DIR "/%1.ical").arg(name))) \
{ +        return KCalCore::Calendar::Ptr();
+    }
+
+    return calendar;
+}
+
+bool IncidenceFormatterTest::validateHtml(const QString &name, const QString &_html)
+{
+    QString html = QStringLiteral("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 \
Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n" +              \
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" +                "  <head>\n"
+                "    <title></title>\n"
+                "    <style></style>\n"
+                "  </head>\n"
+                "<body>")
+        + _html
+        + QStringLiteral("</body>\n</html>");
+
+    const QString outFileName = QStringLiteral(TEST_DATA_DIR "/%1.out").arg(name);
+    const QString htmlFileName = QStringLiteral(TEST_DATA_DIR \
"/%1.out.html").arg(name); +    QFile outFile(outFileName);
+    if (!outFile.open(QIODevice::WriteOnly)) {
+        return false;
+    }
+    outFile.write(html.toUtf8());
+    outFile.close();
+
+    // validate xml and pretty-print for comparisson
+    // TODO add proper cmake check for xmllint and diff
+    const QStringList args = {
+        QStringLiteral("--format"),
+        QStringLiteral("--encode"),
+        QStringLiteral("UTF8"),
+        QStringLiteral("--output"),
+        htmlFileName,
+        outFileName
+    };
+
+    const int result = QProcess::execute(QLatin1String("xmllint"), args);
+    return result == 0;
+}
+
+bool IncidenceFormatterTest::compareHtml(const QString &name)
+{
+    const QString htmlFileName = QStringLiteral(TEST_DATA_DIR \
"/%1.out.html").arg(name); +    const QString referenceFileName = \
QStringLiteral(TEST_DATA_DIR "/%1.html").arg(name); +
+    // get rid of system dependent or random paths
+    {
+        QFile f(htmlFileName);
+        if (!f.open(QIODevice::ReadOnly)) {
+            return false;
+        }
+        QString content = QString::fromUtf8(f.readAll());
+        f.close();
+        content.replace(QRegExp(QLatin1String("\"file:[^\"]*[/(?:%2F)]([^\"/(?:%2F)]*)\"")), \
QStringLiteral("\"file:\\1\"")); +        if (!f.open(QIODevice::WriteOnly | \
QIODevice::Truncate)) { +            return false;
+        }
+        f.write(content.toUtf8());
+        f.close();
+    }
+
+    // compare to reference file
+    const QStringList args = {
+        QStringLiteral("-u"),
+        referenceFileName,
+        htmlFileName
+    };
+
+    QProcess proc;
+    proc.setProcessChannelMode(QProcess::ForwardedChannels);
+    proc.start(QLatin1String("diff"), args);
+    if (!proc.waitForFinished()) {
+        return false;
+    }
+
+    return proc.exitCode() == 0;
+}
+
+void IncidenceFormatterTest::cleanup(const QString &name)
+{
+    QFile::remove(QStringLiteral(TEST_DATA_DIR "/%1.out").arg(name));
+    QFile::remove(QStringLiteral(TEST_DATA_DIR "/%1.out.html").arg(name));
+}
+
+void IncidenceFormatterTest::testErrorTemplate()
+{
+    GrantleeTemplateManager::instance()->setTemplatePath(QStringLiteral(TEST_DATA_DIR));
 +    const QString html = \
GrantleeTemplateManager::instance()->render(QStringLiteral("broken-template.html"), \
QVariantHash()); +    \
GrantleeTemplateManager::instance()->setTemplatePath(QStringLiteral(TEST_TEMPLATE_PATH));
 +
+    const QString expected = QStringLiteral(
+        "<h1>Template parsing error</h1>\n"
+        "<b>Template:</b> broken-template.html<br>\n"
+        "<b>Error message:</b> Unclosed tag in template broken-template.html. \
Expected one of: (else endif), line 2, broken-template.html"); +
+    QCOMPARE(html, expected);
+}
+
+void IncidenceFormatterTest::testDisplayViewFormatEvent_data()
+{
+    QTest::addColumn<QString>("name");
+
+    QTest::newRow("event-1") << QStringLiteral("event-1");
+    QTest::newRow("event-2") << QStringLiteral("event-2");
+    QTest::newRow("event-exception-thisandfuture") << \
QStringLiteral("event-exception-thisandfuture"); +    \
QTest::newRow("event-exception-single") << QStringLiteral("event-exception-single"); \
+    QTest::newRow("event-allday-multiday") << \
QStringLiteral("event-allday-multiday"); +    QTest::newRow("event-allday") << \
QStringLiteral("event-allday"); +    QTest::newRow("event-multiday") << \
QStringLiteral("event-multiday"); +}
+
+void IncidenceFormatterTest::testDisplayViewFormatEvent()
+{
+    QFETCH(QString, name);
+
+    KCalCore::Calendar::Ptr calendar = loadCalendar(name);
+    QVERIFY(calendar);
+
+    const auto events = calendar->events();
+    QCOMPARE(events.size(), 1);
+
+    const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, \
events[0]); +
+    QVERIFY(validateHtml(name, html));
+    QVERIFY(compareHtml(name));
+
+    cleanup(name);
+}
+
+void IncidenceFormatterTest::testDisplayViewFormatTodo_data()
+{
+    QTest::addColumn<QString>("name");
+
+    QTest::newRow("todo-1") << QStringLiteral("todo-1");
+}
+
+void IncidenceFormatterTest::testDisplayViewFormatTodo()
+{
+    QFETCH(QString, name);
+
+    KCalCore::Calendar::Ptr calendar = loadCalendar(name);
+    QVERIFY(calendar);
+
+    const auto todos = calendar->todos();
+    QCOMPARE(todos.size(), 1);
+
+    const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, \
todos[0]); +
+    QVERIFY(validateHtml(name, html));
+    QVERIFY(compareHtml(name));
+
+    cleanup(name);
+}
+
+void IncidenceFormatterTest::testDisplayViewFormatJournal_data()
+{
+    QTest::addColumn<QString>("name");
+
+    QTest::newRow("journal-1") << QStringLiteral("journal-1");
+}
+
+void IncidenceFormatterTest::testDisplayViewFormatJournal()
+{
+    QFETCH(QString, name);
+
+    KCalCore::Calendar::Ptr calendar = loadCalendar(name);
+    QVERIFY(calendar);
+
+    const auto journals = calendar->journals();
+    QCOMPARE(journals.size(), 1);
+
+    const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, \
journals[0]); +
+    QVERIFY(validateHtml(name, html));
+    QVERIFY(compareHtml(name));
+
+    cleanup(name);
+}
+
+void IncidenceFormatterTest::testDisplayViewFreeBusy_data()
+{
+    QTest::addColumn<QString>("name");
+
+    QTest::newRow("freebusy-1") << QStringLiteral("freebusy-1");
+}
+
+void IncidenceFormatterTest::testDisplayViewFreeBusy()
+{
+    QFETCH(QString, name);
+
+    KCalCore::Calendar::Ptr calendar = loadCalendar(name);
+    QVERIFY(calendar);
+
+    QFile file(QStringLiteral(TEST_DATA_DIR "/%1.ical").arg(name));
+    QVERIFY(file.open(QIODevice::ReadOnly));
+    const QByteArray fbData = file.readAll();
+
+    KCalCore::ICalFormat format;
+    KCalCore::FreeBusy::Ptr freeBusy = \
format.parseFreeBusy(QString::fromUtf8(fbData)); +    QVERIFY(freeBusy);
+
+    const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, \
freeBusy); +
+    QVERIFY(validateHtml(name, html));
+    QVERIFY(compareHtml(name));
+
+    cleanup(name);
+}
diff --git a/autotests/testincidenceformatter.h b/autotests/testincidenceformatter.h
index 0065745..11e4d14 100644
--- a/autotests/testincidenceformatter.h
+++ b/autotests/testincidenceformatter.h
@@ -24,11 +24,37 @@
 
 #include <QtCore/QObject>
 
+#include <KCalCore/MemoryCalendar>
+
 class IncidenceFormatterTest : public QObject
 {
     Q_OBJECT
+
+private:
+    /* Helper functions for testDisplayViewFormat* */
+    KCalCore::Calendar::Ptr loadCalendar(const QString &name);
+    bool validateHtml(const QString &name, const QString &html);
+    bool compareHtml(const QString &name);
+    void cleanup(const QString &name);
+
 private Q_SLOTS:
+    void initTestCase();
+
     void testRecurrenceString();
+
+    void testErrorTemplate();
+
+    void testDisplayViewFormatEvent_data();
+    void testDisplayViewFormatEvent();
+
+    void testDisplayViewFormatTodo_data();
+    void testDisplayViewFormatTodo();
+
+    void testDisplayViewFormatJournal_data();
+    void testDisplayViewFormatJournal();
+
+    void testDisplayViewFreeBusy_data();
+    void testDisplayViewFreeBusy();
 };
 
 #endif
diff --git a/scripts/extract_strings_ki18n.py b/scripts/extract_strings_ki18n.py
new file mode 100755
index 0000000..5d11148
--- /dev/null
+++ b/scripts/extract_strings_ki18n.py
@@ -0,0 +1,54 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+##
+# Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+##
+
+
+import os, sys, glob, operator
+from grantlee_strings_extractor import TranslationOutputter
+
+class KI18nExtractStrings(TranslationOutputter):
+    def createOutput(self, template_filename, context_strings, outputfile):
+        for context_string in context_strings:
+            outputfile.write("// i18n: file: %s\n" % template_filename)
+            if context_string.context:
+                if not context_string.plural:
+                    outputfile.write("i18nc(\"" + context_string.context + "\", \"" \
+ context_string._string + "\");\n") +                else:
+                    outputfile.write("i18ncp(\"" + context_string.context + "\", \"" \
+ context_string._string + "\", \"" + context_string.plural + "\", 1);\n") +          \
else: +                if context_string.plural:
+                    outputfile.write("i18np(\"" + context_string._string + "\", \"" \
+ context_string.plural + "\", 1);\n") +                else:
+                    outputfile.write("i18n(\"" + context_string._string + "\");\n")
+
+
+
+if __name__ == "__main__":
+    ex = KI18nExtractStrings()
+
+    outputfile = sys.stdout
+
+    files = reduce(operator.add, map(glob.glob, sys.argv[1:]))
+
+    for filename in files:
+        f = open(filename, "r")
+        ex.translate(f, outputfile)
+
+    outputfile.write("\n")
diff --git a/scripts/grantlee_strings_extractor.py \
b/scripts/grantlee_strings_extractor.py new file mode 100644
index 0000000..c7d71dd
--- /dev/null
+++ b/scripts/grantlee_strings_extractor.py
@@ -0,0 +1,365 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+##
+# Copyright 2010,2011 Stephen Kelly <steveire@gmail.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+## Parts of this file are reproduced from the Django framework. The Django licence \
appears below. +
+##
+# Copyright (c) Django Software Foundation and individual contributors.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+#     1. Redistributions of source code must retain the above copyright notice,
+#        this list of conditions and the following disclaimer.
+#
+#     2. Redistributions in binary form must reproduce the above copyright
+#        notice, this list of conditions and the following disclaimer in the
+#        documentation and/or other materials provided with the distribution.
+#
+#     3. Neither the name of Django nor the names of its contributors may be used
+#        to endorse or promote products derived from this software without
+#        specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+##
+
+import re
+import os.path
+
+# == Introduction to the template syntax ==
+#
+# The template syntax looks like this:
+# (For more see here: http://grantlee.org/apidox/for_themers.html )
+#
+# This is plain text
+# This is text with a {{ value }} substitution
+# This is {% if condition_is_met %}a conditional{% endif %}
+# {# This is a comment #}
+# This is a {% comment %} multi-line
+# comment
+# {% endcomment %}
+#
+# That is, we have plain text.
+# We have value substitution with {{ }}
+# We have comments with {# #}
+# We have control tags with {% %}
+#
+# The first token inside {% %} syntax is called a tag name. Above, we have
+# an if tag and a comment tag.
+#
+# The 'value' in {{ value }} is called a filter expression. In the above case
+# the filter expression is a simple value which was inserted into the context.
+# In other cases it can be {{ value|upper }}, that is the value can be passed
+# through a filter called 'upper' with the '|', or filter expression can
+# be {{ value|join:"-" }}, that is it can be passed through the join filter
+# which takes an argument. In this case, the 'value' would actually be a list,
+# and the join filter would concatenate them with a dash. A filter can have
+# either no arguments, like upper, or it can take one argument, delimited by
+# a colon (';'). A filter expression can consist of a value followed by a
+# chain of filters, such as {{ value|join:"-"|upper }}. A filter expression
+# can appear one time inside {{ }} but may appear multiple times inside {% %}
+# For example {% cycle foo|upper bar|join:"-" bat %} contains 3 filter
+# expressions, 'foo|upper', 'bar|join:"-"' and 'bat'.
+#
+# Comments are ignored in the templates.
+#
+# == i18n in templates ==
+#
+# The purpose of this script is to extract translatable strings from templates
+# The aim is to allow template authors to write templates like this:
+#
+# This is a {{ _("translatable string") }} in the template.
+# This is a {% i18n "translatable string about %1" something %}
+# This is a {% i18nc "Some context information" "string about %1" something %}
+# This is a {% i18np "%1 string about %2" numthings something %}
+# This is a {% i18ncp "some context" "%1 string about %2" numthings something %}
+#
+# That is, simple translation with _(), and i18n* tags to allow for variable
+# substitution, context messages and plurals. Translatable strings may appear
+# in a filter expression, either as the value begin filtered, or as the argument
+# or both:
+#
+# {{ _("hello")|upper }}
+# {{ list|join:_("and") }}
+#
+# == How the strings are extracted ==
+#
+# The strings are extracted by parsing the template with regular expressions.
+# The tag_re regular expression breaks the template into a stream of tokens
+# containing plain text, {{ values }} and {% tags %}.
+# That work is done by the tokenize method with the create_token method.
+# Each token is then processed to extract the translatable strings from
+# the filter expressions.
+
+
+# The original context of much of this script is in the django template system:
+# http://code.djangoproject.com/browser/django/trunk/django/template/base.py
+
+
+TOKEN_TEXT = 0
+TOKEN_VAR = 1
+TOKEN_BLOCK = 2
+TOKEN_COMMENT = 3
+
+# template syntax constants
+FILTER_SEPARATOR = '|'
+FILTER_ARGUMENT_SEPARATOR = ':'
+BLOCK_TAG_START = '{%'
+BLOCK_TAG_END = '%}'
+VARIABLE_TAG_START = '{{'
+VARIABLE_TAG_END = '}}'
+COMMENT_TAG_START = '{#'
+COMMENT_TAG_END = '#}'
+
+# match a variable or block tag and capture the entire tag, including start/end \
delimiters +tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), \
re.escape(BLOCK_TAG_END), +                                          \
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) +
+
+# Expression to match some_token and some_token="with spaces" (and similarly
+# for single-quoted strings).
+smart_split_re = re.compile(r"""
+    ((?:
+        [^\s'"]*
+        (?:
+            (?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*')
+            [^\s'"]*
+        )+
+    ) | \S+)
+""", re.VERBOSE)
+
+def smart_split(text):
+    r"""
+    Generator that splits a string by spaces, leaving quoted phrases together.
+    Supports both single and double quotes, and supports escaping quotes with
+    backslashes. In the output, strings will keep their initial and trailing
+    quote marks and escaped quotes will remain escaped (the results can then
+    be further processed with unescape_string_literal()).
+
+    >>> list(smart_split(r'This is "a person\'s" test.'))
+    [u'This', u'is', u'"a person\\\'s"', u'test.']
+    >>> list(smart_split(r"Another 'person\'s' test."))
+    [u'Another', u"'person\\'s'", u'test.']
+    >>> list(smart_split(r'A "\"funky\" style" test.'))
+    [u'A', u'"\\"funky\\" style"', u'test.']
+    """
+    for bit in smart_split_re.finditer(text):
+        yield bit.group(0)
+
+
+# This only matches constant *strings* (things in quotes or marked for
+# translation).
+
+constant_string = r"(?:%(strdq)s|%(strsq)s)" % {
+    'strdq': r'"[^"\\]*(?:\\.[^"\\]*)*"', # double-quoted string
+    'strsq': r"'[^'\\]*(?:\\.[^'\\]*)*'", # single-quoted string
+    }
+
+filter_raw_string = \
r"""^%(i18n_open)s(?P<l10nable>%(constant_string)s)%(i18n_close)s""" % { +    \
'constant_string': constant_string, +    'i18n_open' : re.escape("_("),
+    'i18n_close' : re.escape(")"),
+  }
+
+filter_re = re.compile(filter_raw_string, re.UNICODE|re.VERBOSE)
+
+class TemplateSyntaxError(Exception):
+    pass
+
+class TranslatableString:
+    _string = ''
+    context = ''
+    plural = ''
+
+    def __repr__(self):
+        return "String('%s', '%s', '%s')" % (self._string, self.context, \
self.plural) +
+class Token(object):
+    def __init__(self, token_type, contents):
+        # token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT.
+        self.token_type, self.contents = token_type, contents
+
+    def __str__(self):
+        return '<%s token: "%s...">' % \
+            ({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block', \
TOKEN_COMMENT: 'Comment'}[self.token_type], +            \
self.contents[:20].replace('\n', '')) +
+def create_token(token_string, in_tag):
+    """
+    Convert the given token string into a new Token object and return it.
+    If in_tag is True, we are processing something that matched a tag,
+    otherwise it should be treated as a literal string.
+    """
+    if in_tag:
+        if token_string.startswith(VARIABLE_TAG_START):
+            token = Token(TOKEN_VAR, \
token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) +        elif \
token_string.startswith(BLOCK_TAG_START): +            token = Token(TOKEN_BLOCK, \
token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) +        elif \
token_string.startswith(COMMENT_TAG_START): +            token = Token(TOKEN_COMMENT, \
'') +    else:
+        token = Token(TOKEN_TEXT, token_string)
+    return token
+
+def tokenize(template_string):
+
+    in_tag = False
+    result = []
+    for bit in tag_re.split(template_string):
+        if bit:
+            result.append(create_token(bit, in_tag))
+        in_tag = not in_tag
+    return result
+
+class TranslationOutputter:
+    translatable_strings = []
+
+    def get_translatable_filter_args(self, token):
+        """
+        Find the filter expressions in token and extract the strings in it.
+        """
+        matches = filter_re.finditer(token)
+        upto = 0
+        var_obj = False
+        for match in matches:
+            l10nable = match.group("l10nable")
+
+            if l10nable:
+                # Make sure it's a quoted string
+                if l10nable.startswith('"') and l10nable.endswith('"') \
+                        or l10nable.startswith("'") and l10nable.endswith("'"):
+                    ts = TranslatableString()
+                    ts._string = l10nable[1:-1]
+                    self.translatable_strings.append(ts)
+
+    def get_contextual_strings(self, token):
+        split = []
+        _bits = smart_split(token.contents)
+        _bit = _bits.next()
+        if _bit =="i18n" or _bit == "i18n_var":
+            # {% i18n "A one %1, a two %2, a three %3" var1 var2 var3 %}
+            # {% i18n_var "A one %1, a two %2, a three %3" var1 var2 var3 as result \
%} +            _bit = _bits.next()
+            if not _bit.startswith("'") and not _bit.startswith('"'):
+                return
+
+            sentinal = _bit[0]
+            if not _bit.endswith(sentinal):
+                return
+
+            translatable_string = TranslatableString()
+            translatable_string._string = _bit[1:-1]
+            self.translatable_strings.append(translatable_string)
+        elif _bit =="i18nc" or _bit == "i18nc_var":
+            # {% i18nc "An email send operation failed." "%1 Failed!" var1 %}
+            # {% i18nc_var "An email send operation failed." "%1 Failed!" var1 as \
result %} +            _bit = _bits.next()
+            if not _bit.startswith("'") and not _bit.startswith('"'):
+                return
+
+            sentinal = _bit[0]
+            if not _bit.endswith(sentinal):
+                return
+
+            translatable_string = TranslatableString()
+            translatable_string.context = _bit[1:-1]
+            _bit = _bits.next()
+            translatable_string._string = _bit[1:-1]
+            self.translatable_strings.append(translatable_string)
+        elif _bit =="i18np" or _bit =="i18np_var":
+            # {% i18np "An email send operation failed." "%1 email send operations \
failed. Error : % 2." count count errorMsg %} +            # {% i18np_var "An email \
send operation failed." "%1 email send operations failed. Error : % 2." count count \
errorMsg as result %} +            _bit = _bits.next()
+            if not _bit.startswith("'") and not _bit.startswith('"'):
+                return
+
+            sentinal = _bit[0]
+            if not _bit.endswith(sentinal):
+                return
+
+            translatable_string = TranslatableString()
+            translatable_string._string = _bit[1:-1]
+            _bit = _bits.next()
+            translatable_string.plural = _bit[1:-1]
+            self.translatable_strings.append(translatable_string)
+        elif _bit =="i18ncp" or _bit =="i18ncp_var":
+            # {% i18np "The user tried to send an email, but that failed." "An email \
send operation failed." "%1 email send operation failed." count count %} +            \
# {% i18np_var "The user tried to send an email, but that failed." "An email send \
operation failed." "%1 email send operation failed." count count as result %} +
+            _bit = _bits.next()
+            if not _bit.startswith("'") and not _bit.startswith('"'):
+                return
+
+            sentinal = _bit[0]
+            if not _bit.endswith(sentinal):
+                return
+
+            translatable_string = TranslatableString()
+            translatable_string.context = _bit[1:-1]
+            _bit = _bits.next()
+            translatable_string._string = _bit[1:-1]
+            _bit = _bits.next()
+            translatable_string.plural = _bit[1:-1]
+            self.translatable_strings.append(translatable_string)
+        else:
+          return
+
+        for _bit in _bits:
+
+            if (_bit == "as"):
+                return
+            self.get_translatable_filter_args(_bit)
+
+    def get_plain_strings(self, token):
+        split = []
+        bits = iter(smart_split(token.contents))
+        for bit in bits:
+            self.get_translatable_filter_args(bit)
+
+    def translate(self, template_file, outputfile):
+        template_string = template_file.read()
+        self.translatable_strings = []
+        for token in tokenize(template_string):
+            if token.token_type == TOKEN_VAR or token.token_type == TOKEN_BLOCK:
+                self.get_plain_strings(token)
+            if token.token_type == TOKEN_BLOCK:
+                self.get_contextual_strings(token)
+        self.createOutput(os.path.relpath(template_file.name), \
self.translatable_strings, outputfile) +
+    def createOutput(self, template_filename, translatable_strings, outputfile):
+      pass
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0d7bdcc..633cb37 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,3 +1,5 @@
+add_subdirectory(grantlee_plugin)
+
 set(kcalutils_SRCS
   htmlexport.cpp
   icaldrag.cpp
@@ -7,6 +9,8 @@ set(kcalutils_SRCS
   scheduler.cpp
   vcaldrag.cpp
   dndfactory.cpp
+  grantleeki18nlocalizer.cpp
+  grantleetemplatemanager.cpp
 )
 ecm_qt_declare_logging_category(kcalutils_SRCS HEADER kcalutils_debug.h IDENTIFIER \
KCALUTILS_LOG CATEGORY_NAME log_kcalutils)  
@@ -31,6 +35,7 @@ PRIVATE
   KF5::I18n
   KF5::IdentityManagement
   KF5::Codecs
+  Grantlee5::Templates
 )
 
 set_target_properties(KF5CalendarUtils PROPERTIES
diff --git a/src/Messages.sh b/src/Messages.sh
deleted file mode 100644
index 5f5de62..0000000
--- a/src/Messages.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /bin/sh
-$EXTRACTRC *.kcfg *.ui >> rc.cpp
-$XGETTEXT *.cpp -o $podir/libkcalutils5.pot
diff --git a/src/grantlee_plugin/CMakeLists.txt b/src/grantlee_plugin/CMakeLists.txt
new file mode 100644
index 0000000..4a95707
--- /dev/null
+++ b/src/grantlee_plugin/CMakeLists.txt
@@ -0,0 +1,19 @@
+kde_enable_exceptions()
+
+set(grantleeplugin_SRCS
+    kcalendargrantleeplugin.cpp
+    icon.cpp
+    datetimefilters.cpp
+)
+
+add_library(kcalendar_grantlee_plugin MODULE ${grantleeplugin_SRCS})
+grantlee_adjust_plugin_name(kcalendar_grantlee_plugin)
+target_link_libraries(kcalendar_grantlee_plugin
+    Grantlee5::Templates
+    KF5::IconThemes
+    KF5CalendarUtils
+)
+
+install(TARGETS kcalendar_grantlee_plugin
+        LIBRARY DESTINATION \
${LIB_INSTALL_DIR}/grantlee/${Grantlee5_VERSION_MAJOR}.${Grantlee5_VERSION_MINOR}/ +)
diff --git a/src/grantlee_plugin/datetimefilters.cpp \
b/src/grantlee_plugin/datetimefilters.cpp new file mode 100644
index 0000000..d87c1d5
--- /dev/null
+++ b/src/grantlee_plugin/datetimefilters.cpp
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "datetimefilters.h"
+#include "../incidenceformatter.h"
+
+#include <grantlee/safestring.h>
+
+#include <KDateTime>
+
+#include <QDebug>
+
+KDateFilter::KDateFilter()
+    : Grantlee::Filter()
+{
+}
+
+KDateFilter::~KDateFilter()
+{
+}
+
+QVariant KDateFilter::doFilter(const QVariant &input, const QVariant &argument, bool \
autoescape) const +{
+    Q_UNUSED(autoescape);
+
+    QDate date;
+    if (input.type() == QVariant::Date) {
+        date = input.toDate();
+    } else if (input.type() == QVariant::DateTime) {
+        date = input.toDateTime().date();
+    } else {
+        return QString();
+    }
+
+    const bool shortFmt = \
(argument.value<Grantlee::SafeString>().get().compare(QLatin1String("short"), \
Qt::CaseInsensitive) == 0); +    return \
Grantlee::SafeString(KCalUtils::IncidenceFormatter::dateToString(KDateTime(date), \
shortFmt)); +}
+
+bool KDateFilter::isSafe() const
+{
+    return true;
+}
+
+
+
+KTimeFilter::KTimeFilter()
+    : Grantlee::Filter()
+{
+}
+
+KTimeFilter::~KTimeFilter()
+{
+}
+
+QVariant KTimeFilter::doFilter(const QVariant &input, const QVariant &argument, bool \
autoescape) const +{
+    Q_UNUSED(autoescape);
+
+    QTime time;
+    if (input.type() == QVariant::Time) {
+        time = input.toTime();
+    } else if (input.type() == QVariant::DateTime) {
+        time = input.toDateTime().time();
+    } else {
+        return QString();
+    }
+
+    const bool shortFmt = \
(argument.value<Grantlee::SafeString>().get().compare(QLatin1String("short"), \
Qt::CaseInsensitive) == 0); +
+    return Grantlee::SafeString( \
KCalUtils::IncidenceFormatter::timeToString(KDateTime(QDate(), time), shortFmt)); +}
+
+bool KTimeFilter::isSafe() const
+{
+    return true;
+}
+
+
+
+KDateTimeFilter::KDateTimeFilter()
+    : Grantlee::Filter()
+{
+}
+
+KDateTimeFilter::~KDateTimeFilter()
+{
+}
+
+QVariant KDateTimeFilter::doFilter(const QVariant &input, const QVariant &argument, \
bool autoescape) const +{
+    Q_UNUSED(autoescape);
+
+    if (input.type() != QVariant::DateTime) {
+        return QString();
+    }
+    const QDateTime dt = input.toDateTime();
+
+    const QStringList arguments = \
argument.value<Grantlee::SafeString>().get().split(QLatin1Char(',')); +    const bool \
shortFmt = arguments.contains(QStringLiteral("short"), Qt::CaseInsensitive); +    \
const bool dateOnly = arguments.contains(QStringLiteral("dateonly"), \
Qt::CaseInsensitive); +
+    return Grantlee::SafeString(KCalUtils::IncidenceFormatter::dateTimeToString(KDateTime(dt), \
dateOnly, shortFmt)); +}
+
+bool KDateTimeFilter::isSafe() const
+{
+    return true;
+}
diff --git a/src/grantlee_plugin/datetimefilters.h \
b/src/grantlee_plugin/datetimefilters.h new file mode 100644
index 0000000..8d35c8c
--- /dev/null
+++ b/src/grantlee_plugin/datetimefilters.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef DATETIMEFILTERS_H
+#define DATETIMEFILTERS_H
+
+#include <grantlee/filter.h>
+
+class KDateFilter : public Grantlee::Filter
+{
+public:
+    explicit KDateFilter();
+    ~KDateFilter();
+
+    QVariant doFilter(const QVariant &input, const QVariant &argument = QVariant(),
+                      bool autoescape = false) const Q_DECL_OVERRIDE;
+    bool isSafe() const Q_DECL_OVERRIDE;
+};
+
+class KTimeFilter : public Grantlee::Filter
+{
+public:
+    explicit KTimeFilter();
+    ~KTimeFilter();
+
+    QVariant doFilter(const QVariant &input, const QVariant &argument = QVariant(),
+                      bool autoescape = false) const Q_DECL_OVERRIDE;
+    bool isSafe() const Q_DECL_OVERRIDE;
+};
+
+class KDateTimeFilter : public Grantlee::Filter
+{
+public:
+    explicit KDateTimeFilter();
+    ~KDateTimeFilter();
+
+    QVariant doFilter(const QVariant &input, const QVariant &argument = QVariant(),
+                      bool autoescape = false) const Q_DECL_OVERRIDE;
+    bool isSafe() const Q_DECL_OVERRIDE;
+};
+
+#endif // DATETIMEFILTERS_H
diff --git a/src/grantlee_plugin/icon.cpp b/src/grantlee_plugin/icon.cpp
new file mode 100644
index 0000000..b4293f9
--- /dev/null
+++ b/src/grantlee_plugin/icon.cpp
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "icon.h"
+
+#include <grantlee/exception.h>
+#include <grantlee/parser.h>
+#include <grantlee/variable.h>
+
+#include <KIconLoader>
+
+IconTag::IconTag(QObject* parent)
+    : Grantlee::AbstractNodeFactory(parent)
+{
+}
+
+IconTag::~IconTag()
+{
+}
+
+Grantlee::Node *IconTag::getNode(const QString &tagContent, Grantlee::Parser *p) \
const +{
+    Q_UNUSED(p);
+
+    static QHash<QString, int> sizeOrGroupLookup
+        = { { QStringLiteral("desktop"), KIconLoader::Desktop },
+            { QStringLiteral("toolbar"), KIconLoader::Toolbar },
+            { QStringLiteral("maintoolbar"), KIconLoader::MainToolbar },
+            { QStringLiteral("small"), KIconLoader::Small },
+            { QStringLiteral("panel"), KIconLoader::Panel },
+            { QStringLiteral("dialog"), KIconLoader::Dialog },
+            { QStringLiteral("sizesmall"), KIconLoader::SizeSmall },
+            { QStringLiteral("sizesmallmedium"), KIconLoader::SizeSmallMedium },
+            { QStringLiteral("sizemedium"), KIconLoader::SizeMedium },
+            { QStringLiteral("sizelarge"), KIconLoader::SizeLarge },
+            { QStringLiteral("sizehuge"), KIconLoader::SizeHuge },
+            { QStringLiteral("sizeenormous"), KIconLoader::SizeEnormous }
+        };
+
+    const QStringList parts = smartSplit(tagContent);
+    const int partsSize = parts.size();
+    if (partsSize < 2) {
+        throw Grantlee::Exception(Grantlee::TagSyntaxError, QStringLiteral("icon tag \
takes at least 1 argument")); +    }
+    if (partsSize > 4) {
+        throw Grantlee::Exception(Grantlee::TagSyntaxError, QStringLiteral("icon tag \
takes at maximum 3 arguments, %1 given").arg(partsSize)); +    }
+
+    int sizeOrGroup = KIconLoader::Small;
+    QString altText;
+    if (partsSize >= 3) {
+        const QString sizeStr = parts.at(2);
+        bool ok = false;
+        // Try to convert to pixel size
+        sizeOrGroup = sizeStr.toInt(&ok);
+        if (!ok) {
+            // If failed, then try to map the string to one of tne enums
+            const auto size = sizeOrGroupLookup.constFind(sizeStr);
+            if (size == sizeOrGroupLookup.cend()) {
+                // If it's not  a valid size string, assume it's an alt text
+                altText = sizeStr;
+            } else {
+                sizeOrGroup = (*size);
+            }
+        }
+    }
+    if (partsSize == 4) {
+        altText = parts.at(3);
+    }
+
+    return new IconNode(parts.at(1), sizeOrGroup, altText);
+}
+
+
+
+IconNode::IconNode(QObject* parent)
+    : Grantlee::Node(parent)
+{
+}
+
+IconNode::IconNode(const QString &iconName, int sizeOrGroup, const QString &altText, \
QObject *parent) +    : Grantlee::Node(parent)
+    , mIconName(iconName)
+    , mAltText(altText)
+    , mSizeOrGroup(sizeOrGroup)
+{
+}
+
+IconNode::~IconNode()
+{
+}
+
+void IconNode::render(Grantlee::OutputStream *stream, Grantlee::Context *c) const
+{
+    Q_UNUSED(c);
+
+    QString iconName = mIconName;
+    if (iconName.startsWith(QLatin1Char('"')) && \
iconName.endsWith(QLatin1Char('"'))) { +        iconName = iconName.mid(1, \
iconName.size() - 2); +    } else {
+        iconName = Grantlee::Variable(mIconName).resolve(c).toString();
+    }
+
+    QString altText;
+    if (!mAltText.isEmpty()) {
+        if (mAltText.startsWith(QLatin1Char('"')) && \
mAltText.endsWith(QLatin1Char('"'))) { +            altText = mAltText.mid(1, \
mAltText.size() - 2); +        } else {
+            const QVariant v = Grantlee::Variable(mAltText).resolve(c);
+            if (v.isValid()) {
+                if (v.canConvert<Grantlee::SafeString>()) {
+                    altText = v.value<Grantlee::SafeString>().get();
+                } else {
+                    altText = v.toString();
+                }
+            }
+        }
+    }
+
+    const QString html = QStringLiteral("<img src=\"file://%1\" align=\"top\" \
height=\"%2\" width=\"%2\" alt=\"%3\" title=\"%4\" />") +                            \
.arg(KIconLoader::global()->iconPath(iconName, mSizeOrGroup)) +                       \
.arg(mSizeOrGroup < KIconLoader::LastGroup ? +                                    \
IconSize(static_cast<KIconLoader::Group>(mSizeOrGroup)) +                             \
: mSizeOrGroup) +                            .arg(altText.isEmpty() ? iconName : \
altText) +                            .arg(altText); // title is intentionally blank \
if no alt is provided +    (*stream) << Grantlee::SafeString(html, \
Grantlee::SafeString::IsSafe); +}
diff --git a/src/grantlee_plugin/icon.h b/src/grantlee_plugin/icon.h
new file mode 100644
index 0000000..5b32802
--- /dev/null
+++ b/src/grantlee_plugin/icon.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef ICON_H
+#define ICON_H
+
+#include <grantlee/node.h>
+
+/**
+ * @name icon tag
+ * @brief Provides {% icon %} tag for inserting themed icons
+ *
+ * The syntax is:
+ * @code
+ * {% icon "icon-name"|var-with-icon-name [ sizeOrGroup ] [ alt text ] %}
+ * @endcode
+ *
+ * Where @p icon-name is a string literal with icon name, @p var-with-icon-name
+ * is a variable that contains a string with the icon name. @p sizeOrGrop is
+ * one of the KIconLoader::Group or KIconLoader::StdSizes enum values. The value
+ * is case-insensitive.
+ *
+ * The tag generates a full <img> HTML code:
+ * @code
+ * <img src="/usr/share/icons/[theme]/[type]/[size]/[icon-name].png" width="[width]" \
height="[height]"> + * @endcode
+ *
+ * The full path to the icon is resolved using KIconLoader::iconPath(). The
+ * @p width and @p height attributes are calculated based on current settings
+ * for icon sizes in KDE.
+ *
+ * @note Support for nested variables inside tags is non-standard for Grantlee
+ * tags, but makes it easier to use {% icon %} in sub-templates.
+ */
+
+
+class IconTag : public Grantlee::AbstractNodeFactory
+{
+public:
+    explicit IconTag(QObject *parent = Q_NULLPTR);
+    ~IconTag();
+
+    Grantlee::Node *getNode(const QString &tagContent, Grantlee::Parser *p) const \
Q_DECL_OVERRIDE; +};
+
+class IconNode : public Grantlee::Node
+{
+    Q_OBJECT
+public:
+    explicit IconNode(QObject *parent = Q_NULLPTR);
+    IconNode(const QString &iconName, int sizeOrGroup, const QString &altText, \
QObject *parent = Q_NULLPTR); +    ~IconNode();
+
+    void render(Grantlee::OutputStream *stream, Grantlee::Context *c) const \
Q_DECL_OVERRIDE; +
+private:
+    QString mIconName;
+    QString mAltText;
+    int mSizeOrGroup;
+};
+
+
+#endif // ICON_H
diff --git a/src/grantlee_plugin/kcalendargrantleeplugin.cpp \
b/src/grantlee_plugin/kcalendargrantleeplugin.cpp new file mode 100644
index 0000000..8f39754
--- /dev/null
+++ b/src/grantlee_plugin/kcalendargrantleeplugin.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "kcalendargrantleeplugin.h"
+#include "icon.h"
+#include "datetimefilters.h"
+
+KCalendarGrantleePlugin::KCalendarGrantleePlugin(QObject *parent)
+    : QObject(parent)
+    , Grantlee::TagLibraryInterface()
+{
+}
+
+KCalendarGrantleePlugin::~KCalendarGrantleePlugin()
+{
+}
+
+QHash<QString, Grantlee::AbstractNodeFactory *> \
KCalendarGrantleePlugin::nodeFactories(const QString &name) +{
+    Q_UNUSED(name);
+
+    QHash<QString, Grantlee::AbstractNodeFactory *> nodeFactories;
+    nodeFactories[QStringLiteral("icon")] = new IconTag();
+
+    return nodeFactories;
+}
+
+QHash<QString, Grantlee::Filter *> KCalendarGrantleePlugin::filters(const QString& \
name) +{
+    Q_UNUSED(name);
+
+    QHash<QString, Grantlee::Filter *> filters;
+    filters[QStringLiteral("kdate")] = new KDateFilter();
+    filters[QStringLiteral("ktime")] = new KTimeFilter();
+    filters[QStringLiteral("kdatetime")] = new KDateTimeFilter();
+
+    return filters;
+}
diff --git a/src/grantlee_plugin/kcalendargrantleeplugin.h \
b/src/grantlee_plugin/kcalendargrantleeplugin.h new file mode 100644
index 0000000..d6c1e14
--- /dev/null
+++ b/src/grantlee_plugin/kcalendargrantleeplugin.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef KCALENDARGRANTLEEPLUGIN_H
+#define KCALENDARGRANTLEEPLUGIN_H
+
+#include <grantlee/taglibraryinterface.h>
+
+class KCalendarGrantleePlugin : public QObject
+                              , public Grantlee::TagLibraryInterface
+{
+    Q_OBJECT
+    Q_INTERFACES(Grantlee::TagLibraryInterface)
+    Q_PLUGIN_METADATA(IID "org.kde.KCalendarGrantleePlugin")
+
+public:
+    explicit KCalendarGrantleePlugin(QObject *parent = Q_NULLPTR);
+    ~KCalendarGrantleePlugin();
+
+    QHash<QString, Grantlee::Filter *> filters(const QString  &name) \
Q_DECL_OVERRIDE; +    QHash<QString, Grantlee::AbstractNodeFactory *> \
nodeFactories(const QString &name) Q_DECL_OVERRIDE; +};
+
+#endif // KCALENDARGRANTLEEPLUGIN_H
diff --git a/src/grantleeki18nlocalizer.cpp b/src/grantleeki18nlocalizer.cpp
new file mode 100644
index 0000000..21a3c79
--- /dev/null
+++ b/src/grantleeki18nlocalizer.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "grantleeki18nlocalizer_p.h"
+#include "kcalutils_debug.h"
+
+#include <QLocale>
+#include <QDate>
+
+#include <grantlee/safestring.h>
+
+#include <KLocalizedString>
+
+GrantleeKi18nLocalizer::GrantleeKi18nLocalizer()
+    : Grantlee::QtLocalizer()
+{
+}
+
+GrantleeKi18nLocalizer::~GrantleeKi18nLocalizer()
+{
+}
+
+
+QString GrantleeKi18nLocalizer::processArguments(const KLocalizedString &kstr,
+                                                 const QVariantList &arguments) \
const +{
+    KLocalizedString str = kstr;
+    for (auto iter = arguments.cbegin(), end = arguments.cend(); iter != end; \
++iter) { +        switch (iter->type()) {
+        case QVariant::String:
+            str = str.subs(iter->toString());
+            break;
+        case QVariant::Int:
+            str = str.subs(iter->toInt());
+            break;
+        case QVariant::UInt:
+            str = str.subs(iter->toUInt());
+            break;
+        case QVariant::LongLong:
+            str = str.subs(iter->toLongLong());
+            break;
+        case QVariant::ULongLong:
+            str = str.subs(iter->toULongLong());
+            break;
+        case QVariant::Char:
+            str = str.subs(iter->toChar());
+            break;
+        case QVariant::Double:
+            str = str.subs(iter->toDouble());
+            break;
+        case QVariant::UserType:
+            if (iter->canConvert<Grantlee::SafeString>()) {
+                str = str.subs(iter->value<Grantlee::SafeString>().get());
+                break;
+            }
+            // fall-through
+        default:
+            qCWarning(KCALUTILS_LOG) << "Unknown type" << iter->typeName() << "(" << \
iter->type() << ")"; +            break;
+        }
+    }
+
+    // Return localized in the currenctly active locale
+    return str.toString({ currentLocale() });
+}
+
+QString GrantleeKi18nLocalizer::localizeContextString(const QString &string, const \
QString &context, const QVariantList &arguments) const +{
+    const KLocalizedString str = kxi18nc(qPrintable(context), qPrintable(string));
+    return processArguments(str, arguments);
+}
+
+QString GrantleeKi18nLocalizer::localizeString(const QString &string, const \
QVariantList &arguments) const +{
+    const KLocalizedString str = kxi18n(qPrintable(string));
+    return processArguments(str, arguments);
+}
+
+QString GrantleeKi18nLocalizer::localizePluralContextString(const QString &string, \
const QString &pluralForm, +                                                          \
const QString &context, const QVariantList &arguments) const +{
+    const KLocalizedString str = kxi18ncp(qPrintable(context), qPrintable(string), \
qPrintable(pluralForm)); +    return processArguments(str, arguments);
+}
+
+QString GrantleeKi18nLocalizer::localizePluralString(const QString &string, const \
QString &pluralForm, +                                                     const \
QVariantList &arguments) const +{
+    const KLocalizedString str = kxi18np(qPrintable(string), \
qPrintable(pluralForm)); +    return processArguments(str, arguments);
+}
+
+QString GrantleeKi18nLocalizer::localizeMonetaryValue(qreal value, const QString \
&currencySymbol) const +{
+    return QLocale(currentLocale()).toCurrencyString(value, currencySymbol);
+}
diff --git a/src/grantleeki18nlocalizer_p.h b/src/grantleeki18nlocalizer_p.h
new file mode 100644
index 0000000..edd1d90
--- /dev/null
+++ b/src/grantleeki18nlocalizer_p.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GRANTLEEKI18NLOCALIZER_H
+#define GRANTLEEKI18NLOCALIZER_H
+
+#include <grantlee/qtlocalizer.h>
+
+#include <QLocale>
+
+class KLocalizedString;
+
+class GrantleeKi18nLocalizer : public Grantlee::QtLocalizer
+{
+public:
+    explicit GrantleeKi18nLocalizer();
+    ~GrantleeKi18nLocalizer();
+
+    // Only reimplement string localization to use KLocalizedString instead of
+    // tr(), the remaining methods use QLocale internally, so we can reuse them
+    QString localizeContextString(const QString &string, const QString &context,
+                                  const QVariantList &arguments) const \
Q_DECL_OVERRIDE; +    QString localizeString(const QString &string, const \
QVariantList &arguments) const Q_DECL_OVERRIDE; +    QString \
localizePluralContextString(const QString &string, const QString &pluralForm, +       \
const QString &context, const QVariantList &arguments) const Q_DECL_OVERRIDE; +    \
QString localizePluralString(const QString &string, const QString &pluralForm, +      \
const QVariantList &arguments) const Q_DECL_OVERRIDE; +
+    // Only exception, Grantlee's implementation is not using QLocale for this
+    // for some reason
+    QString localizeMonetaryValue(qreal value, const QString &currenctCode) const \
Q_DECL_OVERRIDE; +
+private:
+    QString processArguments(const KLocalizedString &str,
+                             const QVariantList &arguments) const;
+};
+
+#endif // GRANTLEEKI18NLOCALIZER_H
diff --git a/src/grantleetemplatemanager.cpp b/src/grantleetemplatemanager.cpp
new file mode 100644
index 0000000..fa81fd2
--- /dev/null
+++ b/src/grantleetemplatemanager.cpp
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "grantleetemplatemanager_p.h"
+#include "grantleeki18nlocalizer_p.h"
+
+#include <QString>
+#include <QStandardPaths>
+#include <QDebug>
+
+#include <grantlee/engine.h>
+#include <grantlee/template.h>
+#include <grantlee/templateloader.h>
+
+#include <KLocalizedString>
+
+GrantleeTemplateManager *GrantleeTemplateManager::sInstance = Q_NULLPTR;
+
+GrantleeTemplateManager::GrantleeTemplateManager()
+    : mEngine(new Grantlee::Engine)
+    , mLoader(new Grantlee::FileSystemTemplateLoader)
+    , mLocalizer(new GrantleeKi18nLocalizer)
+{
+    const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, \
QStringLiteral("kcalendar/templates"), +                                              \
QStandardPaths::LocateDirectory); +    if (path.isEmpty()) {
+        qFatal("Cannot find KCalendarUtils templates, check your instalation");
+    }
+
+    mLoader->setTemplateDirs({ path });
+    mLoader->setTheme(QStringLiteral("default"));
+    mEngine->addTemplateLoader(mLoader);
+    mEngine->addDefaultLibrary(QStringLiteral("grantlee_i18ntags"));
+    mEngine->addDefaultLibrary(QStringLiteral("kcalendar_grantlee_plugin"));
+    mEngine->setSmartTrimEnabled(true);
+}
+
+GrantleeTemplateManager::~GrantleeTemplateManager()
+{
+    delete mEngine;
+}
+
+GrantleeTemplateManager * GrantleeTemplateManager::instance()
+{
+    if (!sInstance) {
+        sInstance = new GrantleeTemplateManager;
+    }
+    return sInstance;
+}
+
+void GrantleeTemplateManager::setTemplatePath(const QString &path)
+{
+    mLoader->setTemplateDirs({ path });
+    mLoader->setTheme(QString());
+}
+
+void GrantleeTemplateManager::setPluginPath(const QString &path)
+{
+    QStringList pluginPaths = mEngine->pluginPaths();
+    pluginPaths.prepend(path);
+    mEngine->setPluginPaths(pluginPaths);
+}
+
+Grantlee::Context GrantleeTemplateManager::createContext(const QVariantHash &hash) \
const +{
+    Grantlee::Context ctx;
+    ctx.insert(QStringLiteral("incidence"), hash);
+    ctx.setLocalizer(mLocalizer);
+    return ctx;
+}
+
+QString GrantleeTemplateManager::errorTemplate(const QString &reason,
+                                       const QString &origTemplateName,
+                                       const Grantlee::Template &failedTemplate) \
const +{
+    Grantlee::Template tpl = mEngine->newTemplate(
+        QStringLiteral("<h1>{{ error }}</h1>\n"
+                       "<b>%1:</b> {{ templateName }}<br>\n"
+                       "<b>%2:</b> {{ errorMessage }}")
+            .arg(i18n("Template"))
+            .arg(i18n("Error message")),
+        QStringLiteral("TemplateError"));
+
+    Grantlee::Context ctx = createContext();
+    ctx.insert(QStringLiteral("error"), reason);
+    ctx.insert(QStringLiteral("templateName"), origTemplateName);
+    ctx.insert(QStringLiteral("errorMessage"), failedTemplate->errorString());
+    return tpl->render(&ctx);
+}
+
+QString GrantleeTemplateManager::render(const QString &templateName, const \
QVariantHash &data) const +{
+    if (!mLoader->canLoadTemplate(templateName)) {
+        qWarning() << "Cannot load template" << templateName << ", please check your \
installation"; +        return QString();
+    }
+
+    Grantlee::Template tpl = mLoader->loadByName(templateName, mEngine);
+    if (tpl->error()) {
+        return errorTemplate(i18n("Template parsing error"), templateName, tpl);
+    }
+
+    Grantlee::Context ctx = createContext(data);
+    const QString result = tpl->render(&ctx);
+    if (tpl->error()) {
+        return errorTemplate(i18n("Template rendering error"), templateName, tpl);
+    }
+
+    return result;
+}
diff --git a/src/grantleetemplatemanager_p.h b/src/grantleetemplatemanager_p.h
new file mode 100644
index 0000000..473d4a2
--- /dev/null
+++ b/src/grantleetemplatemanager_p.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GRANTLEETEMPLATEMANAGER_H_P
+#define GRANTLEETEMPLATEMANAGER_H_P
+
+#include <QSharedPointer>
+
+namespace Grantlee {
+class Engine;
+class FileSystemTemplateLoader;
+class TemplateImpl;
+class Context;
+typedef QSharedPointer<TemplateImpl> Template;
+}
+
+class QString;
+class GrantleeKi18nLocalizer;
+
+class GrantleeTemplateManager
+{
+public:
+    ~GrantleeTemplateManager();
+
+    static GrantleeTemplateManager *instance();
+
+    void setTemplatePath(const QString &path);
+    void setPluginPath(const QString &path);
+
+    QString render(const QString &templateName, const QVariantHash &data) const;
+
+private:
+    GrantleeTemplateManager();
+
+    QString errorTemplate(const QString &reason,
+                          const QString &origTemplateName,
+                          const Grantlee::Template &failedTemplate) const;
+    Grantlee::Context createContext(const QVariantHash &hash = QVariantHash()) \
const; +
+    Grantlee::Engine *mEngine;
+    QSharedPointer<Grantlee::FileSystemTemplateLoader> mLoader;
+    QSharedPointer<GrantleeKi18nLocalizer> mLocalizer;
+
+    static GrantleeTemplateManager *sInstance;
+};
+
+#endif // TEMPLATEMANAGER_H_P
diff --git a/src/incidenceformatter.cpp b/src/incidenceformatter.cpp
index c58e961..052c042 100644
--- a/src/incidenceformatter.cpp
+++ b/src/incidenceformatter.cpp
@@ -35,6 +35,7 @@
 */
 #include "incidenceformatter.h"
 #include "stringify.h"
+#include "grantleetemplatemanager_p.h"
 
 #include <kcalcore/event.h>
 #include <kcalcore/freebusy.h>
@@ -274,34 +275,32 @@ static QString firstAttendeeName(const Incidence::Ptr \
&incidence, const QString  return name;
 }
 
-static QString rsvpStatusIconPath(Attendee::PartStat status)
+static QString rsvpStatusIconName(Attendee::PartStat status)
 {
-    QString iconPath;
     switch (status) {
     case Attendee::Accepted:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("dialog-ok-apply"), \
KIconLoader::Small); +        return QStringLiteral("dialog-ok-apply");
         break;
     case Attendee::Declined:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("dialog-cancel"), \
KIconLoader::Small); +        return QStringLiteral("dialog-cancel");
         break;
     case Attendee::NeedsAction:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("help-about"), \
KIconLoader::Small); +        return QStringLiteral("help-about");
         break;
     case Attendee::InProcess:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("help-about"), \
KIconLoader::Small); +        return QStringLiteral("help-about");
         break;
     case Attendee::Tentative:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("dialog-ok"), \
KIconLoader::Small); +        return QStringLiteral("dialog-ok");
         break;
     case Attendee::Delegated:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("mail-forward"), \
KIconLoader::Small); +        return QStringLiteral("mail-forward");
         break;
     case Attendee::Completed:
-        iconPath = KIconLoader::global()->iconPath(QStringLiteral("mail-mark-read"), \
KIconLoader::Small); +        return QStringLiteral("mail-mark-read");
     default:
-        break;
+        return QString();
     }
-    return iconPath;
 }
 
 //@endcond
@@ -311,39 +310,41 @@ static QString rsvpStatusIconPath(Attendee::PartStat status)
  *******************************************************************/
 
 //@cond PRIVATE
-static QString displayViewFormatPerson(const QString &email, const QString &name,
-                                       const QString &uid, const QString &iconPath)
+static QVariantHash displayViewFormatPerson(const QString &email, const QString \
&name, +                                            const QString &uid, const QString \
&iconName)  {
     // Search for new print name or uid, if needed.
     QPair<QString, QString> s = searchNameAndUid(email, name, uid);
     const QString printName = s.first;
     const QString printUid = s.second;
 
-    QString personString;
-    if (!iconPath.isEmpty()) {
-        personString += QLatin1String("<img valign=\"top\" src=\"") + iconPath + \
                QLatin1String("\">") + QLatin1String("&nbsp;");
-    }
-
-    // Make the uid link
-    if (!printUid.isEmpty()) {
-        personString += htmlAddUidLink(email, printName, printUid);
-    } else {
-        // No UID, just show some text
-        personString += (printName.isEmpty() ? email : printName);
-    }
+    QVariantHash personData;
+    personData[QStringLiteral("icon")] = iconName;
+    personData[QStringLiteral("uid")] = printUid;
+    personData[QStringLiteral("name")] = printName;
+    personData[QStringLiteral("email")] = email;
 
     // Make the mailto link
     if (!email.isEmpty()) {
-        personString += QLatin1String("&nbsp;") + htmlAddMailtoLink(email, \
printName); +        Person person(name, email);
+        QString path = person.fullName().simplified();
+        if (path.isEmpty() || path.startsWith(QLatin1Char('"'))) {
+            path = email;
+        }
+        QUrl mailto;
+        mailto.setScheme(QStringLiteral("mailto"));
+        mailto.setPath(path);
+
+        personData[QStringLiteral("mailto")] = mailto.url();
     }
 
-    return personString;
+    return personData;
 }
 
-static QString displayViewFormatPerson(const QString &email, const QString &name,
-                                       const QString &uid, Attendee::PartStat \
status) +static QVariantHash displayViewFormatPerson(const QString &email, const \
QString &name, +                                            const QString &uid, \
Attendee::PartStat status)  {
-    return displayViewFormatPerson(email, name, uid, rsvpStatusIconPath(status));
+    return displayViewFormatPerson(email, name, uid, rsvpStatusIconName(status));
 }
 
 static bool incOrganizerOwnsCalendar(const Calendar::Ptr &calendar,
@@ -358,32 +359,28 @@ static bool incOrganizerOwnsCalendar(const Calendar::Ptr \
&calendar,  
 static QString displayViewFormatDescription(const Incidence::Ptr &incidence)
 {
-    QString tmpStr;
     if (!incidence->description().isEmpty()) {
-        QString descStr;
         if (!incidence->descriptionIsRich() &&
                 !incidence->description().startsWith(QLatin1String("<!DOCTYPE \
                HTML"))) {
-            descStr = string2HTML(incidence->description());
+            return string2HTML(incidence->description());
+        } else if (!incidence->description().startsWith(QLatin1String("<!DOCTYPE \
HTML"))) { +            return incidence->richDescription();
         } else {
-            if (!incidence->description().startsWith(QLatin1String("<!DOCTYPE \
                HTML"))) {
-                descStr = incidence->richDescription();
-            } else {
-                descStr = incidence->description();
-            }
+            return incidence->description();
         }
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Description:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + descStr + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
     }
-    return tmpStr;
+
+    return QString();
 }
 
 
-static QString displayViewFormatAttendeeRoleList(const Incidence::Ptr &incidence, \
                Attendee::Role role,
-        bool showStatus)
+static QVariantList displayViewFormatAttendeeRoleList(const Incidence::Ptr \
&incidence, +                                                      Attendee::Role \
role, +                                                      bool showStatus)
 {
-    QString tmpStr;
+    QVariantList attendeeDataList;
+    attendeeDataList.reserve(incidence->attendeeCount());
+
     Attendee::List::ConstIterator it;
     Attendee::List attendees = incidence->attendees();
 
@@ -397,26 +394,50 @@ static QString displayViewFormatAttendeeRoleList(const \
Incidence::Ptr &incidence  // skip attendee that is also the organizer
             continue;
         }
-        tmpStr += displayViewFormatPerson(a->email(), a->name(), a->uid(),
-                                          showStatus ? a->status() : \
Attendee::None); +        QVariantHash attendeeData = \
displayViewFormatPerson(a->email(), a->name(), a->uid(), +                            \
showStatus ? a->status() : Attendee::None);  if (!a->delegator().isEmpty()) {
-            tmpStr += i18n(" (delegated by %1)", a->delegator());
+            attendeeData[QStringLiteral("delegator")] = a->delegator();
         }
         if (!a->delegate().isEmpty()) {
-            tmpStr += i18n(" (delegated to %1)", a->delegate());
+            attendeeData[QStringLiteral("delegate")] = a->delegate();
         }
-        tmpStr += QLatin1String("<br>");
-    }
-    if (tmpStr.endsWith(QLatin1String("<br>"))) {
-        tmpStr.chop(4);
+        if (showStatus) {
+            switch (a->status()) {
+            case Attendee::NeedsAction:
+                attendeeData[QStringLiteral("status")] = i18n("Needs action");
+                break;
+            case Attendee::Accepted:
+                attendeeData[QStringLiteral("status")] = i18n("Accepted");
+                break;
+            case Attendee::Declined:
+                attendeeData[QStringLiteral("status")] = i18n("Declined");
+                break;
+            case Attendee::Tentative:
+                attendeeData[QStringLiteral("status")] = i18n("Tentative");
+                break;
+            case Attendee::Delegated:
+                attendeeData[QStringLiteral("status")] = i18n("Delegated");
+                break;
+            case Attendee::Completed:
+                attendeeData[QStringLiteral("status")] = i18n("Completed");
+                break;
+            case Attendee::InProcess:
+                attendeeData[QStringLiteral("status")] = i18n("In Process");
+                break;
+            case Attendee::None:
+                break;
+            }
+        }
+
+        attendeeDataList << attendeeData;
     }
-    return tmpStr;
+
+    return attendeeDataList;
 }
 
-static QString displayViewFormatAttendees(const Calendar::Ptr &calendar, const \
Incidence::Ptr &incidence) +static QVariantHash displayViewFormatOrganizer(const \
Incidence::Ptr &incidence)  {
-    QString tmpStr, str;
-
     // Add organizer link
     int attendeeCount = incidence->attendees().count();
     if (attendeeCount > 1 ||
@@ -426,67 +447,24 @@ static QString displayViewFormatAttendees(const Calendar::Ptr \
                &calendar, const I
         QPair<QString, QString> s = \
searchNameAndUid(incidence->organizer()->email(),  incidence->organizer()->name(),
                                     QString());
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Organizer:") + \
                QLatin1String("</b></td>");
-        const QString iconPath =
-            KIconLoader::global()->iconPath(QStringLiteral("meeting-organizer"), \
                KIconLoader::Small);
-        tmpStr += QLatin1String("<td>") + \
                displayViewFormatPerson(incidence->organizer()->email(),
-                  s.first, s.second, iconPath) +
-                  QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    // Show the attendee status if the incidence's organizer owns the resource \
                calendar,
-    // which means they are running the show and have all the up-to-date response \
                info.
-    bool showStatus = incOrganizerOwnsCalendar(calendar, incidence);
-
-    // Add "chair"
-    str = displayViewFormatAttendeeRoleList(incidence, Attendee::Chair, showStatus);
-    if (!str.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Chair:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + str + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
+        return displayViewFormatPerson(incidence->organizer()->email(), s.first, \
s.second, +                                       \
QStringLiteral("meeting-organizer"));  }
 
-    // Add required participants
-    str = displayViewFormatAttendeeRoleList(incidence, Attendee::ReqParticipant, \
                showStatus);
-    if (!str.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Required Participants:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + str + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    // Add optional participants
-    str = displayViewFormatAttendeeRoleList(incidence, Attendee::OptParticipant, \
                showStatus);
-    if (!str.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Optional Participants:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + str + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    // Add observers
-    str = displayViewFormatAttendeeRoleList(incidence, Attendee::NonParticipant, \
                showStatus);
-    if (!str.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Observers:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + str + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    return tmpStr;
+    return QVariantHash();
 }
 
-static QString displayViewFormatAttachments(const Incidence::Ptr &incidence)
+static QVariantList displayViewFormatAttachments(const Incidence::Ptr &incidence)
 {
-    QString tmpStr;
-    Attachment::List as = incidence->attachments();
-    Attachment::List::ConstIterator it;
+    const Attachment::List as = incidence->attachments();
+
+    QVariantList dataList;
+    dataList.reserve(as.count());
+
     int count = 0;
-    for (it = as.constBegin(); it != as.constEnd(); ++it) {
+    for (auto it = as.cbegin(), end = as.cend(); it != end; ++it) {
         count++;
+        QVariantHash attData;
         if ((*it)->isUri()) {
             QString name;
             if ((*it)->uri().startsWith(QLatin1String("kmail:"))) {
@@ -498,91 +476,53 @@ static QString displayViewFormatAttachments(const \
Incidence::Ptr &incidence)  name = (*it)->label();
                 }
             }
-            tmpStr += htmlAddLink((*it)->uri(), name);
+            attData[QStringLiteral("uri")] = (*it)->uri();
+            attData[QStringLiteral("label")] = name;
         } else {
-            tmpStr += htmlAddLink(QStringLiteral("ATTACH:%1").
-                                  \
                arg(QString::fromUtf8((*it)->label().toUtf8().toBase64())),
-                                  (*it)->label());
-        }
-        if (count < as.count()) {
-            tmpStr += QLatin1String("<br>");
+            attData[QStringLiteral("uri")] = \
QStringLiteral("ATTACH:%1").arg(QString::fromUtf8((*it)->label().toUtf8().toBase64()));
 +            attData[QStringLiteral("label")] = (*it)->label();
         }
+        dataList << attData;
     }
-    return tmpStr;
+    return dataList;
 }
 
-static QString displayViewFormatCategories(const Incidence::Ptr &incidence)
-{
-    // We do not use Incidence::categoriesStr() since it does not have whitespace
-    return incidence->categories().join(QStringLiteral(", "));
-}
-
-static QString displayViewFormatCreationDate(const Incidence::Ptr &incidence, const \
                KDateTime::Spec &spec)
-{
-    KDateTime kdt = incidence->created().toTimeSpec(spec);
-    return i18n("Creation date: %1", dateTimeToString(incidence->created(), false, \
                true, spec));
-}
-
-static QString displayViewFormatBirthday(const Event::Ptr &event)
+static QVariantHash displayViewFormatBirthday(const Event::Ptr &event)
 {
     if (!event) {
-        return QString();
-    }
-    if (event->customProperty("KABC", "BIRTHDAY") != QLatin1String("YES") &&
-            event->customProperty("KABC", "ANNIVERSARY") != QLatin1String("YES")) {
-        return QString();
+        return QVariantHash();
     }
 
+    // It's callees duty to ensure this
+    Q_ASSERT(event->customProperty("KABC", "BIRTHDAY") == QLatin1String("YES") ||
+            event->customProperty("KABC", "ANNIVERSARY") == QLatin1String("YES"));
+
     const QString uid_1 = event->customProperty("KABC", "UID-1");
     const QString name_1 = event->customProperty("KABC", "NAME-1");
     const QString email_1 = event->customProperty("KABC", "EMAIL-1");
-    KCalCore::Person::Ptr p = Person::fromFullName(email_1);
-    const QString tmpStr = displayViewFormatPerson(p->email(), name_1, uid_1, \
                QString());
-    return tmpStr;
+    const KCalCore::Person::Ptr p = Person::fromFullName(email_1);
+    return displayViewFormatPerson(p->email(), name_1, uid_1, QString());
 }
 
-static QString displayViewFormatHeader(const Incidence::Ptr &incidence)
+static QVariantHash incidenceTemplateHeader(const Incidence::Ptr &incidence)
 {
-    QString tmpStr = QStringLiteral("<table><tr>");
-
-    // show icons
-    KIconLoader *iconLoader = KIconLoader::global();
-    tmpStr += QLatin1String("<td>");
-
+    QVariantHash incidenceData;
     QString iconPath;
     if (incidence->customProperty("KABC", "BIRTHDAY") == QLatin1String("YES")) {
-        iconPath = iconLoader->iconPath(QStringLiteral("view-calendar-birthday"), \
KIconLoader::Small); +        incidenceData[QStringLiteral("icon")] = \
                QStringLiteral("view-calendar-birthday");
     } else if (incidence->customProperty("KABC", "ANNIVERSARY") == \
                QLatin1String("YES")) {
-        iconPath = iconLoader->iconPath(QStringLiteral("view-calendar-wedding-anniversary"), \
KIconLoader::Small); +        incidenceData[QStringLiteral("icon")] = \
QStringLiteral("view-calendar-wedding-anniversary");  } else {
-        iconPath = iconLoader->iconPath(incidence->iconName(), KIconLoader::Small);
+        incidenceData[QStringLiteral("icon")] = incidence->iconName();
     }
-    tmpStr += QLatin1String("<img valign=\"top\" src=\"") + iconPath + \
QLatin1String("\">");  
-    if (incidence->hasEnabledAlarms()) {
-        tmpStr += QLatin1String("<img valign=\"top\" src=\"") +
-                  iconLoader->iconPath(QStringLiteral("preferences-desktop-notification-bell"), \
                KIconLoader::Small) +
-                  QLatin1String("\">");
-    }
-    if (incidence->recurs()) {
-        tmpStr += QLatin1String("<img valign=\"top\" src=\"") +
-                  iconLoader->iconPath(QStringLiteral("edit-redo"), \
                KIconLoader::Small) +
-                  QLatin1String("\">");
-    }
-    if (incidence->isReadOnly()) {
-        tmpStr += QLatin1String("<img valign=\"top\" src=\"") +
-                  iconLoader->iconPath(QStringLiteral("object-locked"), \
                KIconLoader::Small) +
-                  QLatin1String("\">");
-    }
-    tmpStr += QLatin1String("</td>");
-
-    tmpStr += QLatin1String("<td>");
-    tmpStr += QLatin1String("<b><u>") + incidence->richSummary() + \
                QLatin1String("</u></b>");
-    tmpStr += QLatin1String("</td>");
-
-    tmpStr += QLatin1String("</tr></table>");
+    incidenceData[QStringLiteral("hasEnabledAlarms")] = \
incidence->hasEnabledAlarms(); +    incidenceData[QStringLiteral("recurs")] = \
incidence->recurs(); +    incidenceData[QStringLiteral("isReadOnly")] = \
incidence->isReadOnly(); +    incidenceData[QStringLiteral("summary")] = \
incidence->summary(); +    incidenceData[QStringLiteral("allDay")] = \
incidence->allDay();  
-    return tmpStr;
+    return incidenceData;
 }
 
 static QString displayViewFormatEvent(const Calendar::Ptr &calendar, const QString \
&sourceName, @@ -593,26 +533,11 @@ static QString displayViewFormatEvent(const \
Calendar::Ptr &calendar, const QStri  return QString();
     }
 
-    QString tmpStr = displayViewFormatHeader(event);
-
-    tmpStr += QLatin1String("<table>");
-    tmpStr += QLatin1String("<col width=\"25%\"/>");
-    tmpStr += QLatin1String("<col width=\"75%\"/>");
+    QVariantHash incidence = incidenceTemplateHeader(event);
 
-    const QString calStr = calendar ? resourceString(calendar, event) : sourceName;
-    if (!calStr.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Calendar:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + calStr + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
+    incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, \
event) : sourceName; +    incidence[QStringLiteral("location")] = \
event->richLocation();  
-    if (!event->location().isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Location:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + event->richLocation() + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
 
     KDateTime startDt = event->dtStart();
     KDateTime endDt = event->dtEnd();
@@ -632,133 +557,47 @@ static QString displayViewFormatEvent(const Calendar::Ptr \
&calendar, const QStri  }
     }
 
-    tmpStr += QLatin1String("<tr>");
-    if (event->allDay()) {
-        if (event->isMultiDay()) {
-            tmpStr += QLatin1String("<td><b>") + i18n("Date:") + \
                QLatin1String("</b></td>");
-            tmpStr += QLatin1String("<td>") +
-                      i18nc("<beginTime> - <endTime>", "%1 - %2",
-                            dateToString(startDt, false, spec),
-                            dateToString(endDt, false, spec)) +
-                      QLatin1String("</td>");
-        } else {
-            tmpStr += QLatin1String("<td><b>") + i18n("Date:") + \
                QLatin1String("</b></td>");
-            tmpStr += QLatin1String("<td>") +
-                      i18nc("date as string", "%1",
-                            dateToString(startDt, false, spec)) +
-                      QLatin1String("</td>");
-        }
-    } else {
-        if (event->isMultiDay()) {
-            tmpStr += QLatin1String("<td><b>") + i18n("Date:") + \
                QLatin1String("</b></td>");
-            tmpStr += QLatin1String("<td>") +
-                      i18nc("<beginTime> - <endTime>", "%1 - %2",
-                            dateToString(startDt, false, spec),
-                            dateToString(endDt, false, spec)) +
-                      QLatin1String("</td>");
-        } else {
-            tmpStr += QLatin1String("<td><b>") + i18n("Date:") + \
                QLatin1String("</b></td>");
-            tmpStr += QLatin1String("<td>") +
-                      i18nc("date as string", "%1",
-                            dateToString(startDt, false, spec)) +
-                      QLatin1String("</td>");
-
-            tmpStr += QLatin1String("</tr><tr>");
-            tmpStr += QLatin1String("<td><b>") + i18n("Time:") + \
                QLatin1String("</b></td>");
-            if (event->hasEndDate() && startDt != endDt) {
-                tmpStr += QLatin1String("<td>") +
-                          i18nc("<beginTime> - <endTime>", "%1 - %2",
-                                timeToString(startDt, true, spec),
-                                timeToString(endDt, true, spec)) +
-                          QLatin1String("</td>");
-            } else {
-                tmpStr += QLatin1String("<td>") +
-                          timeToString(startDt, true, spec) +
-                          QLatin1String("</td>");
-            }
-        }
-    }
-    tmpStr += QLatin1String("</tr>");
-
-    QString durStr = durationString(event);
-    if (!durStr.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Duration:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + durStr + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
+    if (spec.isValid()) {
+        startDt = startDt.toTimeSpec(spec);
+        endDt = endDt.toTimeSpec(spec);
     }
 
-    if (event->recurs() || event->hasRecurrenceId()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Recurrence:") + \
                QLatin1String("</b></td>");
-
-        QString str;
-        if (event->hasRecurrenceId()) {
-            str = i18n("Exception");
-        } else {
-            str = recurrenceString(event);
-        }
+    incidence[QStringLiteral("isAllDay")] = event->allDay();
+    incidence[QStringLiteral("isMultiDay")] = event->isMultiDay();
+    incidence[QStringLiteral("startDate")] = startDt.date();
+    incidence[QStringLiteral("endDate")] = endDt.date();
+    incidence[QStringLiteral("startTime")] = startDt.time();
+    incidence[QStringLiteral("endTime")] = endDt.time();
+    incidence[QStringLiteral("duration")] = durationString(event);
+    incidence[QStringLiteral("isException")] = event->hasRecurrenceId();
+    incidence[QStringLiteral("recurrence")] = recurrenceString(event);
 
-        tmpStr += QLatin1String("<td>") + str +
-                  QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
+    if (event->customProperty("KABC", "BIRTHDAY") == QLatin1String("YES")) {
+        incidence[QStringLiteral("birthday")] = displayViewFormatBirthday(event);
     }
 
-    const bool isBirthday = event->customProperty("KABC", "BIRTHDAY") == \
                QLatin1String("YES");
-    const bool isAnniversary = event->customProperty("KABC", "ANNIVERSARY") == \
                QLatin1String("YES");
-
-    if (isBirthday || isAnniversary) {
-        tmpStr += QLatin1String("<tr>");
-        if (isAnniversary) {
-            tmpStr += QLatin1String("<td><b>") + i18n("Anniversary:") + \
                QLatin1String("</b></td>");
-        } else {
-            tmpStr += QLatin1String("<td><b>") + i18n("Birthday:") + \
                QLatin1String("</b></td>");
-        }
-        tmpStr += QLatin1String("<td>") + displayViewFormatBirthday(event) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-        tmpStr += QLatin1String("</table>");
-        return tmpStr;
+    if (event->customProperty("KABC", "ANNIVERSARY") == QLatin1String("YES")) {
+        incidence[QStringLiteral("anniversary")] = displayViewFormatBirthday(event);
     }
 
-    tmpStr += displayViewFormatDescription(event);
+    incidence[QStringLiteral("description")] = displayViewFormatDescription(event);
     // TODO: print comments?
 
-    int reminderCount = event->alarms().count();
-    if (reminderCount > 0 && event->hasEnabledAlarms()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18np("Reminder:", "Reminders:", reminderCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + \
                reminderStringList(event).join(QStringLiteral("<br>")) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    tmpStr += displayViewFormatAttendees(calendar, event);
+    incidence[QStringLiteral("reminders")] = reminderStringList(event);
 
-    int categoryCount = event->categories().count();
-    if (categoryCount > 0) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>");
-        tmpStr += i18np("Category:", "Categories:", categoryCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + displayViewFormatCategories(event) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
+    incidence[QStringLiteral("organizer")] = displayViewFormatOrganizer(event);
+    const bool showStatus = incOrganizerOwnsCalendar(calendar, event);
+    incidence[QStringLiteral("chair")] = displayViewFormatAttendeeRoleList(event, \
Attendee::Chair, showStatus); +    incidence[QStringLiteral("requiredParticipants")] \
= displayViewFormatAttendeeRoleList(event, Attendee::ReqParticipant, showStatus); +   \
incidence[QStringLiteral("optionalParticipants")] = \
displayViewFormatAttendeeRoleList(event, Attendee::OptParticipant, showStatus); +    \
incidence[QStringLiteral("observers")] = displayViewFormatAttendeeRoleList(event, \
Attendee::NonParticipant, showStatus);  
-    int attachmentCount = event->attachments().count();
-    if (attachmentCount > 0) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18np("Attachment:", "Attachments:", attachmentCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + displayViewFormatAttachments(event) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-    tmpStr += QLatin1String("</table>");
+    incidence[QStringLiteral("categories")] = event->categories();
 
-    tmpStr += QLatin1String("<p><em>") + displayViewFormatCreationDate(event, spec) \
+ QLatin1String("</em>"); +    incidence[QStringLiteral("attachments")] = \
displayViewFormatAttachments(event); +    incidence[QStringLiteral("creationDate")] = \
event->created().toTimeSpec(spec).dateTime();  
-    return tmpStr;
+    return GrantleeTemplateManager::instance()->render(QStringLiteral("event.html"), \
incidence);  }
 
 static QString displayViewFormatTodo(const Calendar::Ptr &calendar, const QString \
&sourceName, @@ -770,26 +609,10 @@ static QString displayViewFormatTodo(const \
Calendar::Ptr &calendar, const QStrin  return QString();
     }
 
-    QString tmpStr = displayViewFormatHeader(todo);
+    QVariantHash incidence = incidenceTemplateHeader(todo);
 
-    tmpStr += QLatin1String("<table>");
-    tmpStr += QLatin1String("<col width=\"25%\"/>");
-    tmpStr += QLatin1String("<col width=\"75%\"/>");
-
-    const QString calStr = calendar ? resourceString(calendar, todo) : sourceName;
-    if (!calStr.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Calendar:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + calStr + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    if (!todo->location().isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Location:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + todo->richLocation() + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
+    incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, \
todo) : sourceName; +    incidence[QStringLiteral("location")] = \
todo->richLocation();  
     const bool hastStartDate = todo->hasStartDate();
     const bool hasDueDate = todo->hasDueDate();
@@ -811,14 +634,10 @@ static QString displayViewFormatTodo(const Calendar::Ptr \
&calendar, const QStrin  startDt.setDate(ocurrenceDueDate);
             }
         }
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18nc("to-do start date/time", "Start:") +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") +
-                  dateTimeToString(startDt, todo->allDay(), false, spec) +
-                  QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
+        if (spec.isValid()) {
+            startDt = startDt.toTimeSpec(spec);
+        }
+        incidence[QStringLiteral("startDate")] = startDt.dateTime();
     }
 
     if (hasDueDate) {
@@ -830,100 +649,42 @@ static QString displayViewFormatTodo(const Calendar::Ptr \
                &calendar, const QStrin
                 dueDt.setDate(todo->recurrence()->getNextDateTime(kdt).date());
             }
         }
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18nc("to-do due date/time", "Due:") +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") +
-                  dateTimeToString(dueDt, todo->allDay(), false, spec) +
-                  QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    QString durStr = durationString(todo);
-    if (!durStr.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Duration:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + durStr + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    if (todo->recurs() || todo->hasRecurrenceId()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Recurrence:") + \
                QLatin1String("</b></td>");
-        QString str;
-        if (todo->hasRecurrenceId()) {
-            str = i18n("Exception");
-        } else {
-            str = recurrenceString(todo);
+        if (spec.isValid()) {
+            dueDt = dueDt.toTimeSpec(spec);
         }
-        tmpStr += QLatin1String("<td>") +
-                  str +
-                  QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    tmpStr += displayViewFormatDescription(todo);
-    // TODO: print comments?
-
-    int reminderCount = todo->alarms().count();
-    if (reminderCount > 0 && todo->hasEnabledAlarms()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18np("Reminder:", "Reminders:", reminderCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + \
                reminderStringList(todo).join(QStringLiteral("<br>")) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    tmpStr += displayViewFormatAttendees(calendar, todo);
-
-    int categoryCount = todo->categories().count();
-    if (categoryCount > 0) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18np("Category:", "Categories:", categoryCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + displayViewFormatCategories(todo) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    if (todo->priority() > 0) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Priority:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>");
-        tmpStr += QString::number(todo->priority());
-        tmpStr += QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
+        incidence[QStringLiteral("dueDate")] = dueDt.dateTime();
     }
 
-    tmpStr += QLatin1String("<tr>");
-    if (todo->isCompleted()) {
-        tmpStr += QLatin1String("<td><b>") + i18nc("Completed: date", "Completed:") \
                + QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>");
-        tmpStr += Stringify::todoCompletedDateTime(todo);
-    } else {
-        tmpStr += QLatin1String("<td><b>") + i18n("Percent Done:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>");
-        tmpStr += i18n("%1%", todo->percentComplete());
+    incidence[QStringLiteral("duration")] = durationString(todo);
+    incidence[QStringLiteral("isException")] = todo->hasRecurrenceId();
+    if (todo->recurs()) {
+        incidence[QStringLiteral("recurrence")] = recurrenceString(todo);
     }
-    tmpStr += QLatin1String("</td>");
-    tmpStr += QLatin1String("</tr>");
 
-    int attachmentCount = todo->attachments().count();
-    if (attachmentCount > 0) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18np("Attachment:", "Attachments:", attachmentCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + displayViewFormatAttachments(todo) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-    tmpStr += QLatin1String("</table>");
+    incidence[QStringLiteral("description")] = displayViewFormatDescription(todo);
 
-    tmpStr += QLatin1String("<p><em>") + displayViewFormatCreationDate(todo, spec) + \
QLatin1String("</em>"); +    // TODO: print comments?
 
-    return tmpStr;
+    incidence[QStringLiteral("reminders")] = reminderStringList(todo);
+
+    incidence[QStringLiteral("organizer")] = displayViewFormatOrganizer(todo);
+    const bool showStatus = incOrganizerOwnsCalendar(calendar, todo);
+    incidence[QStringLiteral("chair")] = displayViewFormatAttendeeRoleList(todo, \
Attendee::Chair, showStatus); +    incidence[QStringLiteral("requiredParticipants")] \
= displayViewFormatAttendeeRoleList(todo, Attendee::ReqParticipant, showStatus); +    \
incidence[QStringLiteral("optionalParticipants")] = \
displayViewFormatAttendeeRoleList(todo, Attendee::OptParticipant, showStatus); +    \
incidence[QStringLiteral("observers")] = displayViewFormatAttendeeRoleList(todo, \
Attendee::NonParticipant, showStatus); +
+    incidence[QStringLiteral("categories")] = todo->categories();
+    incidence[QStringLiteral("priority")] = todo->priority();
+     if (todo->isCompleted()) {
+        incidence[QStringLiteral("completedDate")] = todo->completed().dateTime();
+     } else {
+        incidence[QStringLiteral("percent")] = todo->percentComplete();
+     }
+    incidence[QStringLiteral("attachments")] = displayViewFormatAttachments(todo);
+    incidence[QStringLiteral("creationDate")] = \
todo->created().toTimeSpec(spec).dateTime(); +
+    return GrantleeTemplateManager::instance()->render(QStringLiteral("todo.html"), \
incidence);  }
 
 static QString displayViewFormatJournal(const Calendar::Ptr &calendar, const QString \
&sourceName, @@ -933,44 +694,14 @@ static QString displayViewFormatJournal(const \
Calendar::Ptr &calendar, const QSt  return QString();
     }
 
-    QString tmpStr = displayViewFormatHeader(journal);
+    QVariantHash incidence = incidenceTemplateHeader(journal);
+    incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, \
journal) : sourceName; +    incidence[QStringLiteral("date")] = \
journal->dtStart().toTimeSpec(spec).dateTime(); +    \
incidence[QStringLiteral("description")] = displayViewFormatDescription(journal); +   \
incidence[QStringLiteral("categories")] = journal->categories(); +    \
incidence[QStringLiteral("creationDate")] = \
journal->created().toTimeSpec(spec).dateTime();  
-    tmpStr += QLatin1String("<table>");
-    tmpStr += QLatin1String("<col width=\"25%\"/>");
-    tmpStr += QLatin1String("<col width=\"75%\"/>");
-
-    const QString calStr = calendar ? resourceString(calendar, journal) : \
                sourceName;
-    if (!calStr.isEmpty()) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") + i18n("Calendar:") + \
                QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + calStr + QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    tmpStr += QLatin1String("<tr>");
-    tmpStr += QLatin1String("<td><b>") + i18n("Date:") + QLatin1String("</b></td>");
-    tmpStr += QLatin1String("<td>") +
-              dateToString(journal->dtStart(), false, spec) +
-              QLatin1String("</td>");
-    tmpStr += QLatin1String("</tr>");
-
-    tmpStr += displayViewFormatDescription(journal);
-
-    int categoryCount = journal->categories().count();
-    if (categoryCount > 0) {
-        tmpStr += QLatin1String("<tr>");
-        tmpStr += QLatin1String("<td><b>") +
-                  i18np("Category:", "Categories:", categoryCount) +
-                  QLatin1String("</b></td>");
-        tmpStr += QLatin1String("<td>") + displayViewFormatCategories(journal) + \
                QLatin1String("</td>");
-        tmpStr += QLatin1String("</tr>");
-    }
-
-    tmpStr += QLatin1String("</table>");
-
-    tmpStr += QLatin1String("<p><em>") + displayViewFormatCreationDate(journal, \
                spec) + QLatin1String("</em>");
-
-    return tmpStr;
+    return GrantleeTemplateManager::instance()->render(QStringLiteral("journal.html"), \
incidence);  }
 
 static QString displayViewFormatFreeBusy(const Calendar::Ptr &calendar, const \
QString &sourceName, @@ -982,23 +713,17 @@ static QString \
displayViewFormatFreeBusy(const Calendar::Ptr &calendar, const QS  return QString();
     }
 
-    QString tmpStr(
-        htmlAddTag(
-            QStringLiteral("h2"), i18n("Free/Busy information for %1", \
                fb->organizer()->fullName())));
-
-    tmpStr += htmlAddTag(QStringLiteral("h4"),
-                         i18n("Busy times in date range %1 - %2:",
-                              dateToString(fb->dtStart(), true, spec),
-                              dateToString(fb->dtEnd(), true, spec)));
-
-    QString text =
-        htmlAddTag(QStringLiteral("em"),
-                   htmlAddTag(QStringLiteral("b"), i18nc("tag for busy periods \
list", "Busy:"))); +    QVariantHash fbData;
+    fbData[QStringLiteral("organizer")] = fb->organizer()->fullName();
+    fbData[QStringLiteral("start")] = fb->dtStart().toTimeSpec(spec).date();
+    fbData[QStringLiteral("end")] = fb->dtEnd().toTimeSpec(spec).date();
 
     Period::List periods = fb->busyPeriods();
-    Period::List::iterator it;
-    for (it = periods.begin(); it != periods.end(); ++it) {
-        Period per = *it;
+    QVariantList periodsData;
+    periodsData.reserve(periods.size());
+    for ( auto it = periods.cbegin(), end = periods.cend(); it != end; ++it) {
+        const Period per = *it;
+        QVariantHash periodData;
         if (per.hasDuration()) {
             int dur = per.duration().asSeconds();
             QString cont;
@@ -1013,26 +738,27 @@ static QString displayViewFormatFreeBusy(const Calendar::Ptr \
&calendar, const QS  if (dur > 0) {
                 cont += i18ncp("seconds part of duration", "1 second", "%1 seconds", \
dur);  }
-            text += i18nc("startDate for duration", "%1 for %2",
-                          dateTimeToString(per.start(), false, true, spec),
-                          cont);
-            text += QLatin1String("<br>");
+            periodData[QStringLiteral("dtStart")] = \
per.start().toTimeSpec(spec).dateTime(); +            \
periodData[QStringLiteral("duration")] = cont;  } else {
+            const KDateTime pStart = per.start().toTimeSpec(spec);
+            const KDateTime pEnd = per.end().toTimeSpec(spec);
             if (per.start().date() == per.end().date()) {
-                text += i18nc("date, fromTime - toTime ", "%1, %2 - %3",
-                              dateToString(per.start(), true, spec),
-                              timeToString(per.start(), true, spec),
-                              timeToString(per.end(), true, spec));
+                periodData[QStringLiteral("date")] =  pStart.date();
+                periodData[QStringLiteral("start")] = pStart.time();
+                periodData[QStringLiteral("end")] = pEnd.time();
             } else {
-                text += i18nc("fromDateTime - toDateTime", "%1 - %2",
-                              dateTimeToString(per.start(), false, true, spec),
-                              dateTimeToString(per.end(), false, true, spec));
+                periodData[QStringLiteral("start")] = pStart.dateTime();
+                periodData[QStringLiteral("end")] = pEnd.dateTime();
             }
-            text += QLatin1String("<br>");
         }
+
+        periodsData << periodData;
     }
-    tmpStr += htmlAddTag(QStringLiteral("p"), text);
-    return tmpStr;
+
+    fbData[QStringLiteral("periods")] = periodsData;
+
+    return GrantleeTemplateManager::instance()->render(QStringLiteral("freebusy.html"), \
fbData);  }
 //@endcond
 
@@ -3579,7 +3305,7 @@ static QString tooltipPerson(const QString &email, const \
QString &name, Attendee  const QString printName = searchName(email, name);
 
     // Get the icon corresponding to the attendee participation status.
-    const QString iconPath = rsvpStatusIconPath(status);
+    const QString iconPath = \
KIconLoader::global()->iconPath(rsvpStatusIconName(status), KIconLoader::Small);  
     // Make the return string.
     QString personString;
diff --git a/templates/CMakeLists.txt b/templates/CMakeLists.txt
new file mode 100644
index 0000000..02bff77
--- /dev/null
+++ b/templates/CMakeLists.txt
@@ -0,0 +1,10 @@
+
+install(FILES event.html
+              freebusy.html
+              incidence_header.html
+              journal.html
+              template_base.html
+              todo.html
+              attendee_row.html
+        DESTINATION ${KDE_INSTALL_DATADIR}/kcalendar/templates/default
+)
diff --git a/templates/attendee_row.html b/templates/attendee_row.html
new file mode 100644
index 0000000..95224a0
--- /dev/null
+++ b/templates/attendee_row.html
@@ -0,0 +1,28 @@
+{% if attendee.icon %}
+    {% icon attendee.icon small attendee.status %}
+{% endif %}
+
+{% if attendee.uid %}
+    <a href="uid:{{ attendee.uid }}">
+    {% if attendee.name %}
+        {{ attendee.name }}
+    {% else %}
+        {{ attendee.email }}
+    {% endif %}
+    </a>
+{% else %}
+    {% if attendee.name %}
+        {{ attendee.name }}
+    {% else %}
+        {{ attendee.email }}
+    {% endif %}
+{% endif %}
+{% if attendee.delegator %}
+    {% i18n "(delegated by %1)" attendee.delegator %}
+{% endif %}
+{% if attendee.delegate %}
+    {% i18n "(delegated to %1)" attendee.delegate %}\
+{% endif %}
+{% if attendee.mailto %}
+    <a href="{{ attendee.mailto }}">{% icon "mail-message-new" small _("Send email") \
%}</a> +{% endif %}
diff --git a/templates/event.html b/templates/event.html
new file mode 100644
index 0000000..9c9681d
--- /dev/null
+++ b/templates/event.html
@@ -0,0 +1,198 @@
+{% extends "template_base.html" %}
+
+
+{% block body %}
+{% with _("Event") as type %}
+{% include "incidence_header.html" %}
+{% endwith %}
+
+<table>
+    <!-- Calendar name -->
+    {% if incidence.calendar %}
+    <tr>
+        <th>{% i18n "Calendar:" %}</th>
+        <td>{{ incidence.calendar }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Location -->
+    {% if incidence.location %}
+    <tr>
+        <th>{% i18n "Location:" %}</th>
+        <td>{{ incidence.location|safe }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Start/end -->
+    {% if incidence.isAllDay %}
+    <tr>
+        {% if incidence.isMultiDay %}
+        <th>{% i18n "Date:" %}</th>
+        <td>{% i18nc "<beginDate> - <endDate>" "%1 - %2" incidence.startDate|kdate \
incidence.endDate|kdate %}</td> +        {% else %}
+        <th>{% i18n "Date:" %}</th>
+        <td>{% i18nc "Date as string" "%1" incidence.startDate|kdate %}</td>
+        {% endif %}
+    </tr>
+    {% else %}
+    <tr>
+        {% if incidence.isMultiDay %}
+        <th>{% i18n "Date:" %}</th>
+        <td>{% i18nc "<beginDate> - <endDate>" "%1 - %2" incidence.startDate|kdate \
incidence.endDate|kdate %}</td> +        {% else %}
+        <th>{% i18n "Date:" %}</th>
+        <td>{% i18nc "Date as string" "%1" incidence.startDate|kdate %}</td>
+        </tr>
+        <tr>
+            <th>{% i18n "Time:" %}</th>
+            {% if incidence.hasEnd and incidence.overnight %}
+            <td>{% i18nc "<beginTime> - <endTime>" "%1 - %2" \
incidence.startTime|ktime:"short" incidence.endTime|ktime:"short" %}</td> +           \
{% else %} +            <td>{% i18nc "Time as string" "%1" \
incidence.startTime|ktime:"short" %}</td> +            {% endif %}
+        {% endif %}
+    </tr>
+    {% endif %}
+
+    <!-- Duration -->
+    {% if incidence.duration %}
+    <tr>
+        <th>{% i18n "Duration:" %}</th>
+        <td>{{ incidence.duration }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Recurrence -->
+    {% if incidence.recurs or incidence.isException %}
+    <tr>
+        <th>{% i18n "Recurrence:" %}</th>
+        {% if incidence.isException %}
+        <td>{% i18nc "Exception in event recurrence" "Exception" %}</td>
+        {% else %}
+        <td>{{ incidence.recurrence }}</td>
+        {% endif %}
+    </tr>
+    {% endif %}
+
+    <!-- Birthday -->
+    {% if incidence.birthday %}
+    <tr>
+        <th>{% i18n "Birthday:" %}</th>
+        <td>{{ incidence.birthday }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Anniversary -->
+    {% if incidence.anniversary %}
+    <tr>
+        <th>{% i18n "Anniversary:" %}</th>
+        <td>{{ incidence.anniversary }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Description -->
+    {% if incidence.description %}
+    <tr>
+        <th>{% i18n "Description:" %}</th>
+        <td>{{ incidence.description|safe }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Alarms -->
+    {% if incidence.reminders %}
+    <tr>
+        <th>{% i18np "Reminder:" "Reminders:" incidence.reminders|length %}</th>
+        <td>{{ incidence.reminders|join:"<br/>" }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Organizer -->
+    {% if incidence.organizer %}
+    <tr>
+        <th>{% i18n "Organizer:" %}</th>
+        <td>
+        {% with incidence.organizer as attendee %}
+        {% include "attendee_row.html" %}
+        {% endwith %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Chair -->
+    {% if incidence.chairs %}
+    <tr>
+        <th>{% i18n "Chair:" %}</th>
+        <td>
+        {% for attendee in incidence.chair %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Required Participants -->
+    {% if incidence.requiredParticipants %}
+    <tr>
+        <th>{% i18n "Required Participants:" %}</th>
+        <td>
+        {% for attendee in incidence.requiredParticipants %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Optional Participants -->
+    {% if incidence.optionalParticipants %}
+    <tr>
+        <th>{% i18n "Optional participants:" %}</th>
+        <td>
+        {% for attendee in incidence.optionalParticipants %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Observers -->
+    {% if incidence.observers %}
+    <tr>
+        <th>{% i18n "Observers:" %}</th>
+        <td>
+        {% for attendee in incidence.chair %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Categories -->
+    {% if incidence.categories %}
+    <tr>
+        <th>{% i18np "Category:" "Categories:" incidence.categories|length %}</th>
+        <td>{{ incidence.categories|join:", " }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Attachments -->
+    {% if incidence.attachments %}
+    <tr>
+        <th>{% i18np "Attachment:" "Attachments:" incidence.attachments|length \
%}</th> +        <td>{% for attachment in incidence.attachments %}
+            <a href="{{ attachment.uri }}">{{ attachment.label }}</a>
+            {% if not forloop.last %}<br/>{% endif %}
+            {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+</table>
+
+<!-- Creation -->
+<p><em>{% i18n "Creation date: %1" incidence.creationDate|kdatetime %}</em></p>
+
+{% endblock body %}
diff --git a/templates/freebusy.html b/templates/freebusy.html
new file mode 100644
index 0000000..a4ea0d6
--- /dev/null
+++ b/templates/freebusy.html
@@ -0,0 +1,28 @@
+{% extends "template_base.html" %}
+
+{% block body %}
+
+<h2>{% i18n "Free/Busy information for %1" incidence.organizer %}</h2>
+<h4>{% i18n "Busy times in date range %1 - %2:" incidence.start|kdate:"short" \
incidence.end|kdate:"short" %}</h4> +
+<p>
+<em><b>{% i18nc "tag for busy period list" "Busy:" %}</b></em>
+
+{% for period in incidence.periods %}
+    {% if period.duration %}
+        {% i18nc "startDate for duration" "%1 for %2" period.dtStart|kdate:"short" \
period.duration %} +    {% else %}
+        {% if period.date %}
+            {% i18nc "date, fromTime - toTime" "%1, %2 - %3" \
period.date|kdate:"short" period.start|ktime:"short" period.end|ktime:"short" %} +    \
{% else %} +            {% i18nc "fromDateTime - endDateTime" "%1 - %2" \
period.start|kdatetime:"short" period.end|kdatetime:"short" %} +        {% endif %}
+    {% endif %}
+    {% if not forloop.last %}
+        <br/>
+    {% endif %}
+{% endfor %}
+</p>
+
+{% endblock body %}
+
diff --git a/templates/incidence_header.html b/templates/incidence_header.html
new file mode 100644
index 0000000..5f5b60e
--- /dev/null
+++ b/templates/incidence_header.html
@@ -0,0 +1,24 @@
+<table class="header">
+    <tr>
+        <td>
+            {% if incidence.icon %}
+            {% icon incidence.icon small type %}
+            {% endif %}
+
+            {% if incidence.hasEnabledAlarms %}
+            {% icon "preferences-desktop-notification-bell" small _("Incidence with \
a reminder") %} +            {% endif %}
+
+            {% if incidence.recurs %}
+            {% icon "edit-redo" small _("Recurring incidence") %}
+            {% endif %}
+
+            {% if incidence.isReadOnly %}
+            {% icon "object-locked" small _("Incidence is read only") %}
+            {% endif %}
+        </td>
+        <td>
+            <b><u>{{ incidence.summary }}</u></b>
+        </td>
+    </tr>
+</table>
diff --git a/templates/journal.html b/templates/journal.html
new file mode 100644
index 0000000..b8f20a4
--- /dev/null
+++ b/templates/journal.html
@@ -0,0 +1,48 @@
+{% extends "template_base.html" %}
+
+
+{% block body %}
+{% with _("Journal") as type %}
+{% include "incidence_header.html" %}
+{% endwith %}
+
+<table>
+
+    <!-- Calendar -->
+    {% if incidence.calendar %}
+    <tr>
+        <th>{% i18n "Calendar:" %}</th>
+        <td>{{ incidence.calendar }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Date -->
+    {% if incidence.date %}
+    <tr>
+        <th>{% i18n "Date:" %}</th>
+        <td>{{ incidence.date|kdate }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Description -->
+    {% if incidence.description %}
+    <tr>
+        <th>{% i18n "Description:" %}</th>
+        <td>{{ incidence.description|safe }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Categories -->
+    {% if incidence.categories %}
+    <tr>
+        <th>{% i18np "Category:" "Categories:" incidence.categories|length %}</th>
+        <td>{{ incidence.categories|join:", " }}</td>
+    </tr>
+    {% endif %}
+
+</table>
+
+<!-- Creation date -->
+<p><em>{% i18n "Creation date: %1" incidence.creationDate|kdatetime %}</em></p>
+
+{% endblock body %}
diff --git a/templates/template_base.html b/templates/template_base.html
new file mode 100644
index 0000000..07871df
--- /dev/null
+++ b/templates/template_base.html
@@ -0,0 +1,22 @@
+<style type="text/css">
+{# The Qt CSS parser is rather dumb and does not handle more complex #}
+{# selectors like "table.main th" (despite what documentation says) #}
+th {
+    text-align: left;
+    width: 25%;
+    white-space: nowrap;
+    font-weight: bold;
+}
+td {
+    padding-left: 5px;
+}
+
+{% block style %}
+{# Custom style extension provided by subtemplates #}
+{% endblock %}
+</style>
+
+
+{% block body %}
+{# Actual content of the subtemplate #}
+{% endblock %}
diff --git a/templates/todo.html b/templates/todo.html
new file mode 100644
index 0000000..478b7eb
--- /dev/null
+++ b/templates/todo.html
@@ -0,0 +1,203 @@
+{% extends "template_base.html" %}
+
+
+{% block body %}
+{% with _("Todo") as type %}
+{% include "incidence_header.html" %}
+{% endwith %}
+
+<table class="main">
+
+    <!-- Calendar -->
+    {% if incidence.calendar %}
+    <tr>
+        <th>{% i18n "Calendar:" %}</th>
+        <td>{{ incidence.calendar }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Location -->
+    {% if incidence.location %}
+    <tr>
+        <th>{% i18n "Location:" %}</th>
+        <td>{{ incidence.location|safe }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Start date -->
+    {% if incidence.startDate %}
+    <tr>
+        <th>{% i18nc "to-do start date/time" "Start:" %}</th>
+        <td>{{ incidence.startDate|kdatetime }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Due date -->
+    {% if incidence.dueDate %}
+    <tr>
+        <th>{% i18nc "to-do due date/time" "Due:" %}</th>
+        {% if incidence.allDay %}
+        <td>{{ incidence.dueDate|kdatetime:"dateonly" }}</td>
+        {% else %}
+        <td>{{ incidence.dueDate|kdatetime }}</td>
+        {% endif %}
+    </tr>
+    {% endif %}
+
+    <!-- Duration -->
+    {% if incidence.duration %}
+    <tr>
+        <th>{% i18n "Duration:" %}</th>
+        <td>{{ incidence.duration }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Recurrence -->
+    {% if incidence.recurs or incidence.isException %}
+    <tr>
+        <th>{% i18n "Recurrence:" %}</th>
+        {% if incidence.isException %}
+        <td>{% i18n "Exception" %}</td>
+        {% else %}
+        <td>{{ incidence.recurrence }}</td>
+        {% endif %}
+    </tr>
+    {% endif %}
+
+    <!-- Organizer -->
+    {# TODO #}
+
+    <!-- Attendee -->
+    {# TODO #}
+
+    <!-- Description -->
+    {% if incidence.description %}
+    <tr>
+        <th>{% i18n "Description:" %}</th>
+        <td>{{ incidence.description|safe }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Comments -->
+    {# TODO #}
+
+    <!-- Reminders -->
+    {% if incidence.reminders %}
+    <tr>
+        <th>{% i18np "Reminder:" "Reminders:" incidence.reminders|length %}</th>
+        <td>{{ incidence.reminders|join:"<br/>" }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Organizer -->
+    {% if incidence.organizer %}
+    <tr>
+        <th>{% i18n "Organizer:" %}</th>
+        <td>
+        {% with incidence.organizer as attendee %}
+        {% include "attendee_row.html" %}
+        {% endwith %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Chair -->
+    {% if incidence.chairs %}
+    <tr>
+        <th>{% i18n "Chair:" %}</th>
+        <td>
+        {% for attendee in incidence.chair %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Required Participants -->
+    {% if incidence.requiredParticipants %}
+    <tr>
+        <th>{% i18n "Required Participants:" %}</th>
+        <td>
+        {% for attendee in incidence.requiredParticipants %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Optional Participants -->
+    {% if incidence.optionalParticipants %}
+    <tr>
+        <th>{% i18n "Optional participants:" %}</th>
+        <td>
+        {% for attendee in incidence.optionalParticipants %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Attendees - Observers -->
+    {% if incidence.observers %}
+    <tr>
+        <th>{% i18n "Observers:" %}</th>
+        <td>
+        {% for attendee in incidence.chair %}
+            {% include "attendee_row.html" %}
+            {% if not forloop.last %}<br/>{% endif %}
+        {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+
+    <!-- Categories -->
+    {% if incidence.categories %}
+    <tr>
+        <th>{% i18np "Category:" "Categories:" incidence.categories|length %}</th>
+        <td>{{ incidence.categories|join:", " }}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Priority -->
+    {% ifnotequal incidence.priority 0 %}
+    <tr>
+        <th>{% i18n "Priority:" %}</th>
+        <td>{{ incidence.priority }}</td>
+    </tr>
+    {% endifnotequal %}
+
+    <!-- Completed -->
+    {% if incidence.completedDate %}
+    <tr>
+        <th>{% i18nc "Completed: date" "Completed:" %}</th>
+        <td>{{ incidence.completedDate|kdate }}</td>
+    </tr>
+    {% else %}
+    <tr>
+        <th>{% i18n "Percent done:" %}</th>
+        <td>{% i18n "%1%" incidence.percent %}</td>
+    </tr>
+    {% endif %}
+
+    <!-- Attachments -->
+    {% if incidence.attachments %}
+    <tr>
+        <th>{% i18np "Attachment:" "Attachments:" incidence.attachments|length \
%}</th> +        <td>{% for attachment in incidence.attachments %}
+            <a href="{{ attachment.uri }}">{{ attachment.label }}</a>
+            {% if not forloop.last %}
+            <br/>
+            {% endif %}
+            {% endfor %}
+        </td>
+    </tr>
+    {% endif %}
+</table>
+
+<!-- Creation date -->
+<p><em>{% i18n "Creation date: %1" incidence.creationDate|kdatetime %}</em></p>
+
+{% endblock body %}


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic