diff --git a/DesktopClock.Tests/FormatPresetsTests.cs b/DesktopClock.Tests/FormatPresetsTests.cs
new file mode 100644
index 0000000..8906d71
--- /dev/null
+++ b/DesktopClock.Tests/FormatPresetsTests.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace DesktopClock.Tests;
+
+[UseUICulture("en-US")]
+public class FormatPresetsTests
+{
+ private static DateTimeFormatInfo Region(string culture) => CultureInfo.GetCultureInfo(culture).DateTimeFormat;
+
+ [Fact]
+ public void ForClock_KeepsTheUsFormats()
+ {
+ var presets = FormatPresets.ForClock(Region("en-US")).ToDictionary(p => p.Name, p => p.Format);
+
+ Assert.Equal("{h:mm tt}", presets["Time"]);
+ Assert.Equal("{HH:mm}", presets["Time, 24-hour"]);
+ Assert.Equal("{h:mm:ss tt}", presets["Time with seconds"]);
+ Assert.Equal("{ddd}, {MMM d}, {h:mm tt}", presets["Date and time"]);
+ Assert.Equal("{dddd}, {MMMM d}", presets["Date only"]);
+ }
+
+ [Fact]
+ public void ForClock_FollowsRegionalTimeAndDateOrder()
+ {
+ var german = FormatPresets.ForClock(Region("de-DE")).ToDictionary(p => p.Name, p => p.Format);
+ Assert.Equal("{HH:mm}", german["Time"]);
+ Assert.Equal("{dddd}, {d. MMMM}", german["Date only"]);
+
+ var chinese = FormatPresets.ForClock(Region("zh-CN")).ToDictionary(p => p.Name, p => p.Format);
+ Assert.Equal("{dddd}, {M月d日}", chinese["Date only"]);
+ }
+
+ [Theory]
+ [InlineData("en-US", true)]
+ [InlineData("ko-KR", true)]
+ [InlineData("de-DE", false)]
+ [InlineData("ja-JP", false)]
+ [InlineData("fr-CA", false)]
+ public void ForClock_OnlyOffersA24HourPresetWhereTheRegionUses12Hours(string culture, bool expected)
+ {
+ Assert.Equal(expected, FormatPresets.Uses12HourClock(Region(culture)));
+ Assert.Equal(expected, FormatPresets.ForClock(Region(culture)).Any(p => p.Name == "Time, 24-hour"));
+ }
+
+ [Fact]
+ public void DefaultClockFormat_FollowsTheRegion()
+ {
+ Assert.Equal("{ddd}, {MMM d}, {h:mm:ss tt}", FormatPresets.DefaultClockFormat(Region("en-US")));
+ Assert.Equal("{ddd}, {d. MMM}, {HH:mm:ss}", FormatPresets.DefaultClockFormat(Region("de-DE")));
+ }
+
+ [Fact]
+ public void AllPresets_RenderInEveryRegion()
+ {
+ var now = new DateTimeOffset(2026, 9, 22, 13, 1, 22, TimeSpan.Zero);
+
+ foreach (var culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
+ {
+ var formats = FormatPresets.ForClock(culture.DateTimeFormat).Select(p => p.Format).Append(FormatPresets.DefaultClockFormat(culture.DateTimeFormat));
+
+ foreach (var format in formats)
+ Assert.True(Tokenizer.FormatWithTokenizerOrFallBack(now, format, culture) != Tokenizer.FormatErrorMessage, $"{culture.Name}: {format}");
+ }
+ }
+}
diff --git a/DesktopClock/CountdownTargetEditor.xaml.cs b/DesktopClock/CountdownTargetEditor.xaml.cs
index e24a610..0a7559b 100644
--- a/DesktopClock/CountdownTargetEditor.xaml.cs
+++ b/DesktopClock/CountdownTargetEditor.xaml.cs
@@ -68,7 +68,7 @@ private void BuildPresetButtons()
var example = new TextBlock
{
- Text = $"{getTarget():ddd, MMM d} {getTarget():t}",
+ Text = getTarget().ToString($"ddd, {FormatPresets.ShortMonthDayPattern(CultureInfo.CurrentCulture.DateTimeFormat)} {CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern}"),
FontSize = 12,
TextTrimming = TextTrimming.CharacterEllipsis,
};
diff --git a/DesktopClock/FormatEditor.xaml.cs b/DesktopClock/FormatEditor.xaml.cs
index e0a072e..a092cf1 100644
--- a/DesktopClock/FormatEditor.xaml.cs
+++ b/DesktopClock/FormatEditor.xaml.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
@@ -23,18 +24,7 @@ public enum FormatEditorMode
public partial class FormatEditor : UserControl
{
// Presets cover the scenarios users most commonly ask for; the raw box stays the escape hatch.
- private static readonly (string Name, string Format)[] ClockPresets =
- {
- (Loc.Get("ClockPresetTime"), "{h:mm tt}"),
- (Loc.Get("ClockPresetTime24"), "{HH:mm}"),
- (Loc.Get("ClockPresetTimeSeconds"), "{h:mm:ss tt}"),
- (Loc.Get("ClockPresetDayTime"), "{ddd}, {h:mm tt}"),
- (Loc.Get("ClockPresetDateTime"), "{ddd}, {MMM dd}, {h:mm tt}"),
- (Loc.Get("ClockPresetFullDateTime"), "{dddd}, {MMMM dd}, {h:mm tt}"),
- (Loc.Get("ClockPresetDateOnly"), "{dddd}, {MMMM dd}"),
- (Loc.Get("ClockPresetSortable"), "{yyyy-MM-dd} {HH:mm}"),
- (Loc.Get("ClockPresetIsoWeek"), "{weekYear}-W{week}"),
- };
+ private static readonly IReadOnlyList<(string Name, string Format)> ClockPresets = FormatPresets.ForClock(CultureInfo.CurrentCulture.DateTimeFormat);
private static readonly (string Name, string Format)[] CountdownPresets =
{
@@ -55,7 +45,7 @@ private static readonly (string Name, string Token)[] ClockTokens =
(Loc.Get("TokenMonth"), "{MMM}"),
(Loc.Get("TokenMonthFull"), "{MMMM}"),
(Loc.Get("TokenYear"), "{yyyy}"),
- (Loc.Get("TokenTime"), "{h:mm tt}"),
+ (Loc.Get("TokenTime"), "{" + CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern + "}"),
(Loc.Get("TokenTime24"), "{HH:mm}"),
(Loc.Get("TokenSeconds"), "{ss}"),
(Loc.Get("TokenWeekNumber"), "{week}"),
diff --git a/DesktopClock/Properties/Settings.cs b/DesktopClock/Properties/Settings.cs
index a24b6e5..8c816e7 100644
--- a/DesktopClock/Properties/Settings.cs
+++ b/DesktopClock/Properties/Settings.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media;
@@ -117,7 +118,7 @@ public static void TrySaveIfLoaded()
/// Change it when you want a different arrangement, such as adding the weekday, seconds, or a shorter date.
/// See: Custom date and time format strings.
///
- public string Format { get; set; } = "{ddd}, {MMM dd}, {h:mm:ss tt}";
+ public string Format { get; set; } = FormatPresets.DefaultClockFormat(CultureInfo.CurrentCulture.DateTimeFormat);
///
/// Format string used while is enabled.
diff --git a/DesktopClock/Utilities/FormatPresets.cs b/DesktopClock/Utilities/FormatPresets.cs
new file mode 100644
index 0000000..c35bfd3
--- /dev/null
+++ b/DesktopClock/Utilities/FormatPresets.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace DesktopClock;
+
+///
+/// Clock formats built from the Windows regional format, so times are 24-hour where people expect it and dates follow the local day and month order.
+///
+public static class FormatPresets
+{
+ ///
+ /// The clock's default format: short weekday, short date, and time with seconds, such as "Tue, Sep 22, 1:01:22 PM" in the US or "Di., 22. Sep., 13:01:22" in Germany.
+ ///
+ public static string DefaultClockFormat(DateTimeFormatInfo format) =>
+ $"{{ddd}}, {Token(ShortMonthDayPattern(format))}, {Token(format.LongTimePattern)}";
+
+ ///
+ /// One-click clock formats for common scenarios.
+ ///
+ public static IReadOnlyList<(string Name, string Format)> ForClock(DateTimeFormatInfo format)
+ {
+ var time = Token(format.ShortTimePattern);
+ var monthDay = Token(format.MonthDayPattern);
+ var presets = new List<(string Name, string Format)> { (Loc.Get("ClockPresetTime"), time) };
+
+ // A 24-hour preset only adds something where the regional time is 12-hour.
+ if (Uses12HourClock(format))
+ presets.Add((Loc.Get("ClockPresetTime24"), "{HH:mm}"));
+
+ presets.Add((Loc.Get("ClockPresetTimeSeconds"), Token(format.LongTimePattern)));
+ presets.Add((Loc.Get("ClockPresetDayTime"), $"{{ddd}}, {time}"));
+ presets.Add((Loc.Get("ClockPresetDateTime"), $"{{ddd}}, {Token(ShortMonthDayPattern(format))}, {time}"));
+ presets.Add((Loc.Get("ClockPresetFullDateTime"), $"{{dddd}}, {monthDay}, {time}"));
+ presets.Add((Loc.Get("ClockPresetDateOnly"), $"{{dddd}}, {monthDay}"));
+ presets.Add((Loc.Get("ClockPresetSortable"), "{yyyy-MM-dd} {HH:mm}"));
+ presets.Add((Loc.Get("ClockPresetIsoWeek"), "{weekYear}-W{week}"));
+ return presets;
+ }
+
+ ///
+ /// The regional month and day with an abbreviated month, such as "MMM d" in the US or "d. MMM" in Germany.
+ ///
+ public static string ShortMonthDayPattern(DateTimeFormatInfo format) => format.MonthDayPattern.Replace("MMMM", "MMM");
+
+ ///
+ /// Whether the regional time format uses a 12-hour clock. Quoted text is skipped so a literal like the "h" in Canadian French "HH 'h' mm" doesn't count.
+ ///
+ public static bool Uses12HourClock(DateTimeFormatInfo format) => Regex.Replace(format.ShortTimePattern, "'[^']*'", "").Contains("h");
+
+ private static string Token(string pattern) => "{" + pattern + "}";
+}