UPDATE: I've made a library to do this work for you. Read Localized time zone names in .NET.

If you've ever used the TimeZoneInfo object in .Net, you'll be familiar with the following properties:

timeZoneInfo.Id             // "Eastern Standard Time"
timeZoneInfo.StandardName   // "Eastern Standard Time"
timeZoneInfo.DaylightName   // "Eastern Daylight Time"
timeZoneInfo.DisplayName    // "(UTC-05:00) Eastern Time (US & Canada)"


The Id property can be misleading, because it often contains the word "Standard" - even though it's the identifier for the time zone, representing both standard and daylight time. In other words, in the above example, try to think of timeZoneInfo.Id to be "Eastern Time", even though it says "Eastern Standard Time".

The StandardName and DaylightName properties are pretty much what they say they are, but there are some irregularities. For example, consider:

timeZoneInfo.Id             // "GMT Standard Time"
timeZoneInfo.StandardName   // "GMT Standard Time"
timeZoneInfo.DaylightName   // "GMT Daylight Time"
timeZoneInfo.DisplayName    // "(UTC) Dublin, Edinburgh, Lisbon, London"


That's the time zone for the United Kingdom. However, the names are all funny. In real life, they use "Greenwich Mean Time" as the standard name, and "British Summer Time" as the daylight name - but that's not represented here at all. Saying "GMT Standard Time" is just silly. Expand the abbreviation and you would get "Greenwich Mean Time Standard Time"!

Really, saying that the UK is in the GMT time zone at all is quite inaccurate. It only follows GMT during the winter, when it is aligned with UTC. In the summer, when BST is in effect, it is aligned to UTC+01:00.

So the DisplayName property is lying (sometimes) about the offset it shows. It's not the current offset, its only the standard offset. US Eastern Time would be UTC-04:00 during EDT, but the display name would still show UTC-05:00 in it.

Worse yet, the StandardName, DaylightName, and DisplayName fields do not properly participate in .NET's globalization and localization features. If your current thread's language is Spanish, you'll still get English values here! Only the Id property should be fixed to English, because that uniquely identifies the time zone.

But really, they're not quite localized to English. They actually pick up their language from the settings of the Windows operating system where the code is running! You should see them in Japanese!

There's a better way. You can look up localized language-specific names for the time zones from the Unicode CLDR Project. This set of XML files contains all kinds of useful information about time zones, among other things.

I recently answered a question on StackOverflow that uses this data. I'll repeat the technique here. You'll need to download the latest core.zip from the CLDR project and extract, then point the basePath at that folder.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;

static Dictionary<TimeZoneInfo, string> GetCldrGenericLongNames(string basePath,
string language)
{
// Set some file paths
string winZonePath = basePath + @"\common\supplemental\windowsZones.xml";
string metaZonePath = basePath + @"\common\supplemental\metaZones.xml";
string langDataPath = basePath + @"\common\main\" + language + ".xml";

// Make sure the files exist
if (!File.Exists(winZonePath) || !File.Exists(metaZonePath)
|| !File.Exists(langDataPath))
{
throw new FileNotFoundException(
"Could not find CLDR files with language '" + language + "'.");
}

// Prepare the results dictionary
var results = new Dictionary<TimeZoneInfo, string>();

// Loop for each Windows time zone
foreach (var timeZoneInfo in TimeZoneInfo.GetSystemTimeZones())
{
// Get the IANA zone from the Windows zone
string pathToMapZone =
"/supplementalData/windowsZones/mapTimezones/mapZone" +
"[@territory='001' and @other='" + timeZoneInfo.Id + "']";
var mapZoneNode = xmlWinZones.XPathSelectElement(pathToMapZone);
if (mapZoneNode == null) continue;
string primaryIanaZone = mapZoneNode.Attribute("type").Value;

// Get the MetaZone from the IANA zone
string pathToMetaZone =
"/supplementalData/metaZones/metazoneInfo/timezone" +
"[@type='" + primaryIanaZone +  "']/usesMetazone";
var metaZoneNode = xmlMetaZones.XPathSelectElements(pathToMetaZone)
.LastOrDefault();
if (metaZoneNode == null) continue;
string metaZone = metaZoneNode.Attribute("mzone").Value;

// Get the generic name for the MetaZone
string pathToNames =
"/ldml/dates/timeZoneNames/metazone" +
"[@type='" + metaZone + "']/long";
var nameNodes = xmlLangData.XPathSelectElement(pathToNames);
var genericNameNode = nameNodes.Element("generic");
var standardNameNode = nameNodes.Element("standard");
string name = genericNameNode != null
? genericNameNode.Value
: standardNameNode != null
? standardNameNode.Value
: null;

// If we have valid results, add to the dictionary
if (name != null)
{
}
}

return results;
}


Calling this will get you a dictionary which you can then use for lookups. Example:

// load the data once an cache it in a static variable
const string basePath = @"C:\path\to\extracted\cldr\core";
private static readonly Dictionary<TimeZoneInfo, string> timeZoneNames =
GetCldrGenericLongNames(basePath, "en");

// then later access it like this
string tzname = timeZoneNames[yourTimeZoneInfoObject];


And now you know how to do it! Isn't it fun to jump through hoops?