From fe85575833b1860fad81b957613e5908c3baf9ac Mon Sep 17 00:00:00 2001 From: Abdallah Selim Date: Fri, 13 Feb 2026 16:32:40 +0200 Subject: [PATCH] add dashboard time format setting with 12/24-hour override add a Settings dialog option for time format (System, 12-hour, 24-hour) and persist the selection in browser local storage. update BrowserTimeProvider and MainLayout startup flow to restore the saved preference, and apply it through FormatHelpers for time/date-time rendering while preserving culture-specific separators. add localization resources for the new setting and tests covering forced 12/24-hour formatting behavior across cultures. --- .../Components/Dialogs/SettingsDialog.razor | 11 +++ .../Dialogs/SettingsDialog.razor.cs | 29 ++++++++ .../Components/Layout/MainLayout.razor.cs | 6 ++ .../Model/BrowserTimeProvider.cs | 14 ++++ .../Resources/Dialogs.Designer.cs | 36 ++++++++++ src/Aspire.Dashboard/Resources/Dialogs.resx | 14 +++- .../Resources/xlf/Dialogs.cs.xlf | 20 ++++++ .../Resources/xlf/Dialogs.de.xlf | 20 ++++++ .../Resources/xlf/Dialogs.es.xlf | 20 ++++++ .../Resources/xlf/Dialogs.fr.xlf | 20 ++++++ .../Resources/xlf/Dialogs.it.xlf | 20 ++++++ .../Resources/xlf/Dialogs.ja.xlf | 20 ++++++ .../Resources/xlf/Dialogs.ko.xlf | 20 ++++++ .../Resources/xlf/Dialogs.pl.xlf | 20 ++++++ .../Resources/xlf/Dialogs.pt-BR.xlf | 20 ++++++ .../Resources/xlf/Dialogs.ru.xlf | 20 ++++++ .../Resources/xlf/Dialogs.tr.xlf | 20 ++++++ .../Resources/xlf/Dialogs.zh-Hans.xlf | 20 ++++++ .../Resources/xlf/Dialogs.zh-Hant.xlf | 20 ++++++ .../Utils/BrowserStorageKeys.cs | 1 + src/Aspire.Dashboard/Utils/FormatHelpers.cs | 32 +++++++++ .../FormatHelpersTests.cs | 68 +++++++++++++++++++ 22 files changed, 470 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor index 3ee9dd98595..d4b99467754 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor @@ -32,6 +32,17 @@

@Loc[nameof(Dialogs.SettingsDialogLanguagePageReloads)]

