diff --git a/docs/design/features/globalization-hybrid-mode.md b/docs/design/features/globalization-hybrid-mode.md
index 373804adfd9ff9..7966be7224710b 100644
--- a/docs/design/features/globalization-hybrid-mode.md
+++ b/docs/design/features/globalization-hybrid-mode.md
@@ -478,3 +478,34 @@ Below function are used from apple native functions:
- [uppercaseStringWithLocale](https://developer.apple.com/documentation/foundation/nsstring/1413316-uppercasestringwithlocale?language=objc)
- [lowercaseStringWithLocale](https://developer.apple.com/documentation/foundation/nsstring/1417298-lowercasestringwithlocale?language=objc)
+## Calandars
+
+Affected public APIs:
+- DateTimeFormatInfo.AbbreviatedDayNames
+- DateTimeFormatInfo.GetAbbreviatedDayName()
+- DateTimeFormatInfo.AbbreviatedMonthGenitiveNames
+- DateTimeFormatInfo.AbbreviatedMonthNames
+- DateTimeFormatInfo.GetAbbreviatedMonthName()
+- DateTimeFormatInfo.AMDesignator
+- DateTimeFormatInfo.CalendarWeekRule
+- DateTimeFormatInfo.DayNames
+- DateTimeFormatInfo.GetDayName()
+- DateTimeFormatInfo.GetEraName()
+- DateTimeFormatInfo.FirstDayOfWeek
+- DateTimeFormatInfo.FullDateTimePattern
+- DateTimeFormatInfo.LongDatePattern
+- DateTimeFormatInfo.LongTimePattern
+- DateTimeFormatInfo.MonthDayPattern
+- DateTimeFormatInfo.MonthGenitiveNames
+- DateTimeFormatInfo.MonthNames
+- DateTimeFormatInfo.GetMonthName()
+- DateTimeFormatInfo.NativeCalendarName
+- DateTimeFormatInfo.PMDesignator
+- DateTimeFormatInfo.ShortDatePattern
+- DateTimeFormatInfo.ShortestDayNames
+- DateTimeFormatInfo.GetShortestDayName()
+- DateTimeFormatInfo.ShortTimePattern
+- DateTimeFormatInfo.YearMonthPattern
+
+Apple Native API does not have an equivalent for abbreviated era name and will return empty string
+- DateTimeFormatInfo.GetAbbreviatedEraName()
diff --git a/src/libraries/Common/src/Interop/Interop.Calendar.iOS.cs b/src/libraries/Common/src/Interop/Interop.Calendar.iOS.cs
new file mode 100644
index 00000000000000..11a6246a1d700b
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Interop.Calendar.iOS.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Globalization
+ {
+ [LibraryImport(Libraries.GlobalizationNative, EntryPoint = "GlobalizationNative_GetCalendarInfoNative", StringMarshalling = StringMarshalling.Utf8)]
+ internal static partial string GetCalendarInfoNative(string localeName, CalendarId calendarId, CalendarDataType calendarDataType);
+ }
+}
diff --git a/src/libraries/System.Globalization.Calendars/tests/Hybrid/System.Globalization.Calendars.IOS.Tests.csproj b/src/libraries/System.Globalization.Calendars/tests/Hybrid/System.Globalization.Calendars.IOS.Tests.csproj
new file mode 100644
index 00000000000000..614c0d2a47f65a
--- /dev/null
+++ b/src/libraries/System.Globalization.Calendars/tests/Hybrid/System.Globalization.Calendars.IOS.Tests.csproj
@@ -0,0 +1,108 @@
+
+
+ $(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-maccatalyst
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs b/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs
index c99b883aa77280..563af14ef500e1 100644
--- a/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs
+++ b/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs
@@ -430,7 +430,7 @@ public void GetEra_Invalid_ThrowsArgumentOutOfRangeException()
Assert.All(DateTime_TestData(calendar), dt =>
{
// JapaneseCalendar throws on ICU, but not on NLS
- if ((calendar is JapaneseCalendar && PlatformDetection.IsNlsGlobalization) ||
+ if ((calendar is JapaneseCalendar && (PlatformDetection.IsNlsGlobalization || PlatformDetection.IsHybridGlobalizationOnOSX)) ||
calendar is HebrewCalendar ||
calendar is TaiwanLunisolarCalendar ||
calendar is JapaneseLunisolarCalendar)
diff --git a/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj
index f21ff6dafff2b8..eac50b0d2f3e0f 100644
--- a/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj
+++ b/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj
@@ -18,9 +18,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 09f13e17a07291..26d78479c72d8a 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -320,6 +320,7 @@
+
@@ -1295,6 +1296,9 @@
Common\Interop\Interop.Calendar.cs
+
+ Common\Interop\Interop.Calendar.iOS.cs
+
Common\Interop\Interop.Casing.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs
index cb15665c979ac6..295a58e9882e17 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs
@@ -13,6 +13,10 @@ private bool LoadCalendarDataFromSystemCore(string localeName, CalendarId calend
return GlobalizationMode.Hybrid ?
JSLoadCalendarDataFromBrowser(localeName, calendarId) :
IcuLoadCalendarDataFromSystem(localeName, calendarId);
+#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
+ return GlobalizationMode.Hybrid ?
+ LoadCalendarDataFromNative(localeName, calendarId) :
+ IcuLoadCalendarDataFromSystem(localeName, calendarId);
#else
return IcuLoadCalendarDataFromSystem(localeName, calendarId);
#endif
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.iOS.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.iOS.cs
new file mode 100644
index 00000000000000..46bfb3d481ca2d
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.iOS.cs
@@ -0,0 +1,108 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace System.Globalization
+{
+ internal sealed partial class CalendarData
+ {
+ private bool LoadCalendarDataFromNative(string localeName, CalendarId calendarId)
+ {
+ Debug.Assert(!GlobalizationMode.UseNls);
+
+ sNativeName = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.NativeName);
+ sMonthDay = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.MonthDay);
+ saShortDates = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.ShortDates).Split("||");
+ saLongDates = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.LongDates).Split("||");
+ saYearMonths = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.YearMonths).Split("||");
+ saDayNames = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.DayNames).Split("||");
+ saAbbrevDayNames = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.AbbrevDayNames).Split("||");
+ saSuperShortDayNames = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.SuperShortDayNames).Split("||");
+
+ string? leapHebrewMonthName = null;
+ saMonthNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.MonthNames).Split("||"), calendarId, ref leapHebrewMonthName);
+ if (leapHebrewMonthName != null)
+ {
+ Debug.Assert(saMonthNames != null);
+
+ // In Hebrew calendar, get the leap month name Adar II and override the non-leap month 7
+ Debug.Assert(calendarId == CalendarId.HEBREW && saMonthNames.Length == 13);
+ saLeapYearMonthNames = (string[]) saMonthNames.Clone();
+ saLeapYearMonthNames[6] = leapHebrewMonthName;
+
+ // The returned data has 6th month name as 'Adar I' and 7th month name as 'Adar'
+ // We need to adjust that in the list used with non-leap year to have 6th month as 'Adar' and 7th month as 'Adar II'
+ // note that when formatting non-leap year dates, 7th month shouldn't get used at all.
+ saMonthNames[5] = saMonthNames[6];
+ saMonthNames[6] = leapHebrewMonthName;
+
+ }
+ saAbbrevMonthNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.AbbrevMonthNames).Split("||"), calendarId, ref leapHebrewMonthName);
+ saMonthGenitiveNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.MonthGenitiveNames).Split("||"), calendarId, ref leapHebrewMonthName);
+ saAbbrevMonthGenitiveNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.AbbrevMonthGenitiveNames).Split("||"), calendarId, ref leapHebrewMonthName);
+
+ saEraNames = NormalizeEraNames(calendarId, GetCalendarInfoNative(localeName, calendarId, CalendarDataType.EraNames).Split("||"));
+ saAbbrevEraNames = Array.Empty();
+
+ return sNativeName != null && saShortDates != null && saLongDates != null && saYearMonths != null &&
+ saDayNames != null && saAbbrevDayNames != null && saSuperShortDayNames != null && saMonthNames != null &&
+ saAbbrevMonthNames != null && saMonthGenitiveNames != null && saAbbrevMonthGenitiveNames != null &&
+ saEraNames != null && saAbbrevEraNames != null;
+ }
+
+ private static string[] NormalizeEraNames(CalendarId calendarId, string[]? eraNames)
+ {
+ // .NET expects that only the Japanese calendars have more than 1 era.
+ // So for other calendars, only return the latest era.
+ if (calendarId != CalendarId.JAPAN && calendarId != CalendarId.JAPANESELUNISOLAR && eraNames?.Length > 0)
+ return new string[] { eraNames![eraNames.Length - 1] };
+
+ return eraNames ?? Array.Empty();
+ }
+
+ private static string[] NormalizeMonthArray(string[] months, CalendarId calendarId, ref string? leapHebrewMonthName)
+ {
+ if (months.Length == 13)
+ return months;
+
+ string[] normalizedMonths = new string[13];
+ // the month-name arrays are expected to have 13 elements. If only returns 12, add an
+ // extra empty string to fill the array.
+ if (months.Length == 12)
+ {
+ normalizedMonths[12] = "";
+ months.CopyTo(normalizedMonths, 0);
+ return normalizedMonths;
+ }
+
+ if (months.Length > 13)
+ {
+ Debug.Assert(calendarId == CalendarId.HEBREW && months.Length == 14);
+
+ if (calendarId == CalendarId.HEBREW)
+ {
+ leapHebrewMonthName = months[13];
+ }
+ for (int i = 0; i < 13; i++)
+ {
+ normalizedMonths[i] = months[i];
+ }
+ return normalizedMonths;
+ }
+
+ throw new Exception("CalendarData.GetCalendarInfoNative() returned an unexpected number of month names.");
+ }
+
+ private static string GetCalendarInfoNative(string localeName, CalendarId calendarId, CalendarDataType calendarDataType)
+ {
+ Debug.Assert(localeName != null);
+
+ return Interop.Globalization.GetCalendarInfoNative(localeName, calendarId, calendarDataType);
+ }
+ }
+}
diff --git a/src/mono/mono/mini/CMakeLists.txt b/src/mono/mono/mini/CMakeLists.txt
index adb4922b9b36cf..47a6abcb7c71e3 100644
--- a/src/mono/mono/mini/CMakeLists.txt
+++ b/src/mono/mono/mini/CMakeLists.txt
@@ -72,7 +72,8 @@ if(HAVE_SYS_ICU)
${icu_shim_sources_base}
pal_locale.m
pal_collation.m
- pal_casing.m)
+ pal_casing.m
+ pal_calendarData.m)
endif()
addprefix(icu_shim_sources "${ICU_SHIM_PATH}" "${icu_shim_sources_base}")
diff --git a/src/native/libs/System.Globalization.Native/CMakeLists.txt b/src/native/libs/System.Globalization.Native/CMakeLists.txt
index b3e70083f32f4f..cf06b6c80482a4 100644
--- a/src/native/libs/System.Globalization.Native/CMakeLists.txt
+++ b/src/native/libs/System.Globalization.Native/CMakeLists.txt
@@ -93,8 +93,8 @@ else()
endif()
if (CLR_CMAKE_TARGET_APPLE)
- set(NATIVEGLOBALIZATION_SOURCES ${NATIVEGLOBALIZATION_SOURCES} pal_locale.m pal_collation.m pal_casing.m)
- set_source_files_properties(pal_locale.m pal_collation.m pal_casing.m PROPERTIES COMPILE_FLAGS ${CLR_CMAKE_COMMON_OBJC_FLAGS})
+ set(NATIVEGLOBALIZATION_SOURCES ${NATIVEGLOBALIZATION_SOURCES} pal_locale.m pal_collation.m pal_casing.m pal_calendarData.m)
+ set_source_files_properties(pal_locale.m pal_collation.m pal_casing.m pal_calendarData.m PROPERTIES COMPILE_FLAGS ${CLR_CMAKE_COMMON_OBJC_FLAGS})
endif()
# time zone names are filtered out of icu data for the browser and associated functionality is disabled
diff --git a/src/native/libs/System.Globalization.Native/entrypoints.c b/src/native/libs/System.Globalization.Native/entrypoints.c
index 1ba348b910b5ff..cffad72a023721 100644
--- a/src/native/libs/System.Globalization.Native/entrypoints.c
+++ b/src/native/libs/System.Globalization.Native/entrypoints.c
@@ -63,6 +63,7 @@ static const Entry s_globalizationNative[] =
DllImportEntry(GlobalizationNative_ChangeCaseNative)
DllImportEntry(GlobalizationNative_CompareStringNative)
DllImportEntry(GlobalizationNative_EndsWithNative)
+ DllImportEntry(GlobalizationNative_GetCalendarInfoNative)
DllImportEntry(GlobalizationNative_GetLocaleInfoIntNative)
DllImportEntry(GlobalizationNative_GetLocaleInfoPrimaryGroupingSizeNative)
DllImportEntry(GlobalizationNative_GetLocaleInfoSecondaryGroupingSizeNative)
diff --git a/src/native/libs/System.Globalization.Native/pal_calendarData.h b/src/native/libs/System.Globalization.Native/pal_calendarData.h
index f6cfbdef448a9f..50458ac471a609 100644
--- a/src/native/libs/System.Globalization.Native/pal_calendarData.h
+++ b/src/native/libs/System.Globalization.Native/pal_calendarData.h
@@ -91,3 +91,9 @@ PALEXPORT int32_t GlobalizationNative_GetJapaneseEraStartDate(int32_t era,
int32_t* startYear,
int32_t* startMonth,
int32_t* startDay);
+
+#ifdef __APPLE__
+PALEXPORT const char* GlobalizationNative_GetCalendarInfoNative(const char* localeName,
+ CalendarId calendarId,
+ CalendarDataType dataType);
+#endif
diff --git a/src/native/libs/System.Globalization.Native/pal_calendarData.m b/src/native/libs/System.Globalization.Native/pal_calendarData.m
new file mode 100644
index 00000000000000..307590dfc7a9be
--- /dev/null
+++ b/src/native/libs/System.Globalization.Native/pal_calendarData.m
@@ -0,0 +1,137 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#include
+#include "pal_icushim_internal.h"
+#include "pal_calendarData.h"
+#import
+
+#if defined(TARGET_MACCATALYST) || defined(TARGET_IOS) || defined(TARGET_TVOS)
+
+/*
+Function:
+GetCalendarIdentifier
+
+Gets the associated NSCalendarIdentifier for the CalendarId.
+*/
+static NSString* GetCalendarIdentifier(CalendarId calendarId)
+{
+ NSString *calendarIdentifier = NSCalendarIdentifierGregorian;
+ switch (calendarId)
+ {
+ case JAPAN:
+ calendarIdentifier = NSCalendarIdentifierJapanese;
+ break;
+ case THAI:
+ calendarIdentifier = NSCalendarIdentifierBuddhist;
+ break;
+ case HEBREW:
+ calendarIdentifier = NSCalendarIdentifierHebrew;
+ break;
+ case PERSIAN:
+ calendarIdentifier = NSCalendarIdentifierPersian;
+ break;
+ case HIJRI:
+ calendarIdentifier = NSCalendarIdentifierIslamic;
+ break;
+ case UMALQURA:
+ calendarIdentifier = NSCalendarIdentifierIslamicUmmAlQura;
+ break;
+ case TAIWAN:
+ calendarIdentifier = NSCalendarIdentifierRepublicOfChina;
+ break;
+ default:
+ break;
+ }
+ return calendarIdentifier;
+}
+
+/*
+Function:
+GlobalizationNative_GetCalendarInfoNative
+
+Gets a single string of calendar information for a given locale, calendar, and calendar data type.
+with the requested value.
+*/
+const char* GlobalizationNative_GetCalendarInfoNative(const char* localeName, CalendarId calendarId, CalendarDataType dataType)
+{
+ NSString *locName = [NSString stringWithFormat:@"%s", localeName];
+ NSLocale *currentLocale = [[NSLocale alloc] initWithLocaleIdentifier:locName];
+
+ if (dataType == CalendarData_MonthDay)
+ {
+ NSString *formatString = [NSDateFormatter dateFormatFromTemplate:@"MMMMd" options:0 locale:currentLocale];
+ return formatString ? strdup([formatString UTF8String]) : NULL;
+ }
+ else if (dataType == CalendarData_YearMonths)
+ {
+ NSString *formatString = [NSDateFormatter dateFormatFromTemplate:@"MMMM yyyy" options:0 locale:currentLocale];
+ return formatString ? strdup([formatString UTF8String]) : NULL;
+ }
+
+ NSString *calendarIdentifier = GetCalendarIdentifier(calendarId);
+ NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:calendarIdentifier];
+
+ if (dataType == CalendarData_NativeName)
+ return calendar ? strdup([[calendar calendarIdentifier] UTF8String]) : NULL;
+
+ NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
+ dateFormat.locale = currentLocale;
+ dateFormat.calendar = calendar;
+
+ NSArray *result;
+ switch (dataType)
+ {
+ case CalendarData_ShortDates:
+ {
+ [dateFormat setDateStyle:NSDateFormatterShortStyle];
+ NSString *shortFormatString = [dateFormat dateFormat];
+ [dateFormat setDateStyle:NSDateFormatterMediumStyle];
+ NSString *mediumFormatString = [dateFormat dateFormat];
+ NSString *yearMonthDayFormat = [NSDateFormatter dateFormatFromTemplate:@"yMd" options:0 locale:currentLocale];
+ result = @[shortFormatString, mediumFormatString, yearMonthDayFormat];
+ break;
+ }
+ case CalendarData_LongDates:
+ {
+ [dateFormat setDateStyle:NSDateFormatterLongStyle];
+ NSString *longFormatString = [dateFormat dateFormat];
+ [dateFormat setDateStyle:NSDateFormatterFullStyle];
+ NSString *fullFormatString = [dateFormat dateFormat];
+ result = @[longFormatString, fullFormatString];
+ break;
+ }
+ case CalendarData_DayNames:
+ result = [dateFormat standaloneWeekdaySymbols];
+ break;
+ case CalendarData_AbbrevDayNames:
+ result = [dateFormat shortStandaloneWeekdaySymbols];
+ break;
+ case CalendarData_MonthNames:
+ result = [dateFormat standaloneMonthSymbols];
+ break;
+ case CalendarData_AbbrevMonthNames:
+ result = [dateFormat shortStandaloneMonthSymbols];
+ break;
+ case CalendarData_SuperShortDayNames:
+ result = [dateFormat veryShortStandaloneWeekdaySymbols];
+ break;
+ case CalendarData_MonthGenitiveNames:
+ result = [dateFormat monthSymbols];
+ break;
+ case CalendarData_AbbrevMonthGenitiveNames:
+ result = [dateFormat shortMonthSymbols];
+ break;
+ case CalendarData_EraNames:
+ case CalendarData_AbbrevEraNames:
+ result = [dateFormat eraSymbols];
+ break;
+ default:
+ assert(false);
+ return NULL;
+ }
+
+ NSString *arrayToString = [[result valueForKey:@"description"] componentsJoinedByString:@"||"];
+ return arrayToString ? strdup([arrayToString UTF8String]) : NULL;
+}
+#endif