2 commits - plugins/calendar plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Wed Mar 25 11:59:28 CET 2015


 plugins/calendar/calendar.php                            |    8 
 plugins/calendar/calendar_ui.js                          |    5 
 plugins/calendar/lib/calendar_ui.php                     |   18 
 plugins/calendar/localization/bg_BG.inc                  |    4 
 plugins/calendar/localization/ca_ES.inc                  |    6 
 plugins/calendar/localization/cs_CZ.inc                  |    6 
 plugins/calendar/localization/da_DK.inc                  |    6 
 plugins/calendar/localization/de_CH.inc                  |    2 
 plugins/calendar/localization/de_DE.inc                  |   12 
 plugins/calendar/localization/en_US.inc                  |   12 
 plugins/calendar/localization/es_AR.inc                  |    6 
 plugins/calendar/localization/es_ES.inc                  |    2 
 plugins/calendar/localization/et_EE.inc                  |   14 
 plugins/calendar/localization/fi_FI.inc                  |   13 
 plugins/calendar/localization/fr_FR.inc                  |    6 
 plugins/calendar/localization/it_IT.inc                  |    6 
 plugins/calendar/localization/pl_PL.inc                  |    6 
 plugins/calendar/localization/pt_PT.inc                  |  274 ++++++++++++++
 plugins/calendar/localization/ru_RU.inc                  |    9 
 plugins/calendar/localization/sl_SI.inc                  |    6 
 plugins/calendar/localization/sv_SE.inc                  |    6 
 plugins/calendar/localization/th_TH.inc                  |    6 
 plugins/calendar/localization/uk_UA.inc                  |    2 
 plugins/calendar/skins/larry/calendar.css                |    4 
 plugins/calendar/skins/larry/templates/calendar.html     |    2 
 plugins/libkolab/js/audittrail.js                        |    4 
 plugins/libkolab/libkolab.php                            |   18 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |  286 ++++++++++++++-
 plugins/tasklist/drivers/tasklist_driver.php             |   76 +++
 plugins/tasklist/localization/en_US.inc                  |   22 +
 plugins/tasklist/skins/larry/tasklist.css                |   59 ++-
 plugins/tasklist/skins/larry/templates/mainview.html     |   79 ++++
 plugins/tasklist/tasklist.js                             |  219 +++++++++++
 plugins/tasklist/tasklist.php                            |  111 +++++
 plugins/tasklist/tasklist_ui.php                         |    2 
 35 files changed, 1189 insertions(+), 128 deletions(-)

New commits:
commit 548d1d93b70ceb213d2693ab4520e861f9f5b0d9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 25 11:59:10 2015 +0100

    Display object history for tasks (#3271)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 4fccf7e..4a192c6 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -38,6 +38,7 @@ class tasklist_kolab_driver extends tasklist_driver
     private $folders = array();
     private $tasks   = array();
     private $tags    = array();
+    private $bonnie_api = false;
 
 
     /**
@@ -55,6 +56,11 @@ class tasklist_kolab_driver extends tasklist_driver
         // tasklist use fully encoded identifiers
         kolab_storage::$encode_ids = true;
 
+        // get configuration for the Bonnie API
+        if ($bonnie_config = $this->rc->config->get('kolab_bonnie_api', false)) {
+            $this->bonnie_api = new kolab_bonnie_api($bonnie_config);
+        }
+
         $this->_read_lists();
 
         $this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
@@ -152,6 +158,7 @@ class tasklist_kolab_driver extends tasklist_driver
             'group' => $folder->default ? 'default' : $folder->get_namespace(),
             'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
             'caldavuid' => $folder->get_uid(),
+            'history' => !empty($this->bonnie_api),
         );
     }
 
@@ -659,6 +666,250 @@ class tasklist_kolab_driver extends tasklist_driver
     }
 
     /**
+     * Provide a list of revisions for the given task
+     *
+     * @param array  $task Hash array with task properties
+     * @return array List of changes, each as a hash array
+     * @see tasklist_driver::get_task_changelog()
+     */
+    public function get_task_changelog($prop)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+        $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null;
+        if (is_array($result) && $result['uid'] == $uid) {
+            return $result['changes'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Return full data of a specific revision of an event
+     *
+     * @param mixed  $task UID string or hash array with task properties
+     * @param mixed  $rev Revision number
+     *
+     * @return array Task object as hash array
+     * @see tasklist_driver::get_task_revision()
+     */
+    public function get_task_revison($prop, $rev)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        $this->_parse_id($prop);
+        $uid     = $prop['uid'];
+        $list_id = $prop['list'];
+        list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+        // call Bonnie API
+        $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid);
+        if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+            $format = kolab_format::factory('task');
+            $format->load($result['xml']);
+            $rec = $format->to_array();
+            $format->get_attachments($rec, true);
+
+            if ($format->is_valid()) {
+                $rec['rev'] = $result['rev'];
+                return self::_to_rcube_task($rec, $list_id, false);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Command the backend to restore a certain revision of a task.
+     * This shall replace the current object with an older version.
+     *
+     * @param mixed  $task UID string or hash array with task properties
+     * @param mixed  $rev Revision number
+     *
+     * @return boolean True on success, False on failure
+     * @see tasklist_driver::restore_task_revision()
+     */
+    public function restore_task_revision($prop, $rev)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        $this->_parse_id($prop);
+        $uid     = $prop['uid'];
+        $list_id = $prop['list'];
+        list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+        $folder = $this->get_folder($list_id);
+        $success = false;
+
+        if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) {
+            $imap = $this->rc->get_storage();
+
+            // insert $raw_msg as new message
+            if ($imap->save_message($folder->name, $raw_msg, null, false)) {
+                $success = true;
+
+                // delete old revision from imap and cache
+                $imap->delete_message($msguid, $folder->name);
+                $folder->cache->set($msguid, false);
+            }
+        }
+
+        return $success;
+    }
+
+    /**
+     * Get a list of property changes beteen two revisions of a task object
+     *
+     * @param array  $task Hash array with task properties
+     * @param mixed  $rev   Revisions: "from:to"
+     *
+     * @return array List of property changes, each as a hash array
+     * @see tasklist_driver::get_task_diff()
+     */
+    public function get_task_diff($prop, $rev1, $rev2)
+    {
+        $this->_parse_id($prop);
+        $uid     = $prop['uid'];
+        $list_id = $prop['list'];
+        list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+        // call Bonnie API
+        $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
+        if (is_array($result) && $result['uid'] == $uid) {
+            $result['rev1'] = $rev1;
+            $result['rev2'] = $rev2;
+
+            $keymap = array(
+                'start'    => 'start',
+                'due'      => 'date',
+                'dstamp'   => 'changed',
+                'summary'  => 'title',
+                'alarm'    => 'alarms',
+                'attendee' => 'attendees',
+                'attach'   => 'attachments',
+                'rrule'    => 'recurrence',
+                'percent-complete' => 'complete',
+                'lastmodified-date' => 'changed',
+            );
+            $prop_keymaps = array(
+                'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
+                'attendees'   => array('partstat' => 'status'),
+            );
+            $special_changes = array();
+
+            // map kolab event properties to keys the client expects
+            array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
+                if (array_key_exists($change['property'], $keymap)) {
+                    $change['property'] = $keymap[$change['property']];
+                }
+                if ($change['property'] == 'priority') {
+                    $change['property'] = 'flagged';
+                    $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null;
+                    $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null;
+                }
+                // map alarms trigger value
+                if ($change['property'] == 'alarms') {
+                    if (is_array($change['old']) && is_array($change['old']['trigger']))
+                        $change['old']['trigger'] = $change['old']['trigger']['value'];
+                    if (is_array($change['new']) && is_array($change['new']['trigger']))
+                        $change['new']['trigger'] = $change['new']['trigger']['value'];
+                }
+                // make all property keys uppercase
+                if ($change['property'] == 'recurrence') {
+                    $special_changes['recurrence'] = $i;
+                    foreach (array('old','new') as $m) {
+                        if (is_array($change[$m])) {
+                            $props = array();
+                            foreach ($change[$m] as $k => $v) {
+                                $props[strtoupper($k)] = $v;
+                            }
+                            $change[$m] = $props;
+                        }
+                    }
+                }
+                // map property keys names
+                if (is_array($prop_keymaps[$change['property']])) {
+                  foreach ($prop_keymaps[$change['property']] as $k => $dest) {
+                    if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
+                        $change['old'][$dest] = $change['old'][$k];
+                        unset($change['old'][$k]);
+                    }
+                    if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
+                        $change['new'][$dest] = $change['new'][$k];
+                        unset($change['new'][$k]);
+                    }
+                  }
+                }
+
+                if ($change['property'] == 'exdate') {
+                    $special_changes['exdate'] = $i;
+                }
+                else if ($change['property'] == 'rdate') {
+                    $special_changes['rdate'] = $i;
+                }
+            });
+
+            // merge some recurrence changes
+            foreach (array('exdate','rdate') as $prop) {
+                if (array_key_exists($prop, $special_changes)) {
+                    $exdate = $result['changes'][$special_changes[$prop]];
+                    if (array_key_exists('recurrence', $special_changes)) {
+                        $recurrence = &$result['changes'][$special_changes['recurrence']];
+                    }
+                    else {
+                        $i = count($result['changes']);
+                        $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
+                        $recurrence = &$result['changes'][$i]['recurrence'];
+                    }
+                    $key = strtoupper($prop);
+                    $recurrence['old'][$key] = $exdate['old'];
+                    $recurrence['new'][$key] = $exdate['new'];
+                    unset($result['changes'][$special_changes[$prop]]);
+                }
+            }
+
+            return $result;
+        }
+
+        return false;
+    }
+
+    /**
+     * Helper method to resolved the given task identifier into uid and folder
+     *
+     * @return array (uid,folder,msguid) tuple
+     */
+    private function _resolve_task_identity($prop)
+    {
+      $mailbox = $msguid = null;
+
+      $this->_parse_id($prop);
+      $uid     = $prop['uid'];
+      $list_id = $prop['list'];
+
+      if ($folder = $this->get_folder($list_id)) {
+          $mailbox = $folder->get_mailbox_id();
+
+          // get task object from storage in order to get the real object uid an msguid
+          if ($rec = $folder->get_object($uid)) {
+              $msguid = $rec['_msguid'];
+              $uid = $rec['uid'];
+          }
+      }
+
+      return array($uid, $mailbox, $msguid);
+    }
+
+
+    /**
      * Get a list of pending alarms to be displayed to the user
      *
      * @param  integer Current time (unix timestamp)
@@ -1232,6 +1483,7 @@ class tasklist_kolab_driver extends tasklist_driver
      * @param array  $task  Hash array with event properties:
      *         id: Task identifier
      *       list: List identifier
+     *        rev: Revision (optional)
      *
      * @return array Hash array with attachment properties:
      *         id: Attachment identifier
@@ -1241,7 +1493,13 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function get_attachment($id, $task)
     {
-        $task = $this->get_task($task);
+        // get old revision of the object
+        if ($task['rev']) {
+            $task = $this->get_task_revison($task, $task['rev']);
+        }
+        else {
+            $task = $this->get_task($task);
+        }
 
         if ($task && !empty($task['attachments'])) {
             foreach ($task['attachments'] as $att) {
@@ -1260,12 +1518,38 @@ class tasklist_kolab_driver extends tasklist_driver
      * @param array  $task  Hash array with event properties:
      *         id: Task identifier
      *       list: List identifier
+     *        rev: Revision (optional)
      *
      * @return string Attachment body
      */
     public function get_attachment_body($id, $task)
     {
         $this->_parse_id($task);
+
+        // get old revision of event
+        if ($task['rev']) {
+            if (empty($this->bonnie_api)) {
+                return false;
+            }
+
+            $cid = substr($id, 4);
+
+            // call Bonnie API and get the raw mime message
+            list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task);
+            if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) {
+                // parse the message and find the part with the matching content-id
+                $message = rcube_mime::parse_message($msg_raw);
+                foreach ((array)$message->parts as $part) {
+                    if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
+                        return $part->body;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+
         if ($storage = $this->get_folder($task['list'])) {
             return $storage->get_attachment($task['uid'], $id);
         }
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 3362e7f..be82344 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -242,6 +242,7 @@ abstract class tasklist_driver
      *
      * @param array   Hash array with task properties:
      *      id: Task identifier
+     *    list: Tasklist identifer
      * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
      * @return boolean True on success, False on error
      */
@@ -266,6 +267,7 @@ abstract class tasklist_driver
      * @param array  $task  Hash array with event properties:
      *         id: Task identifier
      *       list: List identifier
+     *        rev: Revision (optional)
      *
      * @return array Hash array with attachment properties:
      *         id: Attachment identifier
@@ -282,6 +284,7 @@ abstract class tasklist_driver
      * @param array  $task  Hash array with event properties:
      *         id: Task identifier
      *       list: List identifier
+     *        rev: Revision (optional)
      *
      * @return string Attachment body
      */
@@ -319,7 +322,7 @@ abstract class tasklist_driver
     /**
      * Helper method to determine whether the given task is considered "complete"
      *
-     * @param array  $task  Hash array with event properties:
+     * @param array  $task  Hash array with event properties
      * @return boolean True if complete, False otherwiese
      */
     public function is_complete($task)
@@ -328,13 +331,74 @@ abstract class tasklist_driver
     }
 
     /**
-     * List availabale categories
-     * The default implementation reads them from config/user prefs
+     * Provide a list of revisions for the given task
+     *
+     * @param array  $task Hash array with task properties:
+     *         id: Task identifier
+     *       list: List identifier
+     *
+     * @return array List of changes, each as a hash array:
+     *         rev: Revision number
+     *        type: Type of the change (create, update, move, delete)
+     *        date: Change date
+     *        user: The user who executed the change
+     *          ip: Client IP
+     *     mailbox: Destination list for 'move' type
      */
-    public function list_categories()
+    public function get_task_changelog($task)
     {
-        $rcmail = rcube::get_instance();
-        return $rcmail->config->get('tasklist_categories', array());
+        return false;
+    }
+
+    /**
+     * Get a list of property changes beteen two revisions of a task object
+     *
+     * @param array  $task Hash array with task properties:
+     *         id: Task identifier
+     *       list: List identifier
+     * @param mixed  $rev1   Old Revision
+     * @param mixed  $rev2   New Revision
+     *
+     * @return array List of property changes, each as a hash array:
+     *    property: Revision number
+     *         old: Old property value
+     *         new: Updated property value
+     */
+    public function get_task_diff($task, $rev1, $rev2)
+    {
+        return false;
+    }
+
+    /**
+     * Return full data of a specific revision of an event
+     *
+     * @param mixed  $task UID string or hash array with task properties:
+     *         id: Task identifier
+     *       list: List identifier
+     * @param mixed  $rev Revision number
+     *
+     * @return array Task object as hash array
+     * @see self::get_task()
+     */
+    public function get_task_revison($task, $rev)
+    {
+        return false;
+    }
+
+    /**
+     * Command the backend to restore a certain revision of a task.
+     * This shall replace the current object with an older version.
+     *
+     * @param mixed  $task UID string or hash array with task properties:
+     *         id: Task identifier
+     *       list: List identifier
+     * @param mixed  $rev Revision number
+     *
+     * @return boolean True on success, False on failure
+     */
+    public function restore_task_revision($task, $rev)
+    {
+        return false;
     }
 
     /**
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index ee66759..8dc2929 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -50,6 +50,9 @@ $labels['status-cancelled'] = 'Cancelled';
 $labels['assignedto'] = 'Assigned to';
 $labels['created'] = 'Created';
 $labels['changed'] = 'Last Modified';
+$labels['taskoptions'] = 'Options';
+$labels['taskhistory'] = 'History';
+$labels['compare'] = 'Compare';
 
 $labels['all'] = 'All';
 $labels['flagged'] = 'Flagged';
@@ -101,6 +104,7 @@ $labels['on'] = 'on';
 $labels['at'] = 'at';
 $labels['this'] = 'this';
 $labels['next'] = 'next';
+$labels['yes'] = 'yes';
 
 // messages
 $labels['savingdata'] = 'Saving data...';
@@ -150,6 +154,24 @@ $labels['itipcancelsubject'] = '"$title" has been canceled';
 $labels['itipcancelmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
 $labels['saveintasklist'] = 'save in ';
 
+// history dialog
+$labels['objectchangelog'] = 'Change History';
+$labels['objectdiff'] = 'Changes from $rev1 to $rev2';
+
+$labels['actionappend'] = 'Saved';
+$labels['actionmove'] = 'Moved';
+$labels['actiondelete'] = 'Deleted';
+$labels['compare'] = 'Compare';
+$labels['showrevision'] = 'Show this version';
+$labels['restore'] = 'Restore this version';
+
+$labels['objectnotfound'] = 'Failed to load task data';
+$labels['objectchangelognotavailable'] = 'Change history is not available for this task';
+$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['revisionrestoreconfirm'] = 'Do you really want to restore revision $rev of this task? This will replace the current task with the old version.';
+$labels['objectrestoresuccess'] = 'Revision $rev successfully restored';
+$labels['objectrestoreerror'] = 'Failed to restore the old revision';
+
 // invitation handling (overrides labels from libcalendaring)
 $labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
 
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index bd5ec60..a39c1f2 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -642,7 +642,8 @@ ul.toolbarmenu li span.icon.taskadd,
 	font-size: 12px;
 }
 
-.taskhead .flagged {
+.taskhead .flagged,
+.taskshow.status-flagged h2:after {
 	display: inline-block;
 	width: 16px;
 	height: 16px;
@@ -657,7 +658,8 @@ ul.toolbarmenu li span.icon.taskadd,
 	background-position: -2px -3px;
 }
 
-.taskhead.flagged .flagged {
+.taskhead.flagged .flagged,
+.taskshow.status-flagged h2:after {
 	background-position: -2px -23px;
 }
 
@@ -839,8 +841,9 @@ ul.toolbarmenu .sortcol.by-auto a {
 /*** task edit form ***/
 
 #taskedit,
-#taskshow {
-	display:none;
+#taskshow,
+#taskdiff {
+	display: none;
 }
 
 #taskedit {