+
+ +
+
@Loc[nameof(Dialogs.SettingsDialogDashboardLogsAndTelemetry)]
diff --git a/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs index 462ab86dcb7..ae71402a233 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs @@ -15,6 +15,7 @@ public partial class SettingsDialog : IDialogContentComponent, IDisposable private string? _currentSetting; private List _languageOptions = null!; private CultureInfo? _selectedUiCulture; + private TimeFormat _timeFormat; private IDisposable? _themeChangedSubscription; @@ -30,6 +31,12 @@ public partial class SettingsDialog : IDialogContentComponent, IDisposable [Inject] public required DashboardDialogService DialogService { get; init; } + [Inject] + public required BrowserTimeProvider TimeProvider { get; init; } + + [Inject] + public required ILocalStorage LocalStorage { get; init; } + protected override void OnInitialized() { _languageOptions = GlobalizationHelpers.OrderedLocalizedCultures; @@ -39,6 +46,8 @@ protected override void OnInitialized() // Otherwise, Blazor has fallen back to a supported language CultureInfo.CurrentUICulture; + _timeFormat = TimeProvider.TimeFormat; + _currentSetting = ThemeManager.SelectedTheme ?? ThemeManager.ThemeSettingSystem; // Handle value being changed in a different browser window. @@ -99,6 +108,26 @@ private async Task LaunchManageDataAsync() await DialogService.ShowDialogAsync(parameters); } + private async Task OnTimeFormatChanged() + { + TimeProvider.SetTimeFormat(_timeFormat); + await LocalStorage.SetAsync(BrowserStorageKeys.TimeFormat, _timeFormat); + + // Reload the page to ensure all components pick up the new format + var uri = new Uri(NavigationManager.Uri) + .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped); + + NavigationManager.NavigateTo(uri, forceLoad: true); + } + + private string FormatTimeFormatOption(TimeFormat format) => format switch + { + TimeFormat.System => Loc[nameof(Dashboard.Resources.Dialogs.SettingsDialogTimeFormatSystem)], + TimeFormat.TwelveHour => Loc[nameof(Dashboard.Resources.Dialogs.SettingsDialogTimeFormatTwelveHour)], + TimeFormat.TwentyFourHour => Loc[nameof(Dashboard.Resources.Dialogs.SettingsDialogTimeFormatTwentyFourHour)], + _ => format.ToString() + }; + public void Dispose() { _themeChangedSubscription?.Dispose(); diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs index 6c1596b2f27..3b1f1e06c7d 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs @@ -114,6 +114,12 @@ protected override async Task OnInitializedAsync() TimeProvider.SetBrowserTimeZone(result.TimeZone); TelemetryContextProvider.SetBrowserUserAgent(result.UserAgent); + var timeFormatResult = await LocalStorage.GetAsync(BrowserStorageKeys.TimeFormat); + if (timeFormatResult.Success) + { + TimeProvider.SetTimeFormat(timeFormatResult.Value); + } + await DisplayUnsecuredEndpointsMessageAsync(); _aiDisplayChangedSubscription = AIContextProvider.OnDisplayChanged(() => InvokeAsync(StateHasChanged)); diff --git a/src/Aspire.Dashboard/Model/BrowserTimeProvider.cs b/src/Aspire.Dashboard/Model/BrowserTimeProvider.cs index 3d83dc69a4e..95708109b37 100644 --- a/src/Aspire.Dashboard/Model/BrowserTimeProvider.cs +++ b/src/Aspire.Dashboard/Model/BrowserTimeProvider.cs @@ -36,4 +36,18 @@ public void SetBrowserTimeZone(string? timeZone) _logger.LogDebug("Browser time zone set to '{TimeZone}' with UTC offset {UtcOffset}.", timeZoneInfo.Id, timeZoneInfo.BaseUtcOffset); _browserLocalTimeZone = timeZoneInfo; } + + public TimeFormat TimeFormat { get; private set; } = TimeFormat.System; + + public void SetTimeFormat(TimeFormat timeFormat) + { + TimeFormat = timeFormat; + } +} + +public enum TimeFormat +{ + System, + TwelveHour, + TwentyFourHour } diff --git a/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs b/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs index 0ee1cff92b4..3aaace5c0dd 100644 --- a/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Dialogs.Designer.cs @@ -1319,5 +1319,41 @@ public static string TextVisualizerSelectFormatType { return ResourceManager.GetString("TextVisualizerSelectFormatType", resourceCulture); } } + + /// + /// Looks up a localized string similar to Time Format. + /// + public static string SettingsDialogTimeFormat { + get { + return ResourceManager.GetString("SettingsDialogTimeFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System. + /// + public static string SettingsDialogTimeFormatSystem { + get { + return ResourceManager.GetString("SettingsDialogTimeFormatSystem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 12-Hour. + /// + public static string SettingsDialogTimeFormatTwelveHour { + get { + return ResourceManager.GetString("SettingsDialogTimeFormatTwelveHour", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 24-Hour. + /// + public static string SettingsDialogTimeFormatTwentyFourHour { + get { + return ResourceManager.GetString("SettingsDialogTimeFormatTwentyFourHour", resourceCulture); + } + } } } diff --git a/src/Aspire.Dashboard/Resources/Dialogs.resx b/src/Aspire.Dashboard/Resources/Dialogs.resx index 80a7776df81..09a3caf7858 100644 --- a/src/Aspire.Dashboard/Resources/Dialogs.resx +++ b/src/Aspire.Dashboard/Resources/Dialogs.resx @@ -549,4 +549,16 @@ Resource - \ No newline at end of file + + Time Format + + + System + + + 12-Hour + + + 24-Hour + + diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf index d405f9616c0..9b63ca53105 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.cs.xlf @@ -637,6 +637,26 @@ Motiv + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Verze: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf index b696edbf67d..81cddebbbd9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.de.xlf @@ -637,6 +637,26 @@ Design + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Version: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf index a9ad0f95f72..0127ae379bc 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.es.xlf @@ -637,6 +637,26 @@ Tema + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Versión: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf index 70cc79009dd..62452687af1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.fr.xlf @@ -637,6 +637,26 @@ Thème + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Version : {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf index 96b22cb7817..38577055853 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.it.xlf @@ -637,6 +637,26 @@ Tema + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Versione: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf index c2cb7186279..629006a204b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ja.xlf @@ -637,6 +637,26 @@ テーマ + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} バージョン: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf index 0039de13b5f..ce5eb0cb86f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ko.xlf @@ -637,6 +637,26 @@ 테마 + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} 버전: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf index 3bfdefa21bb..65c51881558 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pl.xlf @@ -637,6 +637,26 @@ Motyw + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Wersja: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf index 4c7da7dc54b..1d2626fa9ad 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.pt-BR.xlf @@ -637,6 +637,26 @@ Tema + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Versão: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf index c04f210898e..e72914db7b9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.ru.xlf @@ -637,6 +637,26 @@ Тема + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Версия: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf index 49099dbd949..51a150fc19d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.tr.xlf @@ -637,6 +637,26 @@ Tema + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} Sürüm: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf index 97ee9fa4326..022ef1b2187 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hans.xlf @@ -637,6 +637,26 @@ 主题 + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} 版本: {0} diff --git a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf index d70ecedcfbd..80e2940b02e 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Dialogs.zh-Hant.xlf @@ -637,6 +637,26 @@ 佈景主題 + + Time Format + Time Format + + + + System + System + + + + 12-Hour + 12-Hour + + + + 24-Hour + 24-Hour + + Version: {0} 版本: {0} diff --git a/src/Aspire.Dashboard/Utils/BrowserStorageKeys.cs b/src/Aspire.Dashboard/Utils/BrowserStorageKeys.cs index 640a0a82e7f..1be06837add 100644 --- a/src/Aspire.Dashboard/Utils/BrowserStorageKeys.cs +++ b/src/Aspire.Dashboard/Utils/BrowserStorageKeys.cs @@ -10,6 +10,7 @@ internal static class BrowserStorageKeys { public const string UnsecuredTelemetryMessageDismissedKey = "Aspire_Telemetry_UnsecuredMessageDismissed"; public const string UnsecuredEndpointMessageDismissedKey = "Aspire_Security_UnsecuredEndpointMessageDismissed"; + public const string TimeFormat = "Aspire_TimeFormat"; public const string TracesPageState = "Aspire_PageState_Traces"; public const string StructuredLogsPageState = "Aspire_PageState_StructuredLogs"; diff --git a/src/Aspire.Dashboard/Utils/FormatHelpers.cs b/src/Aspire.Dashboard/Utils/FormatHelpers.cs index ee3c313492d..d81fc4db5c9 100644 --- a/src/Aspire.Dashboard/Utils/FormatHelpers.cs +++ b/src/Aspire.Dashboard/Utils/FormatHelpers.cs @@ -81,6 +81,11 @@ public static string FormatTime(BrowserTimeProvider timeProvider, DateTime value cultureInfo ??= CultureInfo.CurrentCulture; var local = timeProvider.ToLocal(value); + if (timeProvider.TimeFormat != TimeFormat.System) + { + return local.ToString(GetForcedTimePattern(timeProvider.TimeFormat, millisecondsDisplay, cultureInfo), cultureInfo); + } + // Long time return millisecondsDisplay switch { @@ -96,6 +101,12 @@ public static string FormatDateTime(BrowserTimeProvider timeProvider, DateTime v cultureInfo ??= CultureInfo.CurrentCulture; var local = timeProvider.ToLocal(value); + if (timeProvider.TimeFormat != TimeFormat.System) + { + var timePattern = GetForcedTimePattern(timeProvider.TimeFormat, millisecondsDisplay, cultureInfo); + return local.ToString($"{cultureInfo.DateTimeFormat.ShortDatePattern} {timePattern}", cultureInfo); + } + // Short date, long time return millisecondsDisplay switch { @@ -158,4 +169,25 @@ public static string CombineWithSeparator(string separator, params string?[] par { return string.Join(separator, parts.Where(p => !string.IsNullOrEmpty(p))); } + + private static string GetForcedTimePattern(TimeFormat timeFormat, MillisecondsDisplay millisecondsDisplay, CultureInfo cultureInfo) + { + var timeSeparator = cultureInfo.DateTimeFormat.TimeSeparator; + var decimalSeparator = cultureInfo.NumberFormat.NumberDecimalSeparator; + + var secondsPattern = millisecondsDisplay switch + { + MillisecondsDisplay.None => "ss", + MillisecondsDisplay.Truncated => $"ss'{decimalSeparator}'fff", + MillisecondsDisplay.Full => $"ss'{decimalSeparator}'FFFFFFF", + _ => throw new NotImplementedException() + }; + + return timeFormat switch + { + TimeFormat.TwelveHour => $"h'{timeSeparator}'mm'{timeSeparator}'{secondsPattern} tt", + TimeFormat.TwentyFourHour => $"H'{timeSeparator}'mm'{timeSeparator}'{secondsPattern}", + _ => throw new NotImplementedException() + }; + } } diff --git a/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs b/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs index 0851d1f6aa1..8db56df0f44 100644 --- a/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs +++ b/tests/Aspire.Dashboard.Tests/FormatHelpersTests.cs @@ -100,6 +100,74 @@ private static DateTime GetLocalDateTime(string value) return date; } + [Theory] + [InlineData("2009-06-15T13:45:30.0000000Z", TimeFormat.TwelveHour, "1:45:30 PM")] + [InlineData("2009-06-15T13:45:30.0000000Z", TimeFormat.TwentyFourHour, "13:45:30")] + public void FormatTime_WithTimeFormatPreference(string value, TimeFormat format, string expected) + { + var date = GetLocalDateTime(value); + var provider = CreateTimeProvider(); + provider.SetTimeFormat(format); + + // Use a culture that would normally be opposite + var culture = format == TimeFormat.TwelveHour ? CultureInfo.GetCultureInfo("de-DE") : CultureInfo.GetCultureInfo("en-US"); + + Assert.Equal(expected, FormatHelpers.FormatTime(provider, date, MillisecondsDisplay.None, culture)); + } + + [Theory] + [InlineData("2009-06-15T13:45:30.0000000Z", TimeFormat.TwelveHour, "6/15/2009 1:45:30 PM")] // en-US date pattern + 12h time + [InlineData("2009-06-15T13:45:30.0000000Z", TimeFormat.TwentyFourHour, "6/15/2009 13:45:30")] // en-US date pattern + 24h time + public void FormatDateTime_WithTimeFormatPreference_EnUS(string value, TimeFormat format, string expected) + { + var date = GetLocalDateTime(value); + var provider = CreateTimeProvider(); + provider.SetTimeFormat(format); + + Assert.Equal(expected, FormatHelpers.FormatDateTime(provider, date, MillisecondsDisplay.None, CultureInfo.GetCultureInfo("en-US"))); + } + + [Theory] + [InlineData("fi-FI", TimeFormat.TwentyFourHour, MillisecondsDisplay.None, "15.6.2009 13.45.30")] + [InlineData("fi-FI", TimeFormat.TwentyFourHour, MillisecondsDisplay.Truncated, "15.6.2009 13.45.30,123")] + [InlineData("de-DE", TimeFormat.TwentyFourHour, MillisecondsDisplay.Truncated, "15.06.2009 13:45:30,123")] + [InlineData("en-US", TimeFormat.TwelveHour, MillisecondsDisplay.Truncated, "6/15/2009 1:45:30.123 PM")] + public void FormatDateTime_WithTimeFormatPreference_UsesCultureSeparators(string cultureName, TimeFormat format, MillisecondsDisplay includeMilliseconds, string expected) + { + var date = GetLocalDateTime("2009-06-15T13:45:30.1234567Z"); + var provider = CreateTimeProvider(); + provider.SetTimeFormat(format); + + Assert.Equal(expected, FormatHelpers.FormatDateTime(provider, date, includeMilliseconds, CultureInfo.GetCultureInfo(cultureName))); + } + + [Fact] + public void FormatTime_TwelveHour_UsesAmPm_EvenIfCultureIs24Hour() + { + // en-GB is typically 24-hour. + var date = GetLocalDateTime("2009-06-15T13:45:30.0000000Z"); + var provider = CreateTimeProvider(); + provider.SetTimeFormat(TimeFormat.TwelveHour); + + // en-GB has AM/PM designators "am"/"pm" even if standard pattern is 24h. + var result = FormatHelpers.FormatTime(provider, date, MillisecondsDisplay.None, CultureInfo.GetCultureInfo("en-GB")); + Assert.Contains("pm", result.ToLowerInvariant()); + Assert.DoesNotContain("13", result); + } + + [Fact] + public void FormatTime_TwentyFourHour_Uses13_EvenIfCultureIs12Hour() + { + // en-US is typically 12-hour. + var date = GetLocalDateTime("2009-06-15T13:45:30.0000000Z"); + var provider = CreateTimeProvider(); + provider.SetTimeFormat(TimeFormat.TwentyFourHour); + + var result = FormatHelpers.FormatTime(provider, date, MillisecondsDisplay.None, CultureInfo.GetCultureInfo("en-US")); + Assert.Contains("13", result); + Assert.DoesNotContain("PM", result); + } + private static BrowserTimeProvider CreateTimeProvider() { return new BrowserTimeProvider(NullLoggerFactory.Instance);