Software localization
How to Format the hh:mm:ss Separators of a TimeSpan in a Culture-Aware Manner
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 TimeSpan
s in a culture-aware way.
Built-in options
Suppose we have two DateTime
s 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 Replace
ment, 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.