Software localization

How to Format the hh:mm:ss Separators of a TimeSpan in a Culture-Aware Manner

.NET only has limited options with its TimeSpan.toString() method. Here's how to work around it and convert a timespan to a locale-aware string.
Software localization blog category featured image | Phrase

When working on our .NET apps, it's not uncommon that to have date calculations or other logic that requires us to use TimeSpan objects. If we're internationalizing our apps, we may want to format these TimeSpans in a culture-aware way.

Built-in options

Suppose we have two DateTimes and we subtract them to get a TimeSpan interval.

using System;

class MainClass

{

    public static void Main (string[] args)

    {

        var dateTime1 = new DateTime(1970, 1, 1, 0, 0, 0);

        var dateTime2 = new DateTime(2020, 4, 1, 13, 30, 22, 500);

        TimeSpan span = dateTime2 - dateTime1;

        Console.WriteLine(span.ToString());

        // => 18353.13:30:22.5000000

        Console.WriteLine(span.ToString(@"hh\:mm\:ss"));

        // => 13:30:22

    }

}

The built-in TimeSpan.ToString() gives us some formatting options. We can pass it no arguments to get the constant (invariant) format, which looks like "18353.13:30:22.5000000" (days.hours:minues:seconds.ten-millionths-of-a-second).

We can also pass TimeSpan.ToString() a custom string as we do in the above call, span.ToString(@"hh\:mm\:ss"). This uses custom format specifiers to gives us a string formatted in an exact way, which, unfortunately, is not culture-aware.

🔗 Resource » See all custom TimeSpan format strings on Microsoft's .NET docs.

There is an overload of TimeSpan.ToString() that takes an IFormatProvider, which gives us some culture-sensitivity.

using System;

using System.Globalization;

class MainClass

{

    public static void Main (string[] args)

    {

        var dateTime1 = new DateTime(1970, 1, 1, 0, 0, 0);

        var dateTime2 = new DateTime(2020, 4, 1, 13, 30, 22, 500);

        TimeSpan span = dateTime2 - dateTime1;

        Console.WriteLine(span.ToString("G", new CultureInfo("en-US")));

        // => 18353:13:30:22.5000000

        Console.WriteLine(span.ToString("G", new CultureInfo("fr-FR")));

        // => 18353:13:30:22,5000000

        Console.WriteLine(span.ToString("G", new CultureInfo("ml-IN")));

        // => 18353:13:30:22.5000000

    }

}

Notice that when we pass ToString() a French CultureInfo object (which conforms to IFormatProvider), the decimal separator used is a comma (,), not a dot (.). This is the correct decimal separator for French.

However, the time-separator used is always a colon (:), regardless of culture. This is evident in the Malayalam, India locale above. The official Malayalam time separator is a dot (.), and yet the formatted string uses a colon (:). This limitation may well be the correct behavior for localized time interval representation, although I couldn't find a reliable standard for localized time intervals when I researched this.

🔗 Resource » The localizable version of TimeSpan.ToString() only accepts standard format strings ("c" or "g" or "G"). Check out the standard TimeSpan format strings on Microsoft's .NET docs.

The workaround

Regardless of the "correct" behavior of localized time interval formatting, we may have a case in our app where we want to format a TimeSpan such that it mimics a culture's hour-minute-second representation. We can write a custom TimeSpan extension method to achieve this.

using System;

using System.Globalization;

public static class TimeSpanExtensions

{

    public static string LocalizedTimeFormat(

        this TimeSpan timeSpan, CultureInfo cultureInfo)

    {

        string formattedTimeSpan = timeSpan.ToString(@"hh\:mm\:ss");

        string timeSeparator = cultureInfo.DateTimeFormat.TimeSeparator;

        return formattedTimeSpan.Replace(":", timeSeparator);

    }

}

class MainClass

{

    public static void Main (string[] args)

    {

        var dateTime1 = new DateTime(1970, 1, 1, 0, 0, 0);

        var dateTime2 = new DateTime(2020, 4, 1, 13, 30, 22, 500);

        TimeSpan span = dateTime2 - dateTime1;

        Console.WriteLine(span.LocalizedTimeFormat(new CultureInfo("en-US")));

        // => 13:30:22

        Console.WriteLine(span.LocalizedTimeFormat(new CultureInfo("fr-FR")));

        // => 13:30:22

        Console.WriteLine(span.LocalizedTimeFormat(new CultureInfo("ml-IN")));

        // => 13.30.22

    }

}

Our extension method, TimeSpan.LocalizedTimeFormat(CultureInfo), simply formats the TimeSpan using a custom hh:mm:ss format. It then uses the culture-aware TimeSeparator from its given CultureInfo, and does a string Replacement, switching out colons for the culture's time separator.

In the third call above, the Malaylam output is "13.30.22", which reflects that culture's usual hour-minute-second representation —using a dot (.) separator instead of a colon (:).

Til next time

The real-world needs of our apps will sometimes mean extending .NET's built-in i18n functionality to solve our problems. Creative extensions are part of the fun of programming; however, the tedium of juggling string files between translators in a localized app is not.

That's where Phrase comes in. Built by developers for developers, and featuring a sleek web console for translators, two-way string sync via CLI, over-the-air (OTA) translations for mobile apps, and a ton of integrations and extensibility, Phrase does the heavy i18n lifting so you can focus on the code you love. Check out all of Phrase's products, and sign up for a free 14-day trial.