4-digit years for localized dates in Twig templates

Building a Twig extension to display localized dates with 4-digit years

The localizeddate filter in Twig (in the Intl Extension) formats dates according to the current or given locale.

While you can display dates with the dd/mm/yy / mm/dd/yy / yy/mm/dd formats depending on the locale, it’s not possible to display a 4-digit year like in 31/12/2014 (French style) or 12/31/2014 (US style) or any other format depending on the locale.

This post shows how to implement a Twig extension to display such dates, keeping the localization benefit.

You probably know the Twig Intl Extension to localize dates like this:

{{ post.published_at|localizeddate('short', 'none') }}

It uses the current locale along with the PHP IntlDateFormatter to format dates depending on the current locale.

The short format will display a date like dd/mm/yy for the french (fr_FR) locale, while it will display a date like mm/dd/yy for the en_US locale. The medium format displays a date like Jan 12, 1952.

It’s therefore impossible to display a localized date in the dd/mm/yyyy (fr_FR) or mm/dd/yyyy (en_US) formats.

Here is a solution to display localized dates with a 4-digit year, whatever the year, whatever the locale. The solution is to extend the Twig Intl extension in a new extension, so as to customize the date format generated by the PHP IntlDateFormatter class, transforming the “yy” (2-digit years) pattern to “y” (4-digit years), according to the ICU date format syntax.

<?php
namespace Acme\DemoBundle\Twig;

class AcmeIntlExtension extends \Twig_Extensions_Extension_Intl
{
    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter('localizeddate', array($this, 'twigLocalizedDateFilter'), array('needs_environment' => true)),
        );
    }

    public function twigLocalizedDateFilter($env, $date, $dateFormat = 'medium', $timeFormat = 'medium', $locale = null, $timezone = null, $format = null)
    {
        $date = twig_date_converter($env, $date, $timezone);

        $formatValues = array(
            'none'   => \IntlDateFormatter::NONE,
            'short'  => \IntlDateFormatter::SHORT,
            'medium' => \IntlDateFormatter::MEDIUM,
            'long'   => \IntlDateFormatter::LONG,
            'full'   => \IntlDateFormatter::FULL,
        );

        $formatter = \IntlDateFormatter::create(
            $locale,
            $formatValues[$dateFormat],
            $formatValues[$timeFormat],
            $date->getTimezone()->getName(),
            \IntlDateFormatter::GREGORIAN,
            $format
        );

        if ($format === null) {
            // Replace yy to y (but yyy should be kept yyy, and yyyy kept as yyyy)
            // This way, years are NEVER shown as "yy" (eg. 14), but always like "yyyy" (eg. 2014)
            $pattern = preg_replace(':(^|[^y])y{2,2}([^y]|$):', '$1y$2', $formatter->getPattern());

            $formatter->setPattern($pattern);
        }

        return $formatter->format($date->getTimestamp());
    }
}

Declare the extension in the app/config.yml file:

services:
    acme.twig.extension.intl:
        class: Acme\DemoBundle\Twig\AcmeIntlExtension
        tags:
            - { name: twig.extension }

There was no other easy way to change the pattern of the date, so I had to use the regular expression to change “yy” to “y”, while preserving “yyy” or “yyyy”.