Grunt & WordPress development IV: Another task for internationalisation


This is part 4 in a series looking at using Grunt in WordPress plug-in/theme development.

  1. Grunt & WordPress development
  2. Grunt & WordPress development II: Installing Grunt
  3. Grunt & WordPress development III: Tasks for internationalisation
  4. Grunt & WordPress development IV: Another task for internationalisation

WordPress has recently (since 3.5) seen a shift towards a more JavaScript codebase. This shift is still minor (it currently accounts for less than 15%, according to its GitHub repository, in 3.9). But the introduction of Backbone.js and the re-factoring of particular the editor in the WordPress admin (media manager, shortcodes “objects” etc.) are testament to Matt Mullenweg’s comment:

I forgot to mention our biggest architectural change, which is already ongoing: an ever-increasing percentage of our codebase is shifting to Javascript instead of PHP. At some point I expect it to be the vast majority.

Matt Mullenweg,

However, a JavaScript codebase presents difficulties for WordPress in terms of internationalisation. I ran into this while working on Event Organiser Pro’s 1.7 release. This new release saw the booking form customiser rewritten to use Backbone. With so much JavaScript replacing PHP, there became a massive need to allow strings to be translatable (in a sane way).

Received wisdom is that you should use wp_localize_script() to allow strings in javascript files to be translated. Pippin covers the method excellently here but essentially when you enqueue your script you use wp_localize_script() to make a variable available which contains all you translated strings.