@@ -850,15 +853,32 @@ ul.toolbarmenu .sortcol.by-auto a {
 	margin: 0 -0.2em;
 }
 
-#taskshow h2 {
+.taskshow h2 {
 	margin-top: -0.5em;
 }
 
-#taskshow label {
+#taskdiff h2 {
+	font-size: 18px;
+	margin: -0.3em 0 0.4em 0;
+}
+
+.taskshow.status-completed h2 {
+	text-decoration: line-through;
+}
+
+.taskshow.status-flagged h2:after {
+	content: " ";
+	position: relative;
+	margin-left: 0.6em;
+	top: 1px;
+	cursor: default;
+}
+
+.taskshow label {
 	color: #999;
 }
 
-#taskshow.status-cancelled {
+.taskshow.status-cancelled {
 	background: url(images/badge_cancelled.png) top right no-repeat;
 }
 
@@ -1048,10 +1068,33 @@ label.block {
 	margin-bottom: 0.3em;
 }
 
-#task-description {
+.task-description {
 	margin-bottom: 1em;
 }
 
+.taskshow .task-text-old,
+.taskshow .task-text-new,
+.taskshow .task-text-diff {
+	padding: 2px;
+}
+
+.taskshow .task-text-diff del,
+.taskshow .task-text-diff ins {
+	text-decoration: none;
+	color: inherit;
+}
+
+.taskshow .task-text-old,
+.taskshow .task-text-diff del {
+	background-color: #fdd;
+	/* text-decoration: line-through; */
+}
+
+.taskshow .task-text-new,
+.taskshow .task-text-diff ins {
+	background-color: #dfd;
+}
+
 #taskedit-completeness-slider {
 	display: inline-block;
 	margin-left: 2em;
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index f80b2f7..dd38409 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -149,6 +149,9 @@
 			<li role="menuitem"><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
 			<li role="menuitem"><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
 			<li role="menuitem"><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
+			<roundcube:if condition="env:tasklist_driver == 'kolab' && config:kolab_bonnie_api" />
+			<li role="menuitem"><roundcube:button command="task-history" type="link" label="tasklist.taskhistory" class="icon" classAct="icon active" innerclass="icon history" /></li>
+			<roundcube:endif />
 		</ul>
 	</div>
 
@@ -159,12 +162,12 @@
 <roundcube:object name="message" id="messagestack" />
 
 
-<div id="taskshow">
+<div id="taskshow" class="taskshow">
 	<div class="form-section" id="task-parent-title"></div>
 	<div class="form-section">
 		<h2 id="task-title"></h2>
 	</div>
-	<div id="task-description" class="form-section">
+	<div id="task-description" class="form-section task-description">
 	</div>
 	<div id="task-tags" class="form-section">
 		<label><roundcube:label name="tasklist.tags" /></label>
@@ -239,6 +242,78 @@
 	<roundcube:object name="plugin.task_rsvp_buttons" id="task-rsvp" class="task-dialog-message" style="display:none" />
 </div>
 
+<roundcube:if condition="env:tasklist_driver == 'kolab' && config:kolab_bonnie_api" />
+<div id="taskhistory" class="uidialog" aria-hidden="true">
+	<roundcube:object name="plugin.object_changelog_table" class="records-table changelog-table" domain="calendar" />
+	<div class="compare-button"><input type="button" class="button" value="↳ <roundcube:label name='tasklist.compare' />" /></div>
+</div>
+
+<div id="taskdiff" class="uidialog taskshow" aria-hidden="true">
+	<h2 class="task-title">Task Title</h2>
+	<h2 class="task-title-new task-text-new"></h2>
+	<div class="form-section task-description">
+		<label><roundcube:label name="calendar.description" /></label>
+		<div class="task-text-diff" style="white-space:pre-wrap"></div>
+		<div class="task-text-old"></div>
+		<div class="task-text-new"></div>
+	</div>
+	<div class="form-section task-flagged">
+		<label><roundcube:label name="tasklist.flagged" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-start">
+		<label><roundcube:label name="tasklist.start" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-date">
+		<label><roundcube:label name="tasklist.datetime" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-recurrence">
+		<label><roundcube:label name="tasklist.repeat" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-alarms">
+		<label><roundcube:label name="tasklist.alarms" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-attendees">
+		<label><roundcube:label name="tasklist.assignedto" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-organizer">
+		<label><roundcube:label name="tasklist.roleorganizer" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-complete">
+		<label><roundcube:label name="tasklist.complete" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-status">
+		<label><roundcube:label name="tasklist.status" /></label>
+		<span class="task-text-old"></span> ⇢
+		<span class="task-text-new"></span>
+	</div>
+	<div class="form-section task-links">
+		<label><roundcube:label name="tasklist.links" /></label>
+		<span class="task-text"></span>
+	</div>
+	<div class="form-section task-attachments">
+		<label><roundcube:label name="attachments" /></label>
+		<div class="task-text-old"></div>
+		<div class="task-text-new"></div>
+	</div>
+</div>
+<roundcube:endif />
+
 <roundcube:include file="/templates/taskedit.html" />
 
 <div id="tasklistform" class="uidialog">
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index a01689f..514d060 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -282,8 +282,14 @@ function rcube_tasklist_ui(settings)
             setTimeout(fetch_counts, 200);
         });
 
+        rcmail.addEventListener('plugin.task_render_changelog', task_render_changelog);
+        rcmail.addEventListener('plugin.task_show_diff', task_show_diff);
+        rcmail.addEventListener('plugin.task_show_revision', function(data){ task_show_dialog(null, data, true); });
+        rcmail.addEventListener('plugin.close_history_dialog', close_history_dialog);
+
         rcmail.register_command('list-sort', list_set_sort, true);
         rcmail.register_command('list-order', list_set_order, (settings.sort_col || 'auto') != 'auto');
+        rcmail.register_command('task-history', task_history_dialog, false);
 
         $('#taskviewsortmenu .by-' + (settings.sort_col || 'auto')).attr('aria-checked', 'true').addClass('selected');
         $('#taskviewsortmenu .sortorder.' + (settings.sort_order || 'asc')).attr('aria-checked', 'true').addClass('selected');
