diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs
index ac10cc060..5d496963d 100644
--- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs
+++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs
@@ -2,6 +2,7 @@ using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
+using Dalamud.Utility;
using ImGuiNET;
@@ -288,8 +289,8 @@ internal sealed partial class ActiveNotification
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(
ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)
- ? this.CreatedAt.FormatAbsoluteDateTime()
- : this.CreatedAt.FormatRelativeDateTime());
+ ? this.CreatedAt.LocAbsolute()
+ : this.CreatedAt.LocRelativePastLong());
ImGui.PopStyleColor();
ImGui.PopStyleVar();
}
@@ -304,7 +305,7 @@ internal sealed partial class ActiveNotification
ltOffset.X = height;
- var agoText = this.CreatedAt.FormatRelativeDateTimeShort();
+ var agoText = this.CreatedAt.LocRelativePastShort();
var agoSize = ImGui.CalcTextSize(agoText);
rtOffset.X -= agoSize.X;
ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding });
diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs
index f88eac53a..50536baa3 100644
--- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs
+++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs
@@ -1,4 +1,3 @@
-using System.Diagnostics;
using System.Numerics;
using Dalamud.Interface.Colors;
@@ -91,31 +90,6 @@ internal static class NotificationConstants
/// Color for the background progress bar (determinate progress only).
public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f);
- /// Gets the relative time format strings.
- private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings =
- {
- (TimeSpan.FromDays(7), null),
- (TimeSpan.FromDays(2), "{0:%d} days ago"),
- (TimeSpan.FromDays(1), "yesterday"),
- (TimeSpan.FromHours(2), "{0:%h} hours ago"),
- (TimeSpan.FromHours(1), "an hour ago"),
- (TimeSpan.FromMinutes(2), "{0:%m} minutes ago"),
- (TimeSpan.FromMinutes(1), "a minute ago"),
- (TimeSpan.FromSeconds(2), "{0:%s} seconds ago"),
- (TimeSpan.FromSeconds(1), "a second ago"),
- (TimeSpan.MinValue, "just now"),
- };
-
- /// Gets the relative time format strings.
- private static readonly (TimeSpan MinSpan, string FormatString)[] RelativeFormatStringsShort =
- {
- (TimeSpan.FromDays(1), "{0:%d}d"),
- (TimeSpan.FromHours(1), "{0:%h}h"),
- (TimeSpan.FromMinutes(1), "{0:%m}m"),
- (TimeSpan.FromSeconds(1), "{0:%s}s"),
- (TimeSpan.MinValue, "now"),
- };
-
/// Gets the scaled padding of the window (dot(.) in the above diagram).
public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale);
@@ -142,46 +116,6 @@ internal static class NotificationConstants
/// Gets the string format of the initiator name field, if the initiator is unloaded.
public static string UnloadedInitiatorNameFormat => "{0} (unloaded)";
- /// Formats an instance of as a relative time.
- /// When.
- /// The formatted string.
- public static string FormatRelativeDateTime(this DateTime when)
- {
- var ts = DateTime.Now - when;
- foreach (var (minSpan, formatString) in RelativeFormatStrings)
- {
- if (ts < minSpan)
- continue;
- if (formatString is null)
- break;
- return string.Format(formatString, ts);
- }
-
- return when.FormatAbsoluteDateTime();
- }
-
- /// Formats an instance of as an absolute time.
- /// When.
- /// The formatted string.
- public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}";
-
- /// Formats an instance of as a relative time.
- /// When.
- /// The formatted string.
- public static string FormatRelativeDateTimeShort(this DateTime when)
- {
- var ts = DateTime.Now - when;
- foreach (var (minSpan, formatString) in RelativeFormatStringsShort)
- {
- if (ts < minSpan)
- continue;
- return string.Format(formatString, ts);
- }
-
- Debug.Assert(false, "must not reach here");
- return "???";
- }
-
/// Gets the color corresponding to the notification type.
/// The notification type.
/// The corresponding color.
diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs
index b180f113a..39312ac52 100644
--- a/Dalamud/Localization.cs
+++ b/Dalamud/Localization.cs
@@ -36,6 +36,7 @@ public class Localization : IServiceType
/// Use embedded loc resource files.
public Localization(string locResourceDirectory, string locResourcePrefix = "", bool useEmbedded = false)
{
+ this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture;
this.locResourceDirectory = locResourceDirectory;
this.locResourcePrefix = locResourcePrefix;
this.useEmbedded = useEmbedded;
@@ -61,7 +62,24 @@ public class Localization : IServiceType
///
/// Event that occurs when the language is changed.
///
- public event LocalizationChangedDelegate LocalizationChanged;
+ public event LocalizationChangedDelegate? LocalizationChanged;
+
+ ///
+ /// Gets an instance of that corresponds to the language configured from Dalamud Settings.
+ ///
+ public CultureInfo DalamudLanguageCultureInfo { get; private set; }
+
+ ///
+ /// Gets an instance of that corresponds to .
+ ///
+ /// The language code which should be in .
+ /// The corresponding instance of .
+ public static CultureInfo GetCultureInfoFromLangCode(string langCode) =>
+ CultureInfo.GetCultureInfo(langCode switch
+ {
+ "tw" => "zh-tw",
+ _ => langCode,
+ });
///
/// Search the set-up localization data for the provided assembly for the given string key and return it.
@@ -108,6 +126,7 @@ public class Localization : IServiceType
///
public void SetupWithFallbacks()
{
+ this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture;
this.LocalizationChanged?.Invoke(FallbackLangCode);
Loc.SetupWithFallbacks(this.assembly);
}
@@ -124,6 +143,7 @@ public class Localization : IServiceType
return;
}
+ this.DalamudLanguageCultureInfo = GetCultureInfoFromLangCode(langCode);
this.LocalizationChanged?.Invoke(langCode);
try
diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs
new file mode 100644
index 000000000..8f6a2a7ec
--- /dev/null
+++ b/Dalamud/Utility/DateTimeSpanExtensions.cs
@@ -0,0 +1,129 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+
+using CheapLoc;
+
+using Dalamud.Logging.Internal;
+
+namespace Dalamud.Utility;
+
+///
+/// Utility functions for and .
+///
+internal static class DateTimeSpanExtensions
+{
+ private static readonly ModuleLog Log = new(nameof(DateTimeSpanExtensions));
+
+ private static ParsedRelativeFormatStrings? relativeFormatStringLong;
+
+ private static ParsedRelativeFormatStrings? relativeFormatStringShort;
+
+ /// Formats an instance of as a localized absolute time.
+ /// When.
+ /// The formatted string.
+ /// The string will be formatted according to Square Enix Account region settings, if Dalamud default
+ /// language is English.
+ public static unsafe string LocAbsolute(this DateTime when)
+ {
+ var culture = Service.GetNullable()?.DalamudLanguageCultureInfo ?? CultureInfo.InvariantCulture;
+ if (!Equals(culture, CultureInfo.InvariantCulture))
+ return when.ToString("G", culture);
+
+ var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
+ var region = 0;
+ if (framework is not null)
+ region = framework->Region;
+ switch (region)
+ {
+ case 0: // jp
+ default:
+ return when.ToString("yyyy-MM-dd HH:mm:ss");
+ case 1: // na
+ return when.ToString("MM/dd/yyyy HH:mm:ss");
+ case 2: // eu
+ return when.ToString("dd-mm-yyyy HH:mm:ss");
+ }
+ }
+
+ /// Formats an instance of as a localized relative time.
+ /// When.
+ /// The formatted string.
+ public static string LocRelativePastLong(this DateTime when)
+ {
+ var loc = Loc.Localize(
+ "DateTimeSpanExtensions.RelativeFormatStringsLong",
+ "172800,{0:%d} days ago\n86400,yesterday\n7200,{0:%h} hours ago\n3600,an hour ago\n120,{0:%m} minutes ago\n60,a minute ago\n2,{0:%s} seconds ago\n1,a second ago\n-Infinity,just now");
+ Debug.Assert(loc != null, "loc != null");
+
+ if (relativeFormatStringLong?.FormatStringLoc != loc)
+ relativeFormatStringLong ??= new(loc);
+
+ return relativeFormatStringLong.Format(DateTime.Now - when);
+ }
+
+ /// Formats an instance of as a localized relative time.
+ /// When.
+ /// The formatted string.
+ public static string LocRelativePastShort(this DateTime when)
+ {
+ var loc = Loc.Localize(
+ "DateTimeSpanExtensions.RelativeFormatStringsShort",
+ "86400,{0:%d}d\n3600,{0:%h}h\n60,{0:%m}m\n1,{0:%s}s\n-Infinity,now");
+ Debug.Assert(loc != null, "loc != null");
+
+ if (relativeFormatStringShort?.FormatStringLoc != loc)
+ relativeFormatStringShort = new(loc);
+
+ return relativeFormatStringShort.Format(DateTime.Now - when);
+ }
+
+ private sealed class ParsedRelativeFormatStrings
+ {
+ private readonly List<(float MinSeconds, string FormatString)> formatStrings = new();
+
+ public ParsedRelativeFormatStrings(string value)
+ {
+ this.FormatStringLoc = value;
+ foreach (var line in value.Split("\n"))
+ {
+ var sep = line.IndexOf(',');
+ if (sep < 0)
+ {
+ Log.Error("A line without comma has been found: {line}", line);
+ continue;
+ }
+
+ if (!float.TryParse(
+ line.AsSpan(0, sep),
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out var seconds))
+ {
+ Log.Error("Could not parse the duration: {line}", line);
+ continue;
+ }
+
+ this.formatStrings.Add((seconds, line[(sep + 1)..]));
+ }
+
+ this.formatStrings.Sort((a, b) => b.MinSeconds.CompareTo(a.MinSeconds));
+ }
+
+ public string FormatStringLoc { get; }
+
+ /// Formats an instance of as a localized string.
+ /// The duration.
+ /// The formatted string.
+ public string Format(TimeSpan ts)
+ {
+ foreach (var (minSeconds, formatString) in this.formatStrings)
+ {
+ if (ts.TotalSeconds >= minSeconds)
+ return string.Format(formatString, ts);
+ }
+
+ return this.formatStrings[^1].FormatString.Format(ts);
+ }
+ }
+}