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); + } + } +}