@@ -459,6 +465,7 @@ function rcube_tasklist_ui(settings)
                         rcmail.command('menu-close', 'taskitemmenu');
                     }
                     else {
+                        rcmail.enable_command('task-history', me.tasklists[rec.list] && !!me.tasklists[rec.list].history);
                         rcmail.command('menu-open', { menu: 'taskitemmenu', show: true }, e.target, e);
                         menu.data('refid', id);
                         me.selected_task = rec;
@@ -1835,7 +1842,7 @@ function rcube_tasklist_ui(settings)
     /**
      * Show task details in a dialog
      */
-    function task_show_dialog(id)
+    function task_show_dialog(id, data, temp)
     {
         var $dialog = $('#taskshow'), rec, list;
 
@@ -1848,7 +1855,7 @@ function rcube_tasklist_ui(settings)
             return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
         });
 
-        if (!(rec = listdata[id]) || (rcmail.menu_stack && rcmail.menu_stack.length > 0))
+        if (!(rec = (data || listdata[id])) || (rcmail.menu_stack && rcmail.menu_stack.length > 0))
             return;
 
         me.selected_task = rec;
@@ -1892,6 +1899,10 @@ function rcube_tasklist_ui(settings)
           $dialog.addClass('status-' + String(rec.status).toLowerCase());
         }
 
+        if (rec.flagged) {
+          $dialog.addClass('status-flagged');
+        }
+
         if (rec.recurrence && rec.recurrence_text) {
             $('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
         }
@@ -1986,7 +1997,7 @@ function rcube_tasklist_ui(settings)
                     .html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring')));
             }
 */
-            var show_rsvp = rsvp && list.editable && !is_organizer(rec) && rec.status != 'CANCELLED';
+            var show_rsvp = !temp && rsvp && list.editable && !is_organizer(rec) && rec.status != 'CANCELLED';
             $('#task-rsvp')[(show_rsvp ? 'show' : 'hide')]();
             $('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true);
 
@@ -2036,6 +2047,13 @@ function rcube_tasklist_ui(settings)
           },
           close: function() {
               $dialog.dialog('destroy').appendTo(document.body);
+              $('.libcal-rsvp-replymode').hide();
+          },
+          dragStart: function() {
+              $('.libcal-rsvp-replymode').hide();
+          },
+          resizeStart: function() {
+              $('.libcal-rsvp-replymode').hide();
           },
           buttons: buttons,
           minWidth: 500,
@@ -2065,6 +2083,190 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     *
+     */
+    function task_history_dialog()
+    {
+        var dialog, rec = me.selected_task;
+        if (!rec || !rec.id || !window.libkolab_audittrail) {
+            return false;
+        }
+
+        // render dialog
+        $dialog = libkolab_audittrail.object_history_dialog({
+            module: 'tasklist',
+            container: '#taskhistory',
+            title: rcmail.gettext('objectchangelog','tasklist') + ' - ' + rec.title,
+
+            // callback function for list actions
+            listfunc: function(action, rev) {
+                var rec = $dialog.data('rec');
+                saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+                rcmail.http_post('task', { action: action, t: { id: rec.id, list:rec.list, rev: rev } }, saving_lock);
+            },
+
+            // callback function for comparing two object revisions
+            comparefunc: function(rev1, rev2) {
+                var rec = $dialog.data('rec');
+                saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+                rcmail.http_post('task', { action:'diff', t: { id: rec.id, list: rec.list, rev1: rev1, rev2: rev2 } }, saving_lock);
+            }
+        });
+
+        $dialog.data('rec', rec);
+
+        // fetch changelog data
+        saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+        rcmail.http_post('task', { action: 'changelog', t: { id: rec.id, list: rec.list } }, saving_lock);
+    }
+
+    /**
+     *
+     */
+    function task_render_changelog(data)
+    {
+        var $dialog = $('#taskhistory'),
+            rec = $dialog.data('rec');
+
+        if (data === false || !data.length || !event) {
+          // display 'unavailable' message
+          $('<div class="notfound-message task-dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','tasklist') + '</div>')
+            .insertBefore($dialog.find('.changelog-table').hide());
+          return;
+        }
+
+        data.module = 'tasklist';
+        libkolab_audittrail.render_changelog(data, rec, me.tasklists[rec.list]);
+
+        // set dialog size according to content
+        me.dialog_resize($dialog.get(0), $dialog.height(), 600);
+    }
+
+    /**
+     *
+     */
+    function task_show_diff(data)
+    {
+        var rec = me.selected_task,
+            $dialog = $("#taskdiff");
+
+        $dialog.find('div.form-section, h2.task-title-new').hide().data('set', false).find('.index').html('');
+        $dialog.find('div.form-section.clone').remove();
+
+        // always show event title and date
+        $('.task-title', $dialog).text(rec.title).removeClass('task-text-old').show();
+
+        // show each property change
+        $.each(data.changes, function(i, change) {
+            var prop = change.property, r2, html = false,
+                row = $('div.task-' + prop, $dialog).first();
+
+                // special case: title
+                if (prop == 'title') {
+                    $('.task-title', $dialog).addClass('task-text-old').text(change['old'] || '--');
+                    $('.task-title-new', $dialog).text(change['new'] || '--').show();
+                }
+
+              // no display container for this property
+              if (!row.length) {
+                  return true;
+              }
+
+              // clone row if already exists
+              if (row.data('set')) {
+                  r2 = row.clone().addClass('clone').insertAfter(row);
+                  row = r2;
+              }
+
+              // render description text
+              if (prop == 'description') {
+                  if (!change.diff_ && change['old']) change.old_ = text2html(change['old']);
+                  if (!change.diff_ && change['new']) change.new_ = text2html(change['new']);
+                  html = true;
+              }
+              // format attendees struct
+              else if (prop == 'attendees') {
+                  if (change['old']) change.old_ = task_attendee_html(change['old']);
+                  if (change['new']) change.new_ = task_attendee_html($.extend({}, change['old'] || {}, change['new']));
+                  html = true;
+              }
+              // localize status
+              else if (prop == 'status') {
+                  if (change['old']) change.old_ = rcmail.gettext('status-'+String(change['old']).toLowerCase(), 'tasklist');
+                  if (change['new']) change.new_ = rcmail.gettext('status-'+String(change['new']).toLowerCase(), 'tasklist');
+              }
+
+              // format attachments struct
+              if (prop == 'attachments') {
+                  if (change['old']) task_show_attachments([change['old']], row.children('.task-text-old'), rec, false);
+                  else               row.children('.task-text-old').text('--');
+                  if (change['new']) task_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.task-text-new'), rec, false);
+                  else               row.children('.task-text-new').text('--');
+                  // remove click handler in diff view
+                  $('.attachmentslist li a', row).unbind('click').removeAttr('href');
+              }
+              else if (change.diff_) {
+                  row.children('.task-text-diff').html(change.diff_);
+                  row.children('.task-text-old, .task-text-new').hide();
+              }
+              else {
+                  if (!html) {
+                    // escape HTML characters
+                    change.old_ = Q(change.old_ || change['old'] || '--')
+                    change.new_ = Q(change.new_ || change['new'] || '--')
+                  }
+                  row.children('.task-text-old').html(change.old_ || change['old'] || '--').show();
+                  row.children('.task-text-new').html(change.new_ || change['new'] || '--').show();
+              }
+
+              // display index number
+              if (typeof change.index != 'undefined') {
+                  row.find('.index').html('(' + change.index + ')');
+              }
+
+              row.show().data('set', true);
+        });
+
+        var buttons = {};
+        buttons[rcmail.gettext('close')] = function() {
+            $dialog.dialog('close');
+        };
+
+        // open jquery UI dialog
+        $dialog.dialog({
+            modal: false,
+            resizable: true,
+            closeOnEscape: true,
+            title: rcmail.gettext('objectdiff','tasklist').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + rec.title,
+            open: function() {
+                $dialog.attr('aria-hidden', 'false');
+                setTimeout(function(){
+                    $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+                }, 5);
+            },
+            close: function() {
+                $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+            },
+            buttons: buttons,
+            minWidth: 320,
+            width: 450
+        }).show();
+
+        // set dialog size according to content
+        me.dialog_resize($dialog.get(0), $dialog.height(), 400);
+    }
+
+    // close the event history dialog
+    function close_history_dialog()
+    {
+        $('#taskhistory, #taskdiff').each(function(i, elem) {
+        var $dialog = $(elem);
+        if ($dialog.is(':ui-dialog'))
+            $dialog.dialog('close');
+        });
+    };
+
+    /**
      * Opens the dialog to edit a task
      */
     function task_edit_dialog(id, action, presets)
@@ -2371,17 +2573,22 @@ function rcube_tasklist_ui(settings)
         if (!rec.id || rec.id < 0)
             return false;
 
-        var qstring = '_id='+urlencode(att.id)+'&_t='+urlencode(rec.recurrence_id||rec.id)+'&_list='+urlencode(rec.list);
+        var query = { _id: att.id, _t: rec.recurrence_id||rec.id, _list:rec.list, _frame: 1 };
+        if (rec.rev)
+            query._rev = event.rev;
+
 
         // open attachment in frame if it's of a supported mimetype
         // similar as in app.js and calendar_ui.js
         if (att.id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
-            if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) {
+            if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
                 return;
             }
         }
 