function my_load_scripts() {
    wp_enqueue_script( 'my-script', plugin_dir_url( __FILE__ ) . 'my-script.js');
    wp_localize_script('my-script', 'mynamespace', array(
         'helloworld' => __('Hello World', 'mydomain' )
add_action('wp_enqueue_scripts', 'my_load_scripts');

Then in your JavaScript file, instead of the string “Hello World” you would use mynamespace.helloworld. Although this method is common, there are a couple of things wrong with it when you pit it against some sort of gettext function:

It makes code harder to read. For me when reading code it’s much easier to see the actual text rather than a variable. (More so when reading other people’s code as this helps you link the sourcecode with what you actually see)

  alert( mynamespace.welcome_msg ); //ok
  alert( mynamespace.gettext( "Welcome to..." ) ); //better

At the very best its slightly more cryptic and uglier.

It makes it harder to maintain code. – When editing a JavaScript file, a gettext function allows me to edit the string there and then. If I use wp_localize_script(), I need to track down the .php file responsible for that and change it there – and then not forget that that string might have been used elsewhere.

It makes the translator’s job harder. – Hands up anyone who is very poor at providing translators with comments or providing a context when appropriate. Me at least. Regardless, .po files provide a line number so that, if necessary, translators can look up that line to get some sort of context for the string they are translating. It’s not very helpful when that line number points them to a large array in some obscure php file, rather than where the text is being used. Nor is it immediately obvious which JavaScript file(s) are using the string.

You may think that my reasons here are weak and pinnikity… and you might right… But I prefer using gettext-esque function for translating strings.

The problem(s)… (and how Grunt helps solve them)

There a couple of problems with using trying to use a gettext function in a JavaScript file, but they are all easily solved:

  1. There is no native gettext function
  2. How do you get the translations from .po to your JavaScript file
  3. How to get translatable strings from your JavaScript file to your .pot

There is no native gettext function in JavaScript

A very simple solution is to roll your own. Below are four functions which handle translatable strings, plurals and contexts. They all expect the translated strings to be found in mynamespace.locale.

mynamespace.gettext = function( msgid ){
    if( this.locale[msgid] !== undefined ){
        return this.locale[msgid];
    return msgid;

mynamespace.ngettext = function( msgid1, msgid2, n ){
    var key = ( n > 1 ? msgid1 + '_plural' : msgid1 );
    if( this.locale[key] !== undefined ){
        return this.locale[key];
    return ( n > 1 ? msgid2 : msgid1 );

mynamespace.pgettext = function( ctxt, msgid ){
    if( this.locale[msgid+'_'+ctxt] !== undefined ){
        return this.locale[msgid+'_'+ctxt];
    return msgid;

mynamespace.npgettext = function( ctxt, msgid1, msgid2, n ){
    var key = ( n > 1 ? msgid1 + '_' + ctxt + '_plural' : + '_' + ctxt + '_' + msgid1 );
    if( this.locale[key] !== undefined ){
        return this.locale[key];
    return ( n > 1 ? msgid2 : msgid1 );

You may have noticed that only one plural form is supported (so a string is either plural or singular), but some languages use more (and some less). There are ways around this, but the limitation is also a result of the Grunt task that’ll we’ll use later. Plural strings and strings with a context expect _plural and _{context} modifiers – I personally think this is less than ideal, but again is forced upon me by my choice of Grunt task. (This is just a start, and I’d like to see these limitations lifted).

Getting translations from .po to your JavaScript file

This is a two-step process:

  1. Use a Grunt task to generate a .json file for each .mo file
  2. Depending on the user’s choice of locale, load that .json file and use wp_localize_script() to make it available in you JavaScipt file.

I went with grunt-i18next-conv to generate the .json files. I found that converting .po to .json included untranslated strings, so I recommend you opt for converting .mo files to .json. (If you need a Grunt task for generating your .mo task, I recommend po2mo task I covered in my last article). It’s this task, by way of the format of the .json file it produces, that imposes some of the limitations already mentioned.

Next you, when enqueuing your JavaScript file, you use wp_localize_script() to ‘attach’ the relevant .json file to it. In the following I expect that the .json files are of the form /languages/mytextdomain-{locale}.json

 $locale = array();
 $file = plugin_dir_path( __FILE__ ) . 'languages/mytextdomain-'.get_locale().'.json';
 if( file_exists( file ) ){
      $locale = json_decode( file_get_contents( $file ), true );    

 wp_localize_script('my-script', 'mynamespace', array(
      'locale' => $locale

Getting the translatable strings from your JavaScript file to .pot

If you’re using grunt-pot this is easy. Simply include the functions above in the ‘keywords’ option:

  keywords: [ 

and ensure the files to search include your JavaScript file.


As discussed above there are currently two limitations:

  1. Poor support for plurals other than ‘single form plurals’
  2. Awkward ‘.json’ structure (not a massive issue…)

For the time being, however, and for use in Event Organiser’s booking form customiser, this method was ideal.

Grunt & WordPress development III: Tasks for internationalisation


This is part 3 in a series looking at using Grunt in WordPress plug-in/theme development.

  1. Grunt & WordPress development
  2. Grunt & WordPress development II: Installing Grunt
  3. Grunt & WordPress development III: Tasks for internationalisation
  4. Grunt & WordPress development IV: Another task for internationalisation

Internationalisation Tasks

One aspect of WordPress plug-in development that involves a lot of mundane work is that of internalisation: ensuring WordPress’ localisation functions are used correctly, generating a .pot file, compiling submitted .po files to .mo files. The latter two you can do with Poedit – but this still involves manually opening the .po/.pot file. These tasks can be completely automated so let’s do that:

po2mo – Compiling to .po files to .mo

The po2mo plug-in automatically compiles given .po files and produces a .mo file of the same name.

To install:

npm install grunt-po2mo --save-dev

<em>Please not an earlier version of this article executed the above as a super user (`sudo npm`). As pointed out by Lacy in the comments, this necessary and can cause permission issues with the npm cache.</em>

The following set up looks in the languages directory for any .po files and compiles them, creating the corresponding .mo in the same directory:

po2mo: {
    files: {
        src: 'languages/*.po',
        expand: true,

Finally load the task by adding grunt.loadNpmTasks('grunt-po2mo'); at the bottom of your Gruntfile.js, just after grunt.loadTasks('tasks');. Then whenever you add or change a .po file:

grunt po2mo

(You can see a live example of this task, and the others listed below, here.

pot – Create a .pot template file

For users to be able to translate your plug-in you’ll need to create a .po template file ( a .pot file). The pot plug-in does exactly that.

You just need to provide it with:

  • The files to search in,
  • The keywords to search for (and indicate which arguments are translatable strings, and which are context specifiers)
  • A text domain (used only for naming the the .pot file)
  • The directory where you wish to output the .pot file.

To install:

npm install grunt-pot --save-dev


pot: {
          text_domain: 'my-plugin', //Your text domain. Produces my-text-domain.pot
          dest: 'languages/', //directory to place the pot file
          keywords: [ //WordPress localisation functions
          src:  [ '**/*.php' ], //Parse all php files
          expand: true,

Finally load the task by adding grunt.loadNpmTasks('grunt-pot'); to the bottom. Then to generate your .pot file:

grunt pot

checktextdomain – Verify localisation functions have been used correctly

Having generated a .pot file, gathered translations for your plug-in and then compiled them – it would be entirely wasted if you haven’t used the WordPress localisations functions properly. In particular, if you had failed to specify the correct domain, your efforts would have been wasted.

When coding it’s easy to forget to specify a text domain, or to mistype it. Or perhaps you’ve been using a variable for the domain, and now want to switch to a literal string.

The checktextdomain – not only checks if you’ve used the correct textdomain in the localisation function it can also correct it for you.

Simply provide it with:

  • Files to look in,
  • Keywords to look for (important: you must provide a domain argument specifier)
  • A text-domain to check against
  • Whether you want mistakes corrected (it will not add missing domains… yet).

The plug-in will then

  • Warn you if some keywords have been used without a text domain
  • Warn you if some keywords have been used with an incorrect text domain (optionally correct it for you)
  • Warn you if some keywords have been used with a variable text domain (optionally correct it for you)

There are various options for this plug-in to enable you to check (and correct) the things you want to. You can see all the available options for this Grunt plug-in on its Github page.

To install:

npm install grunt-checktextdomain --save-dev

You’ll notice that the keywords option is very similar to grunt-pot. There is an important distinction. For this plug-in to work you must extend the keyword specifier and indicate where the domain should be.

E.g. 2d indicates that the domain should be passed as the second argument of the localisation function

checktextdomain: {
      text_domain: 'my-plugin',
      correct_domain: true, //Will correct missing/variable domains
      keywords: [ //WordPress localisation functions
   files: {
       src:  [ '**/*.php', ], //All php files
       expand: true,

Finally load the task by adding grunt.loadNpmTasks('grunt-checktextdomain'); to the bottom. Then to check your files:

grunt checktextdomain

I’m planning on improving this further to warn you of missing contexts which using functions that expect one.

Final remarks

Remembering to add grunt.loadNpmTasks(...); at the bottom of your Gruntfile.js, just after grunt.loadTasks('tasks'); is easily forgotten. But there’s a way around this which I’ll discuss in my next post.

Just before publishing Brady Vercher announced his Grunt plug-in, which allows you to utilize the internationalisation tools that WordPress uses. There’s a bit more set-up involved, but a notable advantage over grunt-pot is that it recognises theme template headers as translatable.

Grunt & WordPress development


This is the first post of a five part series looking at WordPress plug-in/theme development with Grunt.

  1. Grunt & WordPress development
  2. Grunt & WordPress development II: Installing Grunt
  3. Grunt & WordPress development III: Tasks for internationalisation
  4. Grunt & WordPress development IV: Another task for internationalisation

Back in August the WordPress core team announced they were going to use Grunt in WordPress’ development. This in my view is a major stride forward for WordPress (more so than the much celebrated ‘features as plug-ins’ – which itself marked an improvement to WordPress’ development cycle).

This series of articles however, will be focussed on using Grunt for WordPress plug-ins and themes (which are fundamentally the same thing), and the tasks I use in development. In this first post I’ll discuss the what and the whys:

What is Grunt?

Grunt is a javascript based task runner from Ben Alman. It performs repetitive task such as compression, unit testing, linting, concatenation, preprocessing etc. Almost any task in the development, building or deployment of your WordPress plug-in which can be automated can be performed by Grunt – freeing you from those tedious, and potentially human-error-prone routines.

(Once Grunt is installed) there are two files which set up Grunt for use in your project:

  • package.json – which details your project (in this case: a WordPress plug-in) and it’s dependencies (in this context, Grunt and any Grunt plug-ins you want to use).
  • Gruntfile.js – listing the tasks you wish to perform and their configuration

Those tasks can be executed simply by running

 grunt [task name] 

in your command line. You’ll probably have multiple tasks that you’d want to run one after than other (e.g. one task to copy/generate files from your development environment to a build directory, and another to upload that directory to an Amazon S3 server). Instead of calling each manually Grunt allows you to create tasks which simply call a collection of other tasks. For example

 grunt test

might be configured to trigger unit-test and linting tasks.

Why Grunt?

The idea of automating deployments, unit testing, compressing images, scripts & stylesheets and other tasks you may wish to perform in your plug-in’s development, build and release cycle is certainly not unique to Grunt. Before I switched to Grunt I had a home-grown Makefile to perform a lot of my routine tasks.

Grunt however, brings this all under one roof: giving a familiar command line procedure to execute task(s). Importantly it allows (Grunt) plug-ins, and their end-users to add structure to their tasks. By this I mean tasks being able to call other tasks, being able to initiate an entire list of tasks, being able to configure all your tasks in only one file and easy of portability. In fact, if you download a development repository for a plug-in which includes the package.json and Gruntfile.js files, in one command you can install all the Grunt plug-ins it requires for use in testing, building and deploying that plug-in. (This assumes you have Node.js installed, which I’ll cover in part two).

Grunt doesn’t offer much new – but it does offer a much better solution.

It’s also popular – and popularity is key for any library, platform or tool to succeed and to grow. Popularity brings greater number of developers, they drive the growth of plug-ins & features and the increased functionality drives popularity. (Perhaps not dissimilar to WordPress’ own growth). Grunt’s ecosystem is already very substantial: there’s phpunit for PHP unit testing, jshint for Javascript linting, there is uglifyjs for compressing javascript files and imagemin to optimise images.

The point is: it has a large, and growing ecosystem. In the majority cases, for any task you might want to perform, there will exist a grunt plug-in to perform it.

And if there isn’t? Grunt is incredibly well documented, open-source and easy to dive into. If you find a gap in its armoury, the chances are it’s easy enough to fill.

What’s next?

If you weren’t already sold on Grunt, hopefully that will do it. The next post will be on installing Grunt and executing your first task.

Die query_posts()! Die!

For some it’s well known that query_posts() should never be used, but I still see it dotted around here and there, so this is my attempt to help kill it off.

Every time you use query_posts() a kitten does

Every time you use query_posts() a kitten dies. Photo by Brian Scott.

I’m going to keep this post as non-technical as I can. The ins and outs of query_posts() is best left to this video by Andrew Nacin:

Instead, I’m going to talk about what you should be using instead of query_posts() and some of the nuances of pre_get_posts that could land you in hot water. But first…

Why Is query_posts() So Bad?

Mainly, for two reasons:

Query Then Template

It’s bad because runs against WordPress’ handling of website url to page load. query_posts() is directly linked with the main query – this isn’t a handy-wavy notion – it’s a real (global) object: $wp_query. This query is formed from the url of the current page and determines what WordPress displays. But importantly WordPress performs the query first and then chooses the appropriate template.

However, when you use query_posts() in a template file you doing the opposite – you’re trying set the ‘main query’ based on the template used. This can lead to pagination issues and 404s.

It’s Linked to the Main Query

Secondly, the main query is what WordPress thinks is the main content of what it’s displaying. When you change that via query_posts() to display a list of related posts, for example, you end up changing what WordPress thinks it’s displaying. You may end up displaying the comments for the last post on that ‘related post’ section and not the page being viewed.

What’s the solution?

Nice and easy:

1. Secondary queries

For displaying posts in the sidebar / beneath content – or just generally performing ‘secondary queries’ – that is queries that does not related to the main content of the page:

  global $post;
  $posts = get_posts( array( 'numberposts' => 5, 'category' => 3 ) );
  if( $posts ):
     foreach( $posts as $post ) :   
        setup_postdata($post); ?>

          <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>

     <?php endforeach; 
   endif; ?>


  $my_query = new WP_Query( array( 'numberposts' => 5, 'category' => 3 ) );
  if( $my_query->have_posts() ):
     while( $my_query->have_posts() ):
        $my_query->the_post(); ?>

          <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>

     <?php endwhile; 
   endif; ?>

both work fine.

Note the wp_reset_postdata(); – this is important so WordPress doesn’t confuse the last post in your secondary loop for the post associated with the current page!

2. Altering the Main Query

If you wish to alter the main query, for instance to exclude a category from the search page, then the action pre_get_posts (yes it’s an action not a filter).

function my_alter_query( $query ){

      //Is $query the main query? And is the main query for a search
      if( $query->is_main_query() && is_search() ){
        //Do something to main query

        //Exclude category with ID 12.
        $query->set( 'cat', '-12' );

That snippet should live in a plug-in, though it would also work in your theme’s functions.php.

But before you start tinkering with queries via pre_get_posts, take note of the health warnings…

pre_get_posts is triggered for every query

This includes admin and non-admin queries, for posts, navigation menu items (yes they’re post types), related post widgets. The lot. That is why if you only mean to alter the main query you check the conditional $query->is_main_query().

But this makes it incredibly powerful. Now I can not just alter the main query, but I can alter any query for a particular post type:

function my_alter_query( $query ){

      $pt = $query->get( 'post_type' );

      if( 'event' == $pt || ( is_array( $pt ) && array( 'event' == $pt )  ){                  
         //Do something to queries which are for events only

      if( 'event' == $pt || ( is_array( $pt ) && in_array( 'event', $pt )  ){                  
         //Do something to queries which include events

      if( $query->is_tax( 'event-category' )  ){                  
         //Is the query for an event category


Take away point: pre_get_posts is not just for the ‘main query’

Functions vs Method

You’ll note that in the above example I used the method $query->is_main_query() and the function is_search(). What’s the difference? Quite a lot:

Functions = main query, Methods = current query

Conditional functions such as is_search() and is_tax() are wrappers for

 global $wp_query; 

That is they apply to the ‘main query’. But since $query passed in pre_get_posts is not the always the main query, using a methods or functions are – in general – not the same thing. You’ll need to think about which you need to use: should the conditional apply to the passed $query object, or the main query object?

Lastly, Tom McFarlin has recently written a couple of articles on query_posts() and pre_get_posts. I recommend you check them out:

And also Rarst’s epic answer on WPSE: When should you use WP_Query vs query_posts() vs get_posts()?

Event Organiser – earn 25% commission

Yesterday I announced the launch of Event Organiser’s Affiliates Program, which rewards with 25% of the checkout price of every successful referral.

Event Organiser Pro comes in three licenses: Personal (£40), Business (£80) and Developer (£120), meaning you could earn up to £30 for one sale.

… but that’s not all, because there’s a lot more add-ons that a due to be rolled out which you could be earning money on:

  • ICAL sync – Subscribe your site to a feed, and automatically import the events
  • Front-end submission – Allows users to submit events from the front-end.
  • Discount Codes – (requires Pro) – Add discount codes for event bookings
  • Mailchimp – (requires Pro) – Subscribe attendees to a MailChimp mailing list.

Some of these will be included free for with the Pro Developer license, but all will be available to purchase separately.

ICAL Sync: Automatically Importing Feeds

I’ve just finished writing up an add-on for Event Organiser which allows you to automatically import events from an ICAL feed.


The standard version of Event Organiser allows you to manually import events by uploading an ICAL file, but this ICAL Sync gives you the ability to easily add feeds and then forget about them: the plug-in periodically checks the feeds for updates, and automatically updates your events and trashes events which were at one point imported from the feed, but are no longer present.

This is incredibly useful for users who prefer to manage their events through their Google calendar, or if you have a network of sites and you wish to collate them on one ‘parent’ site (listing local WordPress meetups anyone?).

The add-on is just going through the last bit of beta-testing, and this is where you can help. Sign up to help beta-test the plug-in and get a free life-time license when the add-on is launched.

Click here to find out more

Event Organiser 1.6 – What’s New

1.6 is seeing a lot of improvement in terms of functionality – as well as some additional hooks and functions (documentation will follow 1.6 release). There are also some nagging bug fixes.

First things first..

Beta Testers…. please :)

Obviously I would like the release of 1.6 to go without hitch – so I have been, and will be, testing 1.6 before I release it. But the more testers – the more chance we have of catching any bugs. 1.6. is already pretty stable – so if you just want to take it for a spin then feel free – and please report any issues!

The beta version is available on GitHub (dev branch):

New Features

Multiple Venues On Maps

Venue maps can be produced in one of two ways:

  • via shortcodes in post/page/event content [eo_venue_map]
  • or via a function for use in templates (echo eo_get_venue_map();)

By default they use the current (in the Loop) event’s venue. But now you can have several locations on one map. With the shortcode use a comma delimited string:

 [eo_venue_map venue="london-eye,edinburgh-castle"]

With the function pass an array:

  echo eo_get_venue_map(array('london-eye','edinburgh-castle'));

As you may have noticed you can also have tooltips for venues which appear when you click the location. I talk about how to do this a little further down. Note that it doesn’t by default list upcoming events – but you can do this by using this script in your functions.php:

Extra Options For Venue Maps

The venue shortcode also supports the following attributes which are used to change the settings of the Google map:

  • zoomcontrol
  • rotatecontrol
  • pancontrol
  • overviewmapcontrol
  • streetviewcontrol
  • maptypecontrol
  • draggable
  • maptypeid – (e.g. ‘ROADMAP’, ‘SATELLITE’)

All but the last take true/false values and default to true. The last accepts a string (see Google map type IDs) and defaults to ‘ROADMAP’.

 [eo_venue_map venue="london-eye,edinburgh-castle" zoomcontrol=false]

With the function pass an array:

  echo eo_get_venue_map(array('london-eye','edinburgh-castle'),array('zoomcontrol'=>false));

Tooltips for Venue Maps

There is also ‘tooltip’ option (defaults to true) which displays a tooltip with the venue’s address.

The content of this tooltip is filterable using the hook eventorganiser_venue_tooltip.. So you can, for instance have a map with venues on it, and the tooltip for each venue lists (and links to) upcoming events at that venue.

 [eo_venue_map venue="london-eye,edinburgh-castle" tooltip=true]

With the function pass an array:

  echo eo_get_venue_map(array('london-eye','edinburgh-castle'),array('tooltip'=>true);

Extra Hooks

These will be documented shortly – feel free to poke me via twitter @stephenharris88 if I forget…

  • eventorganiser_event_color Filters an events colour (HEX code). This, for instance, allows you to colour events according to venue rather than category. See this post.
  • eventorganiser_calendar_event_link – allows you to replace or remove the link from the fullcalendar. See this post.
  • eventorganiser_event_tooltip – filtering the content of event tooltips on the fullcalendar. See this gist.
  • eventorganiser_fullcalendar_event – filters the entire event array before it is sent to the fullCalendar. This is an array that contains the event’s categories, venue, description, colour, dates etc.
  • eventorganiser_menu_position – change where EO appears in the admin menu (default: 5).
  • eventorganiser_widget_calendar_date_link – filters the widget calendar link. See this post.
  • eventorganiser_venue_tooltip – filters the tooltip for venues on venue map. See this gist

Create Venues On the Fly

There is now a ‘+’ button next to the Venue drop-down on the events admin page. Clicking this allows you to create a new venue for this event. The venue is created when the event is saved. A cancel button also appears when creating a new venue so that you can abandon that new venue and select a pre-existing one instead.

Widget Calendar Options

Aside from including/excluding past events, the Widget calendar has options to restrict the events it shows to certain categories or venues.

Improved UI

It won’t be long before WordPress ditches thickbox in core – so in EO I’ll be phasing out its use too. The admin calendar now uses jQuery UI Tabs and Dialogs – which I think looks a lot better. In case you’re wondering what the tabs are for – more tabs will be added by the booking add-on I’m creating. (More on that soon!).

More fullCalendar options

A massive thanks to Pascal Zerr who contributed this. The fullCalendar now accepts the following additional arguments (defaults are also given):

  • axisformat (default: ‘ga’ ) – time format displayed on the vertical axis of the agenda views
  • weekends (default: true’) – whether to include weekends in the calendar
  • mintime (default: 0′) – Determines the first hour/time that will be displayed, even when the scrollbars have been scrolled all the way up.
  • maxtime (default: 24′) – Determines the last hour/time that will be displayed, even when the scrollbars have been scrolled all the way down.
  • alldayslot (default: true’) – Determines if the “all-day” slot is displayed at the top of the calendar
  • alldaytext (default: __(‘All Day’,’eventorganiser’)) – Sets the text for the all day alot
  • columnformatmonth(default: ‘D’) – Determines format of the date in the column headers in month view
  • columnformatweek(default: ‘D n/j’) – Determines format of the date in the column headers in month view
  • columnformatday (default: ‘l n/j’) – Determines format of the date in the column headers in day view

Note that all date-time formats are in php format.

More tags for shortcode & event list widget ‘template’

You can now use %event_content% and %event_title_attr% and %event_duration% were added in previous versions. %event_excerpt% now takes an optional %event_excerpt{30}% (the excerpt length in words). Also%event_thumbnail%now accepts an additional second (optional) parameter: attributes (a string passed to get_post_thumbnail();

Improved Performance

1.6 removes some ‘dead wood’ from the database. Namely columns that have long been deprecated. The date columns are now indexed, which should improve performance of date based queries. Finally there’s been a bit of code refractoring to improve readability and performance. Javascript files have been ‘reshuffled’ and some code duplication removed.

As mentioned there have also been numerous bug fixes – including some styling issues on the front-end calendar and some ICS import issues. Even if you don’t need the above features, I recommend upgrading.