TimeZoneInfo.DisplayName values are not localized on Linux
authorEric Erhardt <eric.erhardt@microsoft.com>
Wed, 27 Jan 2016 23:41:14 +0000 (17:41 -0600)
committerEric Erhardt <eric.erhardt@microsoft.com>
Tue, 2 Feb 2016 15:42:29 +0000 (09:42 -0600)
Fixed by calling ICU's ucal_getTimeZoneDisplayName to read the display names for the current locale.

Fix https://github.com/dotnet/corefx/issues/2748

src/corefx/System.Globalization.Native/calendarData.cpp
src/corefx/System.Globalization.Native/errors.h [new file with mode: 0644]
src/corefx/System.Globalization.Native/timeZoneInfo.cpp
src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.Calendar.cs
src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.ResultCode.cs [new file with mode: 0644]
src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.TimeZoneInfo.cs
src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.Utils.cs [new file with mode: 0644]
src/mscorlib/corefx/System/Globalization/CalendarData.Unix.cs
src/mscorlib/mscorlib.shared.sources.props
src/mscorlib/src/System/TimeZoneInfo.cs

index fb035a9..f91cc0c 100644 (file)
@@ -9,6 +9,7 @@
 #include "config.h"
 #include "locale.hpp"
 #include "holders.h"
+#include "errors.h"
 
 #define GREGORIAN_NAME "gregorian"
 #define JAPANESE_NAME "japanese"
@@ -82,43 +83,11 @@ enum CalendarDataType : int32_t
     AbbrevEraNames = 14,
 };
 
-/*
-* These values should be kept in sync with
-* System.Globalization.CalendarDataResult
-*/
-enum CalendarDataResult : int32_t
-{
-    Success = 0,
-    UnknownError = 1,
-    InsufficentBuffer = 2,
-};
-
 // the function pointer definition for the callback used in EnumCalendarInfo
 typedef void (*EnumCalendarInfoCallback)(const UChar*, const void*);
 
 /*
 Function:
-GetCalendarDataResult
-
-Converts a UErrorCode to a CalendarDataResult.
-*/
-CalendarDataResult GetCalendarDataResult(UErrorCode err)
-{
-    if (U_SUCCESS(err))
-    {
-        return Success;
-    }
-
-    if (err == U_BUFFER_OVERFLOW_ERROR)
-    {
-        return InsufficentBuffer;
-    }
-
-    return UnknownError;
-}
-
-/*
-Function:
 GetCalendarName
 
 Gets the associated ICU calendar name for the CalendarId.
@@ -247,18 +216,18 @@ GetMonthDayPattern
 
 Gets the Month-Day DateTime pattern for the specified locale.
 */
-CalendarDataResult GetMonthDayPattern(const char* locale, UChar* sMonthDay, int32_t stringCapacity)
+ResultCode GetMonthDayPattern(const char* locale, UChar* sMonthDay, int32_t stringCapacity)
 {
     UErrorCode err = U_ZERO_ERROR;
     UDateTimePatternGenerator* pGenerator = udatpg_open(locale, &err);
     UDateTimePatternGeneratorHolder generatorHolder(pGenerator, err);
 
     if (U_FAILURE(err))
-        return GetCalendarDataResult(err);
+        return GetResultCode(err);
 
     udatpg_getBestPattern(pGenerator, UDAT_MONTH_DAY_UCHAR, -1, sMonthDay, stringCapacity, &err);
 
-    return GetCalendarDataResult(err);
+    return GetResultCode(err);
 }
 
 /*
@@ -267,8 +236,7 @@ GetNativeCalendarName
 
 Gets the native calendar name.
 */