-        rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
+        query._frame = null;
+        query._download = 1;
+        rcmail.goto_url('get-attachment', query, false);
     };
 
     /**
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 780602c..449f43c 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -208,7 +208,7 @@ class tasklist extends rcube_plugin
         $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
         $rec    = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
         $oldrec = $rec;
-        $success = $refresh = false;
+        $success = $refresh = $got_msg = false;
 
         // force notify if hidden + active
         $itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
@@ -385,13 +385,115 @@ class tasklist extends rcube_plugin
                 }
             }
             break;
+
+        case 'changelog':
+            $data = $this->driver->get_task_changelog($rec);
+            if (is_array($data) && !empty($data)) {
+                $lib = $this->lib;
+                $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
+                array_walk($data, function(&$change) use ($lib, $dtformat) {
+                  if ($change['date']) {
+                    $dt = $lib->adjust_timezone($change['date']);
+                    if ($dt instanceof DateTime)
+                      $change['date'] = $this->rc->format_date($dt, $dtformat, false);
+                  }
+                });
+                $this->rc->output->command('plugin.task_render_changelog', $data);
+            }
+            else {
+                $this->rc->output->command('plugin.task_render_changelog', false);
+            }
+            $got_msg = true;
+            break;
+
+        case 'diff':
+            $data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']);
+            if (is_array($data)) {
+                // convert some properties, similar to self::_client_event()
+                $lib = $this->lib;
+                $date_format = $this->rc->config->get('date_format', 'Y-m-d');
+                $time_format = $this->rc->config->get('time_format', 'H:i');
+                array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) {
+                    // convert date cols
+                    if (in_array($change['property'], array('date','start','created','changed'))) {
+                        if (!empty($change['old'])) {
+                            $dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format;
+                            $change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat);
+                        }
+                        if (!empty($change['new'])) {
+                            $dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format;
+                            $change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat);
+                        }
+                    }
+                    // create textual representation for alarms and recurrence
+                    if ($change['property'] == 'alarms') {
+                        if (is_array($change['old']))
+                            $change['old_'] = libcalendaring::alarm_text($change['old']);
+                        if (is_array($change['new']))
+                            $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
+                    }
+                    if ($change['property'] == 'recurrence') {
+                        if (is_array($change['old']))
+                            $change['old_'] = $lib->recurrence_text($change['old']);
+                        if (is_array($change['new']))
+                            $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
+                    }
+                    if ($change['property'] == 'complete') {
+                        $change['old_'] = intval($change['old']) . '%';
+                        $change['new_'] = intval($change['new']) . '%';
+                    }
+                    if ($change['property'] == 'attachments') {
+                        if (is_array($change['old']))
+                            $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
+                        if (is_array($change['new'])) {
+                            $change['new'] = array_merge((array)$change['old'], $change['new']);
+                            $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
+                        }
+                    }
+                    // compute a nice diff of description texts
+                    if ($change['property'] == 'description') {
+                        $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
+                    }
+                });
+                $this->rc->output->command('plugin.task_show_diff', $data);
+            }
+            else {
+                $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
+            }
+            $got_msg = true;
+            break;
+
+        case 'show':
+            if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) {
+                $this->encode_task($rec);
+                $rec['readonly'] = 1;
+                $this->rc->output->command('plugin.task_show_revision', $rec);
+            }
+            else {
+                $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
+            }
+            $got_msg = true;
+            break;
+
+        case 'restore':
+            if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) {
+                $refresh = $this->driver->get_task($rec);
+                $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation');
+                $this->rc->output->command('plugin.close_history_dialog');
+            }
+            else {
+                $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
+            }
+            $got_msg = true;
+            break;
+
         }
 
         if ($success) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
             $this->update_counts($oldrec, $refresh);
         }
-        else {
+        else if (!$got_msg) {
             $this->rc->output->show_message('tasklist.errorsaving', 'error');
         }
 
@@ -1268,7 +1370,7 @@ class tasklist extends rcube_plugin
         $this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
         $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
         $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
-        $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
+        $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'close', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
 
         $this->rc->output->set_pagetitle($this->gettext('navtitle'));
         $this->rc->output->send('tasklist.mainview');
@@ -1396,8 +1498,9 @@ class tasklist extends rcube_plugin
         $task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
         $list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
         $id   = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+        $rev  = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
 
-        $task = array('id' => $task, 'list' => $list);
+        $task = array('id' => $task, 'list' => $list, 'rev' => $rev);
         $attachment = $this->driver->get_attachment($id, $task);
 
         // show part page
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index a46aa1e..97b1633 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -156,6 +156,7 @@ class tasklist_ui
         $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select'));
         $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
         $this->plugin->register_handler('plugin.task_rsvp_buttons', array($this->plugin->itip, 'itip_rsvp_buttons'));
+        $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
 
         jqueryui::tagedit();
 
@@ -165,6 +166,7 @@ class tasklist_ui
         // include kolab folderlist widget if available
         if (in_array('libkolab', $this->plugin->api->loaded_plugins())) {
             $this->plugin->api->include_script('libkolab/js/folderlist.js');
+            $this->plugin->api->include_script('libkolab/js/audittrail.js');
         }
     }
 


commit dcb60dbee171a5b732bbdd289d3aa7f29f5c16ae
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 25 11:55:21 2015 +0100

    Move more audit trail code to libkolab; unify text labels

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 53d3f20..81ca66e 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1114,7 +1114,7 @@ class calendar extends rcube_plugin
           $this->rc->output->command('plugin.event_show_diff', $data);
         }
         else {
-          $this->rc->output->command('display_message', $this->gettext('eventdiffnotavailable'), 'error');
+          $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
         }
         $got_msg = true;
         $reload = false;
@@ -1125,7 +1125,7 @@ class calendar extends rcube_plugin
           $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
         }
         else {
-          $this->rc->output->command('display_message', $this->gettext('eventnotfound'), 'error');
+          $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
         }
         $got_msg = true;
         $reload = false;
@@ -1135,11 +1135,11 @@ class calendar extends rcube_plugin
         if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
           $_event = $this->driver->get_event($event);
           $reload = $_event['recurrence'] ? 2 : 1;
-          $this->rc->output->command('display_message', $this->gettext(array('name' => 'eventrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation');
+          $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation');
           $this->rc->output->command('plugin.close_history_dialog');
         }
         else {
-          $this->rc->output->command('display_message', $this->gettext('eventrestoreerror'), 'error');
+          $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
           $reload = 0;
         }
         $got_msg = true;
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 6c26e5a..84c5adb 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1025,7 +1025,7 @@ function rcube_calendar_ui(settings)
       var $dialog = libkolab_audittrail.object_history_dialog({
         module: 'calendar',
         container: '#eventhistory',
-        title: rcmail.gettext('eventchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
+        title: rcmail.gettext('objectchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
 
         // callback function for list actions
         listfunc: function(action, rev) {
@@ -1060,6 +1060,7 @@ function rcube_calendar_ui(settings)
         return;
       }
 
+      data.module = 'calendar';
       libkolab_audittrail.render_changelog(data, event, me.calendars[event.calendar]);
 
       // set dialog size according to content
@@ -1177,7 +1178,7 @@ function rcube_calendar_ui(settings)
         modal: false,
         resizable: true,
         closeOnEscape: true,
-        title: rcmail.gettext('eventdiff','calendar').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + event.title,
+        title: rcmail.gettext('objectdiff','calendar').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + event.title,
         open: function() {
           $dialog.attr('aria-hidden', 'false');
           setTimeout(function(){
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 5a1f41e..2912bdf 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -96,7 +96,7 @@ class calendar_ui
     $this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
     $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
     $this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
-    $this->cal->register_handler('plugin.event_changelog_table', array($this, 'event_changelog_table'));
+    $this->cal->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
     $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));  // use generic method from rcube_template
   }
 
@@ -855,22 +855,6 @@ class calendar_ui
   }
 
   /**
-   * Table oultine for event changelog display
-   */
-  function event_changelog_table($attrib = array())
-  {
-    $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
-    $table->add_header('diff', '');
-    $table->add_header('revision', $this->cal->gettext('revision'));
-    $table->add_header('date', $this->cal->gettext('date'));
-    $table->add_header('user', $this->cal->gettext('user'));
-    $table->add_header('operation', $this->cal->gettext('operation'));
-    $table->add_header('actions', ' ');
-
-    return $table->show($attrib);
-  }
-
-  /**
    *
    */
   function event_invitebox($attrib = array())
diff --git a/plugins/calendar/localization/bg_BG.inc b/plugins/calendar/localization/bg_BG.inc
index 76d1d84..afbdfe1 100644
--- a/plugins/calendar/localization/bg_BG.inc
+++ b/plugins/calendar/localization/bg_BG.inc
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * Localizations for Kolab Calendar plugin
  *
@@ -7,7 +6,6 @@
  *
  * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
  */
-
 $labels['default_view'] = 'Изглед по подразбиране';
 $labels['time_format'] = 'Формат на часовете';
 $labels['first_day'] = 'Първи ден от седмицата';
@@ -120,5 +118,5 @@ $labels['deleteventconfirm'] = 'Наистина ли искате да прем
 $labels['deletecalendarconfirm'] = 'Наистина ли искате да премахнете този календар с всичките му събития?';
 $labels['savingdata'] = 'Запазване на данни...';
 $labels['errorsaving'] = 'Неуспешно записването на промените.';
-
+$labels['futurevents'] = 'Бъдещи';
 ?>
diff --git a/plugins/calendar/localization/ca_ES.inc b/plugins/calendar/localization/ca_ES.inc
index d3c5ff9..9226ef5 100644
--- a/plugins/calendar/localization/ca_ES.inc
+++ b/plugins/calendar/localization/ca_ES.inc
@@ -239,7 +239,7 @@ $labels['displaybirthdayscalendar'] = 'Mostra el calendari d\'aniversaris';
 $labels['birthdayscalendarsources'] = 'D\'aquestes llibretes d\'adreces';
 $labels['birthdayeventtitle'] = 'Aniversari de $name';
 $labels['birthdayage'] = 'Edat $age';
-$labels['eventchangelog'] = 'Canvia historial';
+$labels['objectchangelog'] = 'Canvia historial';
 $labels['revision'] = 'Revisió';
 $labels['user'] = 'Usuari';
 $labels['operation'] = 'Acció';
@@ -249,9 +249,9 @@ $labels['actiondelete'] = 'Suprimit';
 $labels['compare'] = 'Compara';
 $labels['showrevision'] = 'Mostra aquesta versió';
 $labels['restore'] = 'Restaura aquesta versió';
-$labels['eventnotfound'] = 'No s\'han pogut carregar les dades d\'aquest esdeveniment';
+$labels['objectnotfound'] = 'No s\'han pogut carregar les dades d\'aquest esdeveniment';
 $labels['objectchangelognotavailable'] = 'No està disponible canviar l\'historial d\'aquest esdeveniment';
-$labels['eventdiffnotavailable'] = 'No és possible comparar les revisions seleccionades';
+$labels['objectdiffnotavailable'] = 'No és possible comparar les revisions seleccionades';
 $labels['revisionrestoreconfirm'] = 'Esteu segurs de voler restaurar la revisió $rev d\'aquest esdeveniment? Això substituirà l\'actual esdeveniment per una versió antiga.';
 $labels['arialabelminical'] = 'Selecció de la data del calendari';
 $labels['arialabelcalendarview'] = 'Vista del calendari';
diff --git a/plugins/calendar/localization/cs_CZ.inc b/plugins/calendar/localization/cs_CZ.inc
index 1ed6cf3..8f47574 100644
--- a/plugins/calendar/localization/cs_CZ.inc
+++ b/plugins/calendar/localization/cs_CZ.inc
@@ -246,7 +246,7 @@ $labels['displaybirthdayscalendar'] = 'Zobrazit kalendář narozenin';
 $labels['birthdayscalendarsources'] = 'Z těchto adresářů';
 $labels['birthdayeventtitle'] = 'Narozeniny $name';
 $labels['birthdayage'] = 'Věk $age';
-$labels['eventchangelog'] = 'Historie změn';
+$labels['objectchangelog'] = 'Historie změn';
 $labels['revision'] = 'Verze';
 $labels['user'] = 'Uživatel';
 $labels['operation'] = 'ÄŒinnost';
@@ -256,9 +256,9 @@ $labels['actiondelete'] = 'Smazáno';
 $labels['compare'] = 'Porovnat';
 $labels['showrevision'] = 'Ukázat tuto verzi';
 $labels['restore'] = 'Obnovit tuto verzi';
-$labels['eventnotfound'] = 'Nepodařilo se nahrát data události';
+$labels['objectnotfound'] = 'Nepodařilo se nahrát data události';
 $labels['objectchangelognotavailable'] = 'Historie změn není pro tuto událost dostupná';
-$labels['eventdiffnotavailable'] = 'Pro vybrané verze není žádné srovnání možné';
+$labels['objectdiffnotavailable'] = 'Pro vybrané verze není žádné srovnání možné';
 $labels['revisionrestoreconfirm'] = 'Opravdu chcete obnovit změnu $rev této události? Tímto dojde k nahrazení nynější události starou verzí.';
 $labels['arialabelminical'] = 'Výběr data v kalendáři';
 $labels['arialabelcalendarview'] = 'Pohled na kalendář';
diff --git a/plugins/calendar/localization/da_DK.inc b/plugins/calendar/localization/da_DK.inc
index 05c9b71..e44851e 100644
--- a/plugins/calendar/localization/da_DK.inc
+++ b/plugins/calendar/localization/da_DK.inc
@@ -247,7 +247,7 @@ $labels['displaybirthdayscalendar'] = 'Vis fødselsdagskalender';
 $labels['birthdayscalendarsources'] = 'Fra disse adressebøger';
 $labels['birthdayeventtitle'] = '$name har fødselsdag';
 $labels['birthdayage'] = '$age år';
-$labels['eventchangelog'] = 'Ændringshistorik';
+$labels['objectchangelog'] = 'Ændringshistorik';
 $labels['revision'] = 'Revision';
 $labels['user'] = 'Bruger';
 $labels['operation'] = 'Handling';
@@ -257,9 +257,9 @@ $labels['actiondelete'] = 'Slettet';
 $labels['compare'] = 'Sammenlign';
 $labels['showrevision'] = 'Vis denne version';
 $labels['restore'] = 'Genskab denne version';
-$labels['eventnotfound'] = 'Kunne ikke indlæse begivenhedsdata';
+$labels['objectnotfound'] = 'Kunne ikke indlæse begivenhedsdata';
 $labels['objectchangelognotavailable'] = 'Ændringshistorikken er ikke tilgængelig for denne begivenhed';
-$labels['eventdiffnotavailable'] = 'Det er ikke muligt at sammenligne de valgte revisioner';
+$labels['objectdiffnotavailable'] = 'Det er ikke muligt at sammenligne de valgte revisioner';
 $labels['revisionrestoreconfirm'] = 'Sikker på at du vil genskabe revision $rev af denne begivenhed? Dette vil erstatte den nuværende begivenhed med den tidligere version.';
 $labels['arialabelminical'] = 'Valg af kalenderdato';
 $labels['arialabelcalendarview'] = 'Kalendervisning';
diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc
index 1bbf464..91726ca 100644
--- a/plugins/calendar/localization/de_CH.inc
+++ b/plugins/calendar/localization/de_CH.inc
@@ -36,7 +36,6 @@ $labels['new_event'] = 'Neuer Termin';
 $labels['edit_event'] = 'Termin bearbeiten';
 $labels['edit'] = 'Bearbeiten';
 $labels['save'] = 'Speichern';
-$labels['removelist'] = 'Remove from list';
 $labels['cancel'] = 'Abbrechen';
 $labels['select'] = 'Auswählen';
 $labels['print'] = 'Drucken';
@@ -165,7 +164,6 @@ $labels['itipsendsuccess'] = 'Einladung an Teilnehmer versendet.';
 $labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden';
 $labels['itipinvalidrequest'] = 'Diese Einladung ist nicht mehr gültig';
 $labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet';
-$labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your calendar and not be sent to the organizer of the event.';
 $labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert';
 $labels['importnone'] = 'Keine Termine zum Importieren gefunden';
 $labels['importerror'] = 'Fehler beim Importieren';
diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc
index ab5a5af..8d72985 100644
--- a/plugins/calendar/localization/de_DE.inc
+++ b/plugins/calendar/localization/de_DE.inc
@@ -247,8 +247,8 @@ $labels['displaybirthdayscalendar'] = 'Geburtstags-Kalender anzeigen';
 $labels['birthdayscalendarsources'] = 'Für diese Adressbücher';
 $labels['birthdayeventtitle'] = '$names Geburtstag';
 $labels['birthdayage'] = 'Alter $age';
-$labels['eventchangelog'] = 'Änderungshistorie';
-$labels['eventdiff'] = 'Änderungen aus $rev1 nach $rev2';
+$labels['objectchangelog'] = 'Änderungshistorie';
+$labels['objectdiff'] = 'Änderungen aus $rev1 nach $rev2';
 $labels['revision'] = 'Version';
 $labels['user'] = 'Benutzer';
 $labels['operation'] = 'Aktion';
@@ -258,12 +258,12 @@ $labels['actiondelete'] = 'Gelöscht';
 $labels['compare'] = 'Vergleichen';
 $labels['showrevision'] = 'Diese Version anzeigen';
 $labels['restore'] = 'Diese Version wiederherstellen';
-$labels['eventnotfound'] = 'Termindaten sind leider nicht vergübar';
+$labels['objectnotfound'] = 'Termindaten sind leider nicht vergübar';
 $labels['objectchangelognotavailable'] = 'Änderungshistorie ist nicht verfügbar für diesen Termin';
-$labels['eventdiffnotavailable'] = 'Vergleich für die gewählten Versionen nicht möglich';
+$labels['objectdiffnotavailable'] = 'Vergleich für die gewählten Versionen nicht möglich';
 $labels['revisionrestoreconfirm'] = 'Wollen Sie wirklich die Version $rev dieses Termins wiederherstellen? Diese Aktion wird die aktuelle Kopie mit der älteren Version ersetzen.';
-$labels['eventrestoresuccess'] = 'Revision $rev erfolgreich wiederhergestellt';
-$labels['eventrestoreerror'] = 'Fehler beim Wiederherstellen der alten Revision';
+$labels['objectrestoresuccess'] = 'Revision $rev erfolgreich wiederhergestellt';
+$labels['objectrestoreerror'] = 'Fehler beim Wiederherstellen der alten Revision';
 $labels['arialabelminical'] = 'Kalender Datumswahl';
 $labels['arialabelcalendarview'] = 'Kalender Ansicht';
 $labels['arialabelsearchform'] = 'Suchformular für Termine';
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 3a15b6f..d967fc7 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -276,8 +276,8 @@ $labels['birthdayeventtitle'] = '$name\'s Birthday';
 $labels['birthdayage'] = 'Age $age';
 
 // history dialog
-$labels['eventchangelog'] = 'Change History';
-$labels['eventdiff'] = 'Changes from $rev1 to $rev2';
+$labels['objectchangelog'] = 'Change History';
+$labels['objectdiff'] = 'Changes from $rev1 to $rev2';
 $labels['revision'] = 'Revision';
 $labels['user'] = 'User';
 $labels['operation'] = 'Action';
@@ -287,12 +287,12 @@ $labels['actiondelete'] = 'Deleted';
 $labels['compare'] = 'Compare';
 $labels['showrevision'] = 'Show this version';
 $labels['restore'] = 'Restore this version';
-$labels['eventnotfound'] = 'Failed to load event data';
+$labels['objectnotfound'] = 'Failed to load event data';
 $labels['objectchangelognotavailable'] = 'Change history is not available for this event';
-$labels['eventdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions';
 $labels['revisionrestoreconfirm'] = 'Do you really want to restore revision $rev of this event? This will replace the current event with the old version.';
-$labels['eventrestoresuccess'] = 'Revision $rev successfully restored';
-$labels['eventrestoreerror'] = 'Failed to restore the old revision';
+$labels['objectrestoresuccess'] = 'Revision $rev successfully restored';
+$labels['objectrestoreerror'] = 'Failed to restore the old revision';
 
 
 // (hidden) titles and labels for accessibility annotations
diff --git a/plugins/calendar/localization/es_AR.inc b/plugins/calendar/localization/es_AR.inc
index 1bb9b51..2a66a0f 100644
--- a/plugins/calendar/localization/es_AR.inc
+++ b/plugins/calendar/localization/es_AR.inc
@@ -242,7 +242,7 @@ $labels['displaybirthdayscalendar'] = 'Mostrar calendario de cumpleaños';
 $labels['birthdayscalendarsources'] = 'De estas libretas de direcciones';
 $labels['birthdayeventtitle'] = 'Cumpleaños de $name';
 $labels['birthdayage'] = 'Edad $age';
-$labels['eventchangelog'] = 'Cambiar Historial';
+$labels['objectchangelog'] = 'Cambiar Historial';
 $labels['revision'] = 'Revisión';
 $labels['user'] = 'Usuario';
 $labels['operation'] = 'Acción';
@@ -252,9 +252,9 @@ $labels['actiondelete'] = 'Eliminado';
 $labels['compare'] = 'Comparar';
 $labels['showrevision'] = 'Mostrar esta versión';
 $labels['restore'] = 'Recuperar esta versión';
-$labels['eventnotfound'] = 'Fallo al cargar datos del evento';
+$labels['objectnotfound'] = 'Fallo al cargar datos del evento';
 $labels['objectchangelognotavailable'] = 'Cambiar historial no esta disponible para este evento';
-$labels['eventdiffnotavailable'] = 'No es posible comparar las revisiones seleccionadas';
+$labels['objectdiffnotavailable'] = 'No es posible comparar las revisiones seleccionadas';
 $labels['revisionrestoreconfirm'] = 'Confirme que quiere recuperar la revisión $rev de este evento. Esta acción reemplazará el evento actual con la versión anterior.';
 $labels['arialabelminical'] = 'Selección de fecha del calendario';
 $labels['arialabelcalendarview'] = 'Vista del calendario';
diff --git a/plugins/calendar/localization/es_ES.inc b/plugins/calendar/localization/es_ES.inc
index 6dc876b..1b025fb 100644
--- a/plugins/calendar/localization/es_ES.inc
+++ b/plugins/calendar/localization/es_ES.inc
@@ -11,8 +11,6 @@ $labels['name'] = 'Nombre';
 $labels['edit'] = 'Editar';
 $labels['save'] = 'Guardar';
 $labels['cancel'] = 'Cancelar';
-$labels['description'] = 'Descripción';
-$labels['confidential'] = 'confidential';
 $labels['comment'] = 'Comentario';
 $labels['eventoptions'] = 'Opciones';
 $labels['rolerequired'] = 'Requerido';
diff --git a/plugins/calendar/localization/et_EE.inc b/plugins/calendar/localization/et_EE.inc
deleted file mode 100644
index 55b92a0..0000000
--- a/plugins/calendar/localization/et_EE.inc
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-/**
- * Localizations for Kolab Calendar plugin
- *
- * Copyright (C) 2014, Kolab Systems AG
- *
- * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
- */
-$labels['name'] = 'Nimi';
-$labels['save'] = 'Salvesta';
-$labels['confidential'] = 'confidential';
-$labels['rolerequired'] = 'Kohustuslik';
-$labels['operation'] = 'Toiming';
-?>
diff --git a/plugins/calendar/localization/fi_FI.inc b/plugins/calendar/localization/fi_FI.inc
index 35d1065..2d67e4e 100644
--- a/plugins/calendar/localization/fi_FI.inc
+++ b/plugins/calendar/localization/fi_FI.inc
@@ -89,6 +89,7 @@ $labels['generated'] = 'generoitu';
 $labels['eventhistory'] = 'Historia';
 $labels['removelink'] = 'Poista sähköpostiviittaus';
 $labels['printdescriptions'] = 'Tulosta kuvaukset';
+$labels['parentcalendar'] = 'Aseta sisään';
 $labels['searchearlierdates'] = '« Etsi aiempia tapahtumia';
 $labels['searchlaterdates'] = 'Etsi myöhempiä tapahtumia »';
 $labels['andnmore'] = '$nr lisää...';
@@ -243,8 +244,8 @@ $labels['displaybirthdayscalendar'] = 'Näytä syntymäpäivät kalenterissa';
 $labels['birthdayscalendarsources'] = 'Näistä osoitekirjoista';
 $labels['birthdayeventtitle'] = 'Syntymäpäivä: $name';
 $labels['birthdayage'] = 'Ikä $age';
-$labels['eventchangelog'] = 'Muuta historiaa';
-$labels['eventdiff'] = 'Muutokset versiosta $rev1 versioon $rev2';
+$labels['objectchangelog'] = 'Muuta historiaa';
+$labels['objectdiff'] = 'Muutokset versiosta $rev1 versioon $rev2';
 $labels['revision'] = 'Versio';
 $labels['user'] = 'Käyttäjä';
 $labels['operation'] = 'Toiminto';
@@ -254,12 +255,12 @@ $labels['actiondelete'] = 'Poistettu';
 $labels['compare'] = 'Vertaa';
 $labels['showrevision'] = 'Näytä tämä versio';
 $labels['restore'] = 'Palauta tämä versio';
-$labels['eventnotfound'] = 'Tapahtumadatan lataus epäonnistui';
+$labels['objectnotfound'] = 'Tapahtumadatan lataus epäonnistui';
 $labels['objectchangelognotavailable'] = 'Tapahtuman muutoshistoria ei ole saatavilla';
-$labels['eventdiffnotavailable'] = 'Vertailu ei ole saatavilla valittujen versioiden välillä';
+$labels['objectdiffnotavailable'] = 'Vertailu ei ole saatavilla valittujen versioiden välillä';
 $labels['revisionrestoreconfirm'] = 'Haluatko varmasti palauttaa tämän tapahtuman version $rev? Nykyinen tapahtuma korvataan vanhalla versiolla.';
-$labels['eventrestoresuccess'] = 'Versio $rev palautettiin onnistuneesti';
-$labels['eventrestoreerror'] = 'Vanhan version palauttaminen epäonnistui';
+$labels['objectrestoresuccess'] = 'Versio $rev palautettiin onnistuneesti';
+$labels['objectrestoreerror'] = 'Vanhan version palauttaminen epäonnistui';
 $labels['arialabelminical'] = 'Kalenterin ajankohdan valinta';
 $labels['arialabelcalendarview'] = 'Kalenterinäkymä';
 $labels['arialabelsearchform'] = 'Tapahtumahaun lomake';
diff --git a/plugins/calendar/localization/fr_FR.inc b/plugins/calendar/localization/fr_FR.inc
index 076a7d6..1c89721 100644
--- a/plugins/calendar/localization/fr_FR.inc
+++ b/plugins/calendar/localization/fr_FR.inc
@@ -244,7 +244,7 @@ $labels['displaybirthdayscalendar'] = 'Afficher le calendrier des anniversaires'
 $labels['birthdayscalendarsources'] = 'Depuis ces carnets d\'adresses';
 $labels['birthdayeventtitle'] = 'Anniversaire de $name';
 $labels['birthdayage'] = 'Age $age';
-$labels['eventchangelog'] = 'Historique des modifications';
+$labels['objectchangelog'] = 'Historique des modifications';
 $labels['revision'] = 'Version';
 $labels['user'] = 'Utilisateur';
 $labels['operation'] = 'Action';
@@ -254,9 +254,9 @@ $labels['actiondelete'] = 'Supprimé';
 $labels['compare'] = 'Comparer';
 $labels['showrevision'] = 'Afficher cette version';
 $labels['restore'] = 'Restaurer cette version';
-$labels['eventnotfound'] = 'Impossible de charger les données de l’évènement';
+$labels['objectnotfound'] = 'Impossible de charger les données de l’évènement';
 $labels['objectchangelognotavailable'] = 'Il n\'y a pas d\'historique des modifications pour cet évènement';
-$labels['eventdiffnotavailable'] = 'La comparaison des versions sélectionnées est impossible';
+$labels['objectdiffnotavailable'] = 'La comparaison des versions sélectionnées est impossible';
 $labels['revisionrestoreconfirm'] = 'Voulez-vous vraiment restaurer le version $rev de cet évènement ? Cette action va remplacer l\'évènement courent  par l\'ancienne version.';
 $labels['arialabelminical'] = 'Sélection de la date du calendrier';
 $labels['arialabelcalendarview'] = 'Vue du calendrier';
diff --git a/plugins/calendar/localization/it_IT.inc b/plugins/calendar/localization/it_IT.inc
index 78b6a05..2cc3ce7 100644
--- a/plugins/calendar/localization/it_IT.inc
+++ b/plugins/calendar/localization/it_IT.inc
@@ -246,7 +246,7 @@ $labels['displaybirthdayscalendar'] = 'Mostra il calendario compleanni';
 $labels['birthdayscalendarsources'] = 'Da queste rubriche';
 $labels['birthdayeventtitle'] = 'Compleanno di $name';
 $labels['birthdayage'] = 'Età: $age anni';
-$labels['eventchangelog'] = 'Storico modifiche';
+$labels['objectchangelog'] = 'Storico modifiche';
 $labels['revision'] = 'Revisione';
 $labels['user'] = 'Utente';
 $labels['operation'] = 'Azione';
@@ -256,9 +256,9 @@ $labels['actiondelete'] = 'Cancellato';
 $labels['compare'] = 'Confronta';
 $labels['showrevision'] = 'Mostra questa versione';
 $labels['restore'] = 'Rirpistina questa versione';
-$labels['eventnotfound'] = 'Caricamento dati dell\'evento fallito';
+$labels['objectnotfound'] = 'Caricamento dati dell\'evento fallito';
 $labels['objectchangelognotavailable'] = 'Lo storico modifiche non è disponibile per questo evento';
-$labels['eventdiffnotavailable'] = 'Nessun confronto possibile tra le revisioni selezionate';
+$labels['objectdiffnotavailable'] = 'Nessun confronto possibile tra le revisioni selezionate';
 $labels['revisionrestoreconfirm'] = 'Vuoi veramente ripristinare la revisione $rev di questo evento? L\'evento corrente verrà sostituito dalla vecchia versione.';
 $labels['arialabelminical'] = 'Selezione della data del calendario';
 $labels['arialabelcalendarview'] = 'Vista calendario';
diff --git a/plugins/calendar/localization/pl_PL.inc b/plugins/calendar/localization/pl_PL.inc
index 4495e2f..7e6d5ac 100644
--- a/plugins/calendar/localization/pl_PL.inc
+++ b/plugins/calendar/localization/pl_PL.inc
@@ -245,7 +245,7 @@ $labels['displaybirthdayscalendar'] = 'Wyświetl kalendarz urodzin';
 $labels['birthdayscalendarsources'] = 'Z tych książek adresowych';
 $labels['birthdayeventtitle'] = 'Urodziny $name\'s';
 $labels['birthdayage'] = 'Wiek $age';
-$labels['eventchangelog'] = 'Historia zmian';
+$labels['objectchangelog'] = 'Historia zmian';
 $labels['revision'] = 'Wersja';
 $labels['user'] = 'Użytkownik';
 $labels['operation'] = 'Akcja';
@@ -255,9 +255,9 @@ $labels['actiondelete'] = 'Usunięte';
 $labels['compare'] = 'Porównaj';
 $labels['showrevision'] = 'Pokaż tą wersję';
 $labels['restore'] = 'Przywróć tą wersję';
-$labels['eventnotfound'] = 'Nie udało się wczytać zdarzenia';
+$labels['objectnotfound'] = 'Nie udało się wczytać zdarzenia';
 $labels['objectchangelognotavailable'] = 'Historia zmian jest niedostępna dla tego zdarzenia';
-$labels['eventdiffnotavailable'] = 'Nie można porównać wybranych wersji';
+$labels['objectdiffnotavailable'] = 'Nie można porównać wybranych wersji';
 $labels['revisionrestoreconfirm'] = 'Czy na pewno chcesz przywrócić wersję $rev tego zdarzenia? Bierzące zdarzenie zostanie zastąpione starszą wersją.';
 $labels['arialabelminical'] = 'Wybór daty kalendarza';
 $labels['arialabelcalendarview'] = 'PodglÄ…d kalendarza';
diff --git a/plugins/calendar/localization/pt_PT.inc b/plugins/calendar/localization/pt_PT.inc
new file mode 100644
index 0000000..d6b38ab
--- /dev/null
+++ b/plugins/calendar/localization/pt_PT.inc
@@ -0,0 +1,274 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Visualização padrão';
+$labels['time_format'] = 'Formato da hora';
+$labels['timeslots'] = 'Entradas por hora';
+$labels['first_day'] = 'Primeiro dia da semana';
+$labels['first_hour'] = 'Primeira hora a mostrar';
+$labels['workinghours'] = 'Horário de trabalho';
+$labels['add_category'] = 'Adicionar categoria';
+$labels['remove_category'] = 'Remover categoria';
+$labels['defaultcalendar'] = 'Criar novos eventos em';
+$labels['eventcoloring'] = 'Cores dos eventos';
+$labels['coloringmode0'] = 'De acordo com o calendário';
+$labels['coloringmode1'] = 'De acordo com a categoria';
+$labels['coloringmode2'] = 'Calendário para esboço, categoria para conteúdo';
+$labels['coloringmode3'] = 'Categoria para esboço, calendário para conteúdo';
+$labels['afternothing'] = 'Manter';
+$labels['aftertrash'] = 'Enviar para o lixo';
+$labels['afterdelete'] = 'Eliminar a mensagem';
+$labels['afterflagdeleted'] = 'Marcar como eliminada';
+$labels['aftermoveto'] = 'Mover para...';
+$labels['itipoptions'] = 'Convites de eventos';
+$labels['afteraction'] = 'Depois do processamento de um convite ou mensagem de alteração';
+$labels['calendar'] = 'Calendário';
+$labels['calendars'] = 'Calendários';
+$labels['category'] = 'Categoria';
+$labels['categories'] = 'Categorias';
+$labels['createcalendar'] = 'Criar um novo calendário';
+$labels['editcalendar'] = 'Alterar propriedades do calendário';
+$labels['name'] = 'Nome';
+$labels['color'] = 'Cor';
+$labels['day'] = 'Dia';
+$labels['week'] = 'Semana';
+$labels['month'] = 'Mês';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Novo';
+$labels['new_event'] = 'Novo evento';
+$labels['edit_event'] = 'Alterar evento';
+$labels['edit'] = 'Alterar';
+$labels['save'] = 'Guardar';
+$labels['removelist'] = 'Remover da lista';
+$labels['cancel'] = 'Cancelar';
+$labels['select'] = 'Selecionar';
+$labels['print'] = 'Imprimir';
+$labels['printtitle'] = 'Imprimir calendários';
+$labels['title'] = 'Sumário';
+$labels['description'] = 'Descrição';
+$labels['all-day'] = 'dia todo';
+$labels['export'] = 'Exportar';
+$labels['exporttitle'] = 'Exportar para iCalendar';
+$labels['exportrange'] = 'Eventos de';
+$labels['exportattachments'] = 'Incluir anexos';
+$labels['customdate'] = 'Definir data';
+$labels['location'] = 'Local';
+$labels['url'] = 'Endereço web';
+$labels['date'] = 'Data';
+$labels['start'] = 'Início';
+$labels['starttime'] = 'Hora de início';
+$labels['end'] = 'Fim';
+$labels['endtime'] = 'Hora de fim';
+$labels['repeat'] = 'Repetir';
+$labels['selectdate'] = 'Escolher data';
+$labels['freebusy'] = 'Mostrar-me como';
+$labels['free'] = 'Livre';
+$labels['busy'] = 'Ocupado';
+$labels['outofoffice'] = 'Ausente';
+$labels['tentative'] = 'Tentativa';
+$labels['mystatus'] = 'O meu estado';
+$labels['status'] = 'Estado';
+$labels['status-confirmed'] = 'Confirmado';
+$labels['status-cancelled'] = 'Cancelado';
+$labels['priority'] = 'Prioridade';
+$labels['sensitivity'] = 'Privacidade';
+$labels['public'] = 'público';
+$labels['private'] = 'privado';
+$labels['confidential'] = 'confidencial';
+$labels['links'] = 'Referência';
+$labels['alarms'] = 'Lembrete';
+$labels['comment'] = 'Comentário';
+$labels['created'] = 'Criado em';
+$labels['changed'] = 'Alterado em';
+$labels['unknown'] = 'Desconhecido';
+$labels['eventoptions'] = 'Opções';
+$labels['generated'] = 'produzido a';
+$labels['eventhistory'] = 'Histórico';
+$labels['removelink'] = 'Remover referência de email ';
+$labels['printdescriptions'] = 'Descrições de impressão';
+$labels['parentcalendar'] = 'Inserir dentro';
+$labels['searchearlierdates'] = '« Procurar eventos anteriores';
+$labels['searchlaterdates'] = 'Procurar eventos posteriores »';
+$labels['andnmore'] = '$nr mais...';
+$labels['togglerole'] = 'Clique para alternar o papel';
+$labels['createfrommail'] = 'Salvar como evento';
+$labels['importevents'] = 'Importar eventos';
+$labels['importrange'] = 'Eventos de';
+$labels['onemonthback'] = '1 mês atrás';
+$labels['nmonthsback'] = '$nr meses atrás';
+$labels['showurl'] = 'Mostrar URL do calendário';
+$labels['showurldescription'] = 'Use o seguinte endereço para obter acesso (somente leitura) ao seu calendário com outras aplicações. Para isso pode copiar e colar este endereço em qualquer software que suporte o formato iCal.';
+$labels['caldavurldescription'] = 'Para sincronizar este calendário com o seu computador ou dispositivos móveis deverá copiar este endereço <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> para a aplicação cliente. (ex. Evolution ou Mozilla Thunderbird)';
+$labels['findcalendars'] = 'Procurar calendários...';
+$labels['searchterms'] = 'Procurar termos';
+$labels['calsearchresults'] = 'Calendários disponíveis';
+$labels['calendarsubscribe'] = 'Listar sempre';
+$labels['nocalendarsfound'] = 'Não foram encontrados calendários';
+$labels['nrcalendarsfound'] = '$nr calendários encontrados';
+$labels['quickview'] = 'Só mostrar este calendário';
+$labels['invitationspending'] = 'Convites pendentes';
+$labels['invitationsdeclined'] = 'Convites recusados';
+$labels['changepartstat'] = 'Mudar estado de participante';
+$labels['rsvpcomment'] = 'Mensagem de convite';
+$labels['listrange'] = 'Intervalo para exibir:';
+$labels['listsections'] = 'Dividir em:';
+$labels['smartsections'] = 'Seções inteligentes';
+$labels['until'] = 'até';
+$labels['today'] = 'Hoje';
+$labels['tomorrow'] = 'Amanhã';
+$labels['thisweek'] = 'Esta semana';
+$labels['nextweek'] = 'Próxima semana';
+$labels['prevweek'] = 'Semana anterior';
+$labels['thismonth'] = 'Este mês';
+$labels['nextmonth'] = 'Próximo mês';
+$labels['weekofyear'] = 'Semana';
+$labels['pastevents'] = 'Passado';
+$labels['futureevents'] = 'Futuro';
+$labels['showalarms'] = 'Mostrar lembretes';
+$labels['defaultalarmtype'] = 'Configuração padrão de lembrete';
+$labels['defaultalarmoffset'] = 'Horário padrão de lembrete';
+$labels['attendee'] = 'Participante';
+$labels['role'] = 'Papel';
+$labels['availability'] = 'Disp.';
+$labels['confirmstate'] = 'Estado';
+$labels['addattendee'] = 'Adicionar participante';
+$labels['roleorganizer'] = 'Organizador';
+$labels['rolerequired'] = 'Obrigatório';
+$labels['roleoptional'] = 'Facultativo';
+$labels['rolechair'] = 'Responsável';
+$labels['rolenonparticipant'] = 'Ausente';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Grupo';
+$labels['cutyperesource'] = 'Recurso';
+$labels['cutyperoom'] = 'Sala';
+$labels['availfree'] = 'Disponível';
+$labels['availbusy'] = 'Ocupado';
+$labels['availunknown'] = 'Desconhecido';
+$labels['availtentative'] = 'Tentativa';
+$labels['availoutofoffice'] = 'Ausente';
+$labels['delegatedto'] = 'Delegado a:';
+$labels['delegatedfrom'] = 'Delegado de:';
+$labels['scheduletime'] = 'Procurar disponibilidade';
+$labels['sendinvitations'] = 'Enviar convites';
+$labels['sendnotifications'] = 'Avisar os participantes sobre as alterações';
+$labels['sendcancellation'] = 'Avisar os participantes sobre o cancelamento do evento';
+$labels['onlyworkinghours'] = 'Procurar disponibilidade dentro do meu horário de trabalho';
+$labels['reqallattendees'] = 'Necessário/todos os participantes';
+$labels['prevslot'] = 'Espaço anterior';
+$labels['nextslot'] = 'Próximo espaço';
+$labels['suggestedslot'] = 'Espaço sugerido';
+$labels['noslotfound'] = 'Incapaz de encontrar um horário disponível';
+$labels['invitationsubject'] = 'Foi convidado para "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nSegue em anexo um arquivo iCalendar com todos os detalhes de um evento, o qual pode importar para o seu calendário.";
+$labels['invitationattendlinks'] = "No caso do seu cliente de e-mail não suportar pedidos do tipo iTIP, pode usar o seguinte link para aceitar ou recusar este convite:\n\$url";
+$labels['eventupdatesubject'] = '"$title" foi atualizado.';
+$labels['eventupdatesubjectempty'] = 'Um evento do seu interesse foi atualizado';
+$labels['eventupdatemailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nSegue em anexo um arquivo iCalendar atualizado com os detalhes de um evento, o qual pode importar para o seu calendário.";
+$labels['eventcancelsubject'] = '"$title" foi cancelado.';
+$labels['eventcancelmailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nO evento foi cancelado por \$organizer.\n\nSegue em anexo um arquivo iCalendar com os detalhes atualizados do evento.";
+$labels['itipobjectnotfound'] = 'O evento citado nesta mensagem não foi encontrado no seu calendário.';
+$labels['itipmailbodyaccepted'] = "\$sender aceitou o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender aceitou o convite como \"tentativa\" para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender recusou o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender rejeitou a sua participação no seguinte evento:\n\n*\$title*\n\nQuando: \$date";
+$labels['itipmailbodydelegated'] = "\$sender delegou a participação no seguinte evento:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender delegou a sua participação no seguinte evento:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipdeclineevent'] = 'Deseja recusar o convite para este evento?';
+$labels['declinedeleteconfirm'] = 'Também deseja apagar o evento recusado do seu calendário?';
+$labels['itipcomment'] = 'Observações — Convite/notificação';
+$labels['itipcommenttitle'] = 'Estas observações serão enviadas com o convite/notificação aos participantes.';
+$labels['notanattendee'] = 'Não está listado como participante neste evento';
+$labels['eventcancelled'] = 'O evento foi cancelado';
+$labels['saveincalendar'] = 'guardar em';
+$labels['updatemycopy'] = 'Atualizar no meu calendário';
+$labels['savetocalendar'] = 'Guardar no calendário';
+$labels['openpreview'] = 'Verificar calendário';
+$labels['noearlierevents'] = 'Sem eventos anteriores';
+$labels['nolaterevents'] = 'Sem eventos posteriores';
+$labels['resource'] = 'Recurso';
+$labels['addresource'] = 'Livro de recursos';
+$labels['findresources'] = 'Encontrar recursos';
+$labels['resourcedetails'] = 'Detalhes';
+$labels['resourceavailability'] = 'Disponibilidade';
+$labels['resourceowner'] = 'Dono';
+$labels['resourceadded'] = 'O recurso foi adicionado ao seu evento';
+$labels['tabsummary'] = 'Sumário';
+$labels['tabrecurrence'] = 'Repetição';
+$labels['tabattendees'] = 'Participantes';
+$labels['tabresources'] = 'Recursos';
+$labels['tabattachments'] = 'Anexos';
+$labels['tabsharing'] = 'Partilha';
+$labels['deleteobjectconfirm'] = 'Tem a certeza que quer eliminar este evento?';
+$labels['deleteventconfirm'] = 'Tem a certeza que quer eliminar este evento?';
+$labels['deletecalendarconfirm'] = 'Tem a certeza que quer eliminar este calendário e todos os seus eventos?';
+$labels['deletecalendarconfirmrecursive'] = 'Tem a certeza que quer eliminar este calendário com todos os seus eventos e sub-calendários?';
+$labels['savingdata'] = 'A guardar os dados...';
+$labels['errorsaving'] = 'Falha ao guardar as alterações.';
+$labels['operationfailed'] = 'A operação pedida falhou.';
+$labels['invalideventdates'] = 'As datas são inválidas! Por favor, verifique novamente.';
+$labels['invalidcalendarproperties'] = 'Propriedades de calendário inválidas! Por favor defina um nome válido.';
+$labels['searchnoresults'] = 'Nenhum evento encontrado nos calendários selecionados.';
+$labels['successremoval'] = 'O evento foi excluído com sucesso.';
+$labels['successrestore'] = 'O evento foi restaurado com sucesso.';
+$labels['errornotifying'] = 'Falha ao enviar notificações para os participantes do evento.';
+$labels['errorimportingevent'] = 'Falha ao importar evento';
+$labels['importwarningexists'] = 'Uma cópia deste evento já existe em seu calendário.';
+$labels['newerversionexists'] = 'Já existe uma nova versão deste evento! Abortado.';
+$labels['nowritecalendarfound'] = 'Nenhum calendário encontrado para salvar o evento';
+$labels['importedsuccessfully'] = 'O evento foi adicionado com sucesso em \'$calendar\'';
+$labels['updatedsuccessfully'] = 'O evento foi atualizado com sucesso em \'$calendar\'.';
+$labels['attendeupdateesuccess'] = 'O status do participante foi atualizado com sucesso.';
+$labels['itipsendsuccess'] = 'Convite enviado aos participantes.';
+$labels['itipresponseerror'] = 'Falha ao enviar a resposta para este convite de evento';
+$labels['itipinvalidrequest'] = 'Este convite já não é válido';
+$labels['sentresponseto'] = 'Resposta de convite enviada com sucesso para $mailto';
+$labels['localchangeswarning'] = 'As alterações que pretende efetuar só serão válidas no seu calendário e não serão enviadas ao organizador do evento.';
+$labels['importsuccess'] = 'Importado com sucesso $nr eventos';
+$labels['importnone'] = 'Não há eventos a serem importados';
+$labels['importerror'] = 'Ocorreu um erro na importação';
+$labels['aclnorights'] = 'Não tem permissão de administrador neste calendário.';
+$labels['changeeventconfirm'] = 'Alterar evento';
+$labels['removeeventconfirm'] = 'Eliminar evento';
+$labels['changerecurringeventwarning'] = 'Este evento é recorrente. Deseja alterar a ocorrência atual, esta e todas as futuras ocorrências ou guardar como um novo evento?';
+$labels['removerecurringeventwarning'] = 'Este evento é recorrente. Deseja eliminar a ocorrência atual, esta e todas as futuras ocorrências ou todas as ocorrências do evento?';
+$labels['removerecurringallonly'] = 'Este evento é recorrente. Como participante, só pode apagar apagar o evento com todas as ocorrências.';
+$labels['currentevent'] = 'Atual';
+$labels['futurevents'] = 'Futuro';
+$labels['allevents'] = 'Todos';
+$labels['saveasnew'] = 'Guardar como';
+$labels['birthdays'] = 'Aniversários';
+$labels['birthdayscalendar'] = 'Calendário de aniversários';
+$labels['displaybirthdayscalendar'] = 'Mostrar calendário de aniversários';
+$labels['birthdayscalendarsources'] = 'From these address books';
+$labels['birthdayeventtitle'] = 'Aniversário de $name';
+$labels['birthdayage'] = 'Idade $age';
+$labels['objectchangelog'] = 'Alterar histórico';
+$labels['revision'] = 'Revisão';
+$labels['user'] = 'Utilizador';
+$labels['operation'] = 'Ação';
+$labels['actionappend'] = 'Guardado';
+$labels['actionmove'] = 'Movido';
+$labels['actiondelete'] = 'Eliminado';
+$labels['compare'] = 'Comparar';
+$labels['showrevision'] = 'Mostrar esta versão';
+$labels['restore'] = 'Restaurar esta versão';
+$labels['objectnotfound'] = 'Falha ao ler os dados do evento';
+$labels['objectchangelognotavailable'] = 'Não é possível alterar o histórico deste evento';
+$labels['objectdiffnotavailable'] = 'Não é possível comparar as revisões selecionadas';
+$labels['revisionrestoreconfirm'] = 'Confirma o restauro da revisão $rev deste evento? Os dados atuais serão substituídos pelos da versão anterior.';
+$labels['arialabelminical'] = 'Seleção da data do calendário';
+$labels['arialabelcalendarview'] = 'Vista do calendário';
+$labels['arialabelsearchform'] = 'Quadro de pesquisa de eventos';
+$labels['arialabelquicksearchbox'] = 'Pesquisa de eventos';
+$labels['arialabelcalsearchform'] = 'Quadro de pesquisa de calendários';
+$labels['calendaractions'] = 'Ações do calendário';
+$labels['arialabeleventattendees'] = 'Lista de participantes do evento';
+$labels['arialabeleventresources'] = 'Lista de recursos para eventos';
+$labels['arialabelresourcesearchform'] = 'Quadro de pesquisa de recursos';
+$labels['arialabelresourceselection'] = 'Recursos disponíveis';
+?>
diff --git a/plugins/calendar/localization/ru_RU.inc b/plugins/calendar/localization/ru_RU.inc
index a7b0918..b5c3c1d 100644
--- a/plugins/calendar/localization/ru_RU.inc
+++ b/plugins/calendar/localization/ru_RU.inc
@@ -247,7 +247,8 @@ $labels['displaybirthdayscalendar'] = 'Показывать календарь 
 $labels['birthdayscalendarsources'] = 'Из этих адресных книг';
 $labels['birthdayeventtitle'] = 'День рождения $name';
 $labels['birthdayage'] = 'Возраст $age';
-$labels['eventchangelog'] = 'История изменений';
+$labels['objectchangelog'] = 'История изменений';
+$labels['objectdiff'] = 'Изменения с $rev1 до $rev2';
 $labels['revision'] = 'Ревизия';
 $labels['user'] = 'Пользователь';
 $labels['operation'] = 'Действие';
@@ -257,10 +258,12 @@ $labels['actiondelete'] = 'Удалено';
 $labels['compare'] = 'Сравнить';
 $labels['showrevision'] = 'Показать эту версию';
 $labels['restore'] = 'Восстановить эту версию';
-$labels['eventnotfound'] = 'Не удалось загрузить информацию о мероприятиях';
+$labels['objectnotfound'] = 'Не удалось загрузить информацию о мероприятиях';
 $labels['objectchangelognotavailable'] = 'История изменений для этого события недоступна';
-$labels['eventdiffnotavailable'] = 'Невозможно провести сравнение выбранных ревизий ';
+$labels['objectdiffnotavailable'] = 'Невозможно провести сравнение выбранных ревизий ';
 $labels['revisionrestoreconfirm'] = 'Вы уверенны, что хотите восстановить это событие из ревизии $rev? Оно заменит текущее событие старой версией. ';
+$labels['objectrestoresuccess'] = 'Ревизия $rev успешно восстановлена';
+$labels['objectrestoreerror'] = 'Не удалось восстановить старую ревизию';
 $labels['arialabelminical'] = 'Выбор даты';
 $labels['arialabelcalendarview'] = 'Вид календаря';
 $labels['arialabelsearchform'] = 'Форма поиска событий';
diff --git a/plugins/calendar/localization/sl_SI.inc b/plugins/calendar/localization/sl_SI.inc
index 9ab874b..0691d42 100644
--- a/plugins/calendar/localization/sl_SI.inc
+++ b/plugins/calendar/localization/sl_SI.inc
@@ -246,7 +246,7 @@ $labels['displaybirthdayscalendar'] = 'Prikaži koledar rojstnih dnevov';
 $labels['birthdayscalendarsources'] = 'Iz teh imenikov';
 $labels['birthdayeventtitle'] = 'Rojstni dan osebe $name';
 $labels['birthdayage'] = 'Starost $age';
-$labels['eventchangelog'] = 'Spremeni Zgodovino';
+$labels['objectchangelog'] = 'Spremeni Zgodovino';
 $labels['revision'] = 'Verzija';
 $labels['user'] = 'Uporabnik';
 $labels['operation'] = 'Dejanje';
@@ -256,9 +256,9 @@ $labels['actiondelete'] = 'Izbrisano';
 $labels['compare'] = 'Primerjaj';
 $labels['showrevision'] = 'Prikaži to verzijo';
 $labels['restore'] = 'Obnovi to verzijo';
-$labels['eventnotfound'] = 'Napaka pri nalaganju podatkov o dogodku';
+$labels['objectnotfound'] = 'Napaka pri nalaganju podatkov o dogodku';
 $labels['objectchangelognotavailable'] = 'Sprememba zgodovine za ta dogodek ni na voljo';
-$labels['eventdiffnotavailable'] = 'Primerjava za izbrane verzije ni na voljo';
+$labels['objectdiffnotavailable'] = 'Primerjava za izbrane verzije ni na voljo';
 $labels['revisionrestoreconfirm'] = 'Ali želite obnoviti verzijo $rev tega dogodka? To bo nadomestilo trenutni dogodek s starejšo verzijo.';
 $labels['arialabelminical'] = 'Izbira datuma v koledarju';
 $labels['arialabelcalendarview'] = 'Prikaz koledarja';
diff --git a/plugins/calendar/localization/sv_SE.inc b/plugins/calendar/localization/sv_SE.inc
index 94e7213..edf1db6 100644
--- a/plugins/calendar/localization/sv_SE.inc
+++ b/plugins/calendar/localization/sv_SE.inc
@@ -246,7 +246,7 @@ $labels['displaybirthdayscalendar'] = 'Visa födelsedagskalender';
 $labels['birthdayscalendarsources'] = 'Från dessa adressböcker';
 $labels['birthdayeventtitle'] = '$name\s födelsedag';
 $labels['birthdayage'] = '$age år';
-$labels['eventchangelog'] = 'Ändringshistorik';
+$labels['objectchangelog'] = 'Ändringshistorik';
 $labels['revision'] = 'Revision';
 $labels['user'] = 'Användare';
 $labels['operation'] = 'Åtgärd';
@@ -256,9 +256,9 @@ $labels['actiondelete'] = 'Borttagen';
 $labels['compare'] = 'Jämför';
 $labels['showrevision'] = 'Visa denna version';
 $labels['restore'] = 'Återställ denna verson';
-$labels['eventnotfound'] = 'Det gick inte att läsa in data för händelsen';
+$labels['objectnotfound'] = 'Det gick inte att läsa in data för händelsen';
 $labels['objectchangelognotavailable'] = 'Ändringshistorik är inte tillgänglig för denna händelse';
-$labels['eventdiffnotavailable'] = 'Ingen jämförelse möjlig för valda revisioner';
+$labels['objectdiffnotavailable'] = 'Ingen jämförelse möjlig för valda revisioner';
 $labels['revisionrestoreconfirm'] = 'Vill du verkligen återställa revision $rev för denna händelse? Det kommer att ersätta den aktuella händelsen med den gamla versionen.';
 $labels['arialabelminical'] = 'Kalender datumurval';
 $labels['arialabelcalendarview'] = 'Kalender vy';
diff --git a/plugins/calendar/localization/th_TH.inc b/plugins/calendar/localization/th_TH.inc
index f157543..c9933fc 100644
--- a/plugins/calendar/localization/th_TH.inc
+++ b/plugins/calendar/localization/th_TH.inc
@@ -233,7 +233,7 @@ $labels['displaybirthdayscalendar'] = 'แสดงปฎิทินวัน
 $labels['birthdayscalendarsources'] = 'จากสมุดที่อยู่เหล่านี้';
 $labels['birthdayeventtitle'] = 'วันเกิดของ $name';
 $labels['birthdayage'] = 'อายุ $age ปี';
-$labels['eventchangelog'] = 'ประวัติการปรับเปลี่ยน';
+$labels['objectchangelog'] = 'ประวัติการปรับเปลี่ยน';
 $labels['revision'] = 'รุ่นการปรับปรุง';
 $labels['user'] = 'ผู้ใช้';
 $labels['operation'] = 'การกระทำ';
@@ -243,9 +243,9 @@ $labels['actiondelete'] = 'ลบเรียบร้อย';
 $labels['compare'] = 'เปรียบเทียบ';
 $labels['showrevision'] = 'แสดงรุ่นการปรับปรุงนี้';
 $labels['restore'] = 'ย้อนกลับไปยังรุ่นการปรับปรุงนี้';
-$labels['eventnotfound'] = 'การโหลดข้อมูลของเหตุการณ์ล้มเหลว';
+$labels['objectnotfound'] = 'การโหลดข้อมูลของเหตุการณ์ล้มเหลว';
 $labels['objectchangelognotavailable'] = 'ไม่มีประวัติการปรับเปลี่ยนสำหรับเหตุการณ์นี้';
-$labels['eventdiffnotavailable'] = 'ไม่สามารถเปรียบเทียบรุ่นการปรับปรุงที่เลือกได้';
+$labels['objectdiffnotavailable'] = 'ไม่สามารถเปรียบเทียบรุ่นการปรับปรุงที่เลือกได้';
 $labels['revisionrestoreconfirm'] = 'คุณต้องการกู้คืนรุ่นการปรับปรุง $rev ของเหตุการณ์นี้หรือ  นี่จะเป็นการเขียนทับข้อมูลปัจจุบันด้วยข้อมูลที่เก่ากว่า';
 $labels['arialabelcalendarview'] = 'มุมมองปฎิทิน';
 $labels['arialabelquicksearchbox'] = 'ป้อนข้อมูลเพื่อค้นหาเหตุการณ์';
diff --git a/plugins/calendar/localization/uk_UA.inc b/plugins/calendar/localization/uk_UA.inc
index aac583a..f430667 100644
--- a/plugins/calendar/localization/uk_UA.inc
+++ b/plugins/calendar/localization/uk_UA.inc
@@ -205,7 +205,7 @@ $labels['displaybirthdayscalendar'] = 'Показувати календар Д
 $labels['birthdayscalendarsources'] = 'З даних адресних книг';
 $labels['birthdayeventtitle'] = 'День Народження $name';
 $labels['birthdayage'] = 'Вік $age';
-$labels['eventchangelog'] = 'Історія змін';
+$labels['objectchangelog'] = 'Історія змін';
 $labels['revision'] = 'Ревізія';
 $labels['user'] = 'Користувач';
 $labels['operation'] = 'Дія';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index b0a8660..0669a8f 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -837,14 +837,14 @@ a.miniColors-trigger {
 	text-decoration: none;
 }
 
-#eventhistory .loading {
+.changelog-table .loading {
 	color: #666;
 	margin: 1em 0;
 	padding: 1px 0 2px 24px;
 	background: url(images/loading_blue.gif) top left no-repeat;
 }
 
-#eventhistory .compare-button {
+.changelog-dialog .compare-button {
 	margin: 4px 0;
 }
 
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index b9ea905..8d8e010 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -335,7 +335,7 @@
 </div>
 
 <div id="eventhistory" class="uidialog" aria-hidden="true">
-    <roundcube:object name="plugin.event_changelog_table" id="event-changelog-table" class="records-table changelog-table" />
+    <roundcube:object name="plugin.object_changelog_table" id="event-changelog-table" class="records-table changelog-table" domain="calendar" />
     <div class="compare-button"><input type="button" class="button" value="↳ <roundcube:label name='calendar.compare' />" /></div>
 </div>
 
diff --git a/plugins/libkolab/js/audittrail.js b/plugins/libkolab/js/audittrail.js
index 5c9eec0..0fe7bcb 100644
--- a/plugins/libkolab/js/audittrail.js
+++ b/plugins/libkolab/js/audittrail.js
@@ -44,7 +44,7 @@ libkolab_audittrail.object_history_dialog = function(p)
         $dialog.dialog('close');
 
     var buttons = {};
-    buttons[rcmail.gettext('close', 'calendar')] = function() {
+    buttons[rcmail.gettext('close')] = function() {
         $dialog.dialog('close');
     };
 
@@ -189,7 +189,7 @@ libkolab_audittrail.render_changelog = function(data, object, folder)
             .append('<td class="revision">' + Q(i+1) + '</td>')
             .append('<td class="date">' + Q(change.date || '') + '</td>')
             .append('<td class="user">' + Q(change.user || 'undisclosed') + '</td>')
-            .append('<td class="operation" title="' + op_append + '">' + Q(rcmail.gettext(op_labels[change.op] || '', 'calendar') + (op_append ? ' ...' : '')) + '</td>')
+            .append('<td class="operation" title="' + op_append + '">' + Q(rcmail.gettext(op_labels[change.op] || '', data.module) + (op_append ? ' ...' : '')) + '</td>')
             .append('<td class="actions">' + (accessible && change.op != 'DELETE' ? actions.replace(/\{rev\}/g, change.rev) : '') + '</td>')
             .appendTo(tbody);
     }
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index f1c2c87..4aa1ce5 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -130,6 +130,24 @@ class libkolab extends rcube_plugin
     }
 
     /**
+     * Table oultine for object changelog display
+     */
+    public static function object_changelog_table($attrib = array())
+    {
+        $rcube = rcube::get_instance();
+
+        $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
+        $table->add_header('diff',      '');
+        $table->add_header('revision',  $rcube->gettext('revision', $attrib['domain']));
+        $table->add_header('date',      $rcube->gettext('date', $attrib['domain']));
+        $table->add_header('user',      $rcube->gettext('user', $attrib['domain']));
+        $table->add_header('operation', $rcube->gettext('operation', $attrib['domain']));
+        $table->add_header('actions',   ' ');
+
+        return $table->show($attrib);
+    }
+
+    /**
      * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
      */
     public static function html_diff($from, $to)




More information about the commits mailing list