-CalendarDataResult
-GetNativeCalendarName(const char* locale, CalendarId calendarId, UChar* nativeName, int32_t stringCapacity)
+ResultCode GetNativeCalendarName(const char* locale, CalendarId calendarId, UChar* nativeName, int32_t stringCapacity)
 {
     UErrorCode err = U_ZERO_ERROR;
     ULocaleDisplayNames* pDisplayNames = uldn_open(locale, ULDN_STANDARD_NAMES, &err);
@@ -276,7 +244,7 @@ GetNativeCalendarName(const char* locale, CalendarId calendarId, UChar* nativeNa
 
     uldn_keyValueDisplayName(pDisplayNames, "calendar", GetCalendarName(calendarId), nativeName, stringCapacity, &err);
 
-    return GetCalendarDataResult(err);
+    return GetResultCode(err);
 }
 
 /*
@@ -286,7 +254,7 @@ GetCalendarInfo
 Gets a single string of calendar information by filling the result parameter
 with the requested value.
 */
-extern "C" CalendarDataResult GlobalizationNative_GetCalendarInfo(
+extern "C" ResultCode GlobalizationNative_GetCalendarInfo(
     const UChar* localeName, CalendarId calendarId, CalendarDataType dataType, UChar* result, int32_t resultCapacity)
 {
     UErrorCode err = U_ZERO_ERROR;
@@ -542,9 +510,9 @@ The context parameter is passed through to the callback along with each string.
 */
 extern "C" int32_t GlobalizationNative_EnumCalendarInfo(
                         EnumCalendarInfoCallback callback, 
-                        const UChar* localeName, 
-                        CalendarId calendarId, 
-                        CalendarDataType dataType, 
+                        const UChar* localeName,
+                        CalendarId calendarId,
+                        CalendarDataType dataType,
                         const void* context)
 {
     UErrorCode err = U_ZERO_ERROR;
diff --git a/src/corefx/System.Globalization.Native/errors.h b/src/corefx/System.Globalization.Native/errors.h
new file mode 100644 (file)
index 0000000..2bfbdb2
--- /dev/null
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#pragma once
+
+#include <unicode/utypes.h>
+
+/*
+* These values should be kept in sync with
+* Interop.GlobalizationInterop.ResultCode
+*/
+enum ResultCode : int32_t
+{
+    Success = 0,
+    UnknownError = 1,
+    InsufficentBuffer = 2,
+};
+
+/*
+Converts a UErrorCode to a ResultCode.
+*/
+static ResultCode GetResultCode(UErrorCode err)
+{
+    if (err == U_BUFFER_OVERFLOW_ERROR || err == U_STRING_NOT_TERMINATED_WARNING)
+    {
+        return InsufficentBuffer;
+    }
+
+    if (U_SUCCESS(err))
+    {
+        return Success;
+    }
+
+    return UnknownError;
+}
index 025bccd..d0e01e5 100644 (file)
@@ -5,11 +5,13 @@
 
 #include <stdint.h>
 #include <unistd.h>
+#include <unicode/ucal.h>
 
-/*
-Function:
-ReadLink
+#include "locale.hpp"
+#include "holders.h"
+#include "errors.h"
 
+/*
 Gets the symlink value for the path.
 */
 extern "C" int32_t GlobalizationNative_ReadLink(const char* path, char* result, size_t resultCapacity)
@@ -22,3 +24,36 @@ extern "C" int32_t GlobalizationNative_ReadLink(const char* path, char* result,
     result[r] = '\0';
     return true;
 }
+
+/*
+These values should be kept in sync with the managed Interop.GlobalizationInterop.TimeZoneDisplayNameType enum.
+*/
+enum TimeZoneDisplayNameType : int32_t
+{
+    Generic = 0,
+    Standard = 1,
+    DaylightSavings = 2,
+};
+
+/*
+Gets the localized display name for the specified time zone.
+*/
+extern "C" ResultCode GlobalizationNative_GetTimeZoneDisplayName(
+    const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength)
+{
+    UErrorCode err = U_ZERO_ERROR;
+    char locale[ULOC_FULLNAME_CAPACITY];
+    GetLocale(localeName, locale, ULOC_FULLNAME_CAPACITY, false, &err);
+
+    int32_t timeZoneIdLength = -1; // timeZoneId is NULL-terminated
+    UCalendar* calendar = ucal_open(timeZoneId, timeZoneIdLength, locale, UCAL_DEFAULT, &err);
+    UCalendarHolder calendarHolder(calendar, err);
+
+    // TODO (https://github.com/dotnet/corefx/issues/5741): need to support Generic names, but ICU "C" api
+    // has no public option for this. For now, just use the ICU standard name for both Standard and Generic
+    // (which is the same behavior on Windows with the mincore TIME_ZONE_INFORMATION APIs).
+    ucal_getTimeZoneDisplayName(
+        calendar, type == DaylightSavings ? UCAL_DST : UCAL_STANDARD, locale, result, resultLength, &err);
+
+    return GetResultCode(err);
+}
index a0076bc..7b3caea 100644 (file)
@@ -19,10 +19,9 @@ internal static partial class Interop
         internal static extern int GetCalendars(string localeName, CalendarId[] calendars, int calendarsCapacity);
 
         [DllImport(Libraries.GlobalizationInterop, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_GetCalendarInfo")]
-        internal static extern CalendarDataResult GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType calendarDataType, [Out] StringBuilder result, int resultCapacity);
+        internal static extern ResultCode GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType calendarDataType, [Out] StringBuilder result, int resultCapacity);
 
         [DllImport(Libraries.GlobalizationInterop, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_EnumCalendarInfo")]
-        [return: MarshalAs(UnmanagedType.Bool)]
         internal static extern bool EnumCalendarInfo(EnumCalendarInfoCallback callback, string localeName, CalendarId calendarId, CalendarDataType calendarDataType, IntPtr context);
 
         [DllImport(Libraries.GlobalizationInterop, EntryPoint = "GlobalizationNative_GetLatestJapaneseEra")]
diff --git a/src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.ResultCode.cs b/src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.ResultCode.cs
new file mode 100644 (file)
index 0000000..4621580
--- /dev/null
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+internal static partial class Interop
+{
+    internal static partial class GlobalizationInterop
+    {
+        // needs to be kept in sync with ResultCode in System.Globalization.Native
+        internal enum ResultCode
+        {
+            Success = 0,
+            UnknownError = 1,
+            InsufficentBuffer = 2,
+        }
+    }
+}
index 9b17aa3..26a9fe0 100644 (file)
@@ -10,7 +10,22 @@ internal static partial class Interop
     internal static partial class GlobalizationInterop
     {
         [DllImport(Libraries.GlobalizationInterop, CharSet = CharSet.Ansi, EntryPoint = "GlobalizationNative_ReadLink")] // readlink requires char*
-        [return: MarshalAs(UnmanagedType.Bool)]
         internal static extern bool ReadLink(string filePath, [Out] StringBuilder result, uint resultCapacity);
+
+        // needs to be kept in sync with TimeZoneDisplayNameType in System.Globalization.Native
+        internal enum TimeZoneDisplayNameType
+        {
+            Generic = 0,
+            Standard = 1,
+            DaylightSavings = 2,
+        }
+
+        [DllImport(Libraries.GlobalizationInterop, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_GetTimeZoneDisplayName")]
+        internal static extern ResultCode GetTimeZoneDisplayName(
+            string localeName, 
+            string timeZoneId, 
+            TimeZoneDisplayNameType type, 
+            [Out] StringBuilder result, 
+            int resultLength);
     }
 }
diff --git a/src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.Utils.cs b/src/mscorlib/corefx/Interop/Unix/System.Globalization.Native/Interop.Utils.cs
new file mode 100644 (file)
index 0000000..33b10c0
--- /dev/null
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text;
+
+internal static partial class Interop
+{
+    /// <summary>
+    /// Helper for making interop calls that return a string, but we don't know
+    /// the correct size of buffer to make. So invoke the interop call with an
+    /// increasing buffer until the size is big enough.
+    /// </summary>
+    internal static bool CallStringMethod<TArg1, TArg2, TArg3>(
+        Func<TArg1, TArg2, TArg3, StringBuilder, GlobalizationInterop.ResultCode> interopCall,
+        TArg1 arg1,
+        TArg2 arg2,
+        TArg3 arg3,
+        out string result)
+    {
+        const int initialStringSize = 80;
+        const int maxDoubleAttempts = 5;
+
+        StringBuilder stringBuilder = StringBuilderCache.Acquire(initialStringSize);
+
+        for (int i = 0; i < maxDoubleAttempts; i++)
+        {
+            GlobalizationInterop.ResultCode resultCode = interopCall(arg1, arg2, arg3, stringBuilder);
+
+            if (resultCode == GlobalizationInterop.ResultCode.Success)
+            {
+                result = StringBuilderCache.GetStringAndRelease(stringBuilder);
+                return true;
+            }
+            else if (resultCode == GlobalizationInterop.ResultCode.InsufficentBuffer)
+            {
+                // increase the string size and loop
+                stringBuilder.EnsureCapacity(stringBuilder.Capacity * 2);
+            }
+            else
+            {
+                // if there is an unknown error, don't proceed
+                break;
+            }
+        }
+
+        StringBuilderCache.Release(stringBuilder);
+        result = null;
+        return false;
+    }
+}
index 26a6b3e..6c6a18e 100644 (file)
@@ -30,14 +30,6 @@ namespace System.Globalization
         AbbrevEraNames = 14,
     }
 
-    // needs to be kept in sync with CalendarDataResult in System.Globalization.Native
-    internal enum CalendarDataResult
-    {
-        Success = 0,
-        UnknownError = 1,
-        InsufficentBuffer = 2,
-    }
-
     internal partial class CalendarData
     {
         private bool LoadCalendarDataFromSystem(String localeName, CalendarId calendarId)
@@ -97,41 +89,18 @@ namespace System.Globalization
         [SecuritySafeCritical]
         private static bool GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string calendarString)
         {
-            calendarString = null;
-
-            const int initialStringSize = 80;
-            const int maxDoubleAttempts = 5;
-
-            for (int i = 0; i < maxDoubleAttempts; i++)
-            {
-                StringBuilder stringBuilder = StringBuilderCache.Acquire((int)(initialStringSize * Math.Pow(2, i)));
-
-                CalendarDataResult result = Interop.GlobalizationInterop.GetCalendarInfo(
-                    localeName,
-                    calendarId,
-                    dataType,
-                    stringBuilder,
-                    stringBuilder.Capacity);
-
-                if (result == CalendarDataResult.Success)
-                {
-                    calendarString = StringBuilderCache.GetStringAndRelease(stringBuilder);
-                    return true;
-                }
-                else
-                {
-                    StringBuilderCache.Release(stringBuilder);
-
-                    if (result != CalendarDataResult.InsufficentBuffer)
-                    {
-                        return false;
-                    }
-
-                    // else, it is an InsufficentBuffer error, so loop and increase the string size
-                }
-            }
-
-            return false;
+            return Interop.CallStringMethod(
+                (locale, calId, type, stringBuilder) =>
+                    Interop.GlobalizationInterop.GetCalendarInfo(
+                        locale,
+                        calId,
+                        type,
+                        stringBuilder,
+                        stringBuilder.Capacity),
+                localeName,
+                calendarId,
+                dataType,
+                out calendarString);
         }
 
         private static bool EnumDatePatterns(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] datePatterns)
index e14bdd6..8b1bce3 100644 (file)
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\Interop\Unix\System.Globalization.Native\Interop.Casing.cs" />
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\Interop\Unix\System.Globalization.Native\Interop.Collation.cs" />
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\Interop\Unix\System.Globalization.Native\Interop.Locale.cs" />
+    <GlobalizationSources Include="$(CoreFxSourcesRoot)\Interop\Unix\System.Globalization.Native\Interop.ResultCode.cs" />
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\Interop\Unix\System.Globalization.Native\Interop.TimeZoneInfo.cs" />
+    <GlobalizationSources Include="$(CoreFxSourcesRoot)\Interop\Unix\System.Globalization.Native\Interop.Utils.cs" />
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\System\Globalization\CalendarData.Unix.cs" />
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\System\Globalization\CompareInfo.Unix.cs" />
     <GlobalizationSources Include="$(CoreFxSourcesRoot)\System\Globalization\CultureData.Unix.cs" />
index baa1fac..08ddc3e 100644 (file)
@@ -1175,7 +1175,9 @@ namespace System {
             m_displayName = c_localId;
             m_baseUtcOffset = TimeSpan.Zero;
          
-            // find the best matching baseUtcOffset and display strings based on the current utcNow value
+            // find the best matching baseUtcOffset and display strings based on the current utcNow value.
+            // NOTE: read the display strings from the the tzfile now in case they can't be loaded later
+            // from the globalization data.
             DateTime utcNow = DateTime.UtcNow;
             for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++) {
                 int type = typeOfLocalTime[i];
@@ -1203,6 +1205,10 @@ namespace System {
             }
             m_displayName = m_standardDisplayName;
 
+            GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType.Generic, ref m_displayName);
+            GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType.Standard, ref m_standardDisplayName);
+            GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType.DaylightSavings, ref m_daylightDisplayName);
+
             // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns
             // with DateTimeOffset, SQL Server, and the W3C XML Specification
             if (m_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0) {
@@ -1216,6 +1222,30 @@ namespace System {
 
             ValidateTimeZoneInfo(m_id, m_baseUtcOffset, m_adjustmentRules, out m_supportsDaylightSavingTime);
         }
+
+        private void GetDisplayName(Interop.GlobalizationInterop.TimeZoneDisplayNameType nameType, ref string displayName)
+        {
+            string timeZoneDisplayName;
+            bool result = Interop.CallStringMethod(
+                (locale, id, type, stringBuilder) => Interop.GlobalizationInterop.GetTimeZoneDisplayName(
+                    locale,
+                    id,
+                    type,
+                    stringBuilder,
+                    stringBuilder.Capacity),
+                CultureInfo.CurrentUICulture.Name,
+                m_id,
+                nameType,
+                out timeZoneDisplayName);
+
+            // If there is an unknown error, don't set the displayName field.
+            // It will be set to the abbreviation that was read out of the tzfile.
+            if (result)
+            {
+                displayName = timeZoneDisplayName;
+            }
+        }
+
 #endif // PLATFORM_UNIX
 
         private TimeZoneInfo(