diff --git a/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs b/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs
index 4e4692d..1e420c7 100644
--- a/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs
+++ b/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs
@@ -2,6 +2,7 @@ using Marathon.Application.Abstractions;
using Marathon.Application.Configuration;
using Marathon.Domain.AnomalyDetection;
using Marathon.Domain.Entities;
+using Marathon.Domain.ValueObjects;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -24,7 +25,6 @@ namespace Marathon.Application.UseCases;
///
public sealed class DetectAnomaliesUseCase
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
private static readonly TimeSpan SnapshotLookback = TimeSpan.FromHours(24);
// Dedup window: two anomalies for the same event within this window are considered duplicates.
@@ -67,7 +67,7 @@ public sealed class DetectAnomaliesUseCase
var events = await _eventRepo.ListAsync(ct);
int newAnomalyCount = 0;
- var now = DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
+ var now = MoscowTime.Now;
var from = now - SnapshotLookback;
// Hoisted outside the per-event loop: load existing anomalies ONCE per cycle
diff --git a/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs b/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs
index 810f541..e570584 100644
--- a/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs
+++ b/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs
@@ -27,8 +27,6 @@ namespace Marathon.Domain.AnomalyDetection;
///
public sealed class AnomalyDetector
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
private readonly int _suspensionGapSeconds;
private readonly decimal _oddsFlipThreshold;
private readonly int _minSnapshotCount;
@@ -156,7 +154,7 @@ public sealed class AnomalyDetector
return new Anomaly(
Id: Guid.NewGuid(),
EventId: eventId,
- DetectedAt: DateTimeOffset.UtcNow.ToOffset(MoscowOffset),
+ DetectedAt: MoscowTime.Now,
Kind: AnomalyKind.SuspensionFlip,
Score: clampedScore,
EvidenceJson: evidenceJson);
diff --git a/src/Marathon.Domain/Entities/Event.cs b/src/Marathon.Domain/Entities/Event.cs
index 5d20b14..f1ac593 100644
--- a/src/Marathon.Domain/Entities/Event.cs
+++ b/src/Marathon.Domain/Entities/Event.cs
@@ -21,8 +21,6 @@ public sealed record Event(
string Side1Name,
string Side2Name)
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
public EventId Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id));
public SportCode Sport { get; } = Sport ?? throw new ArgumentNullException(nameof(Sport));
@@ -37,7 +35,7 @@ public sealed record Event(
public string Category { get; } = Category ?? string.Empty;
- public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowOffset
+ public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowTime.Offset
? ScheduledAt
: throw new ArgumentException(
$"ScheduledAt must be in Europe/Moscow time (UTC+03:00). " +
diff --git a/src/Marathon.Domain/ValueObjects/MoscowTime.cs b/src/Marathon.Domain/ValueObjects/MoscowTime.cs
new file mode 100644
index 0000000..a2fb63c
--- /dev/null
+++ b/src/Marathon.Domain/ValueObjects/MoscowTime.cs
@@ -0,0 +1,32 @@
+namespace Marathon.Domain.ValueObjects;
+
+///
+/// Single source of truth for the Moscow timezone offset.
+///
+///
+///
+/// marathonbet.by serves all timestamps in Moscow time (UTC+3) and the domain
+/// invariant on
+/// rejects any other offset. Code that constructs
+/// values for events, results, snapshots, or test fixtures MUST use this
+/// constant rather than re-deriving TimeSpan.FromHours(3).
+///
+///
+public static class MoscowTime
+{
+ /// The Moscow time offset (UTC+3).
+ public static readonly TimeSpan Offset = TimeSpan.FromHours(3);
+
+ /// Current Moscow time.
+ public static DateTimeOffset Now => DateTimeOffset.UtcNow.ToOffset(Offset);
+
+ ///
+ /// Returns the inclusive end-of-day for the given Moscow date — i.e.,
+ /// the moment one second before the next day starts. Used by date-range
+ /// filters where the user picks "to: 2026-05-09" meaning "through the
+ /// rest of that day."
+ ///
+ public static DateTimeOffset EndOfMoscowDay(DateOnly date) =>
+ new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue), Offset)
+ .AddDays(1).AddSeconds(-1);
+}
diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs b/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs
index e2d718d..86e5116 100644
--- a/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs
+++ b/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs
@@ -17,8 +17,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
///
public abstract class EventListingParserBase
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
protected readonly IServerTimeProvider ServerTimeProvider;
protected readonly ILogger Logger;
@@ -36,7 +34,7 @@ public abstract class EventListingParserBase
CancellationToken ct)
{
var serverTime = ServerTimeProvider.ExtractServerTime(html)
- ?? DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
+ ?? MoscowTime.Now;
var config = AngleSharpConfig.Default;
using var context = BrowsingContext.New(config);
diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs
index 580c046..2fae14f 100644
--- a/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs
+++ b/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs
@@ -18,8 +18,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
///
public sealed partial class EventOddsParser : IEventOddsParser
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
private readonly IServerTimeProvider _serverTime;
private readonly PeriodScopeMapper _periodMapper;
private readonly ILogger _logger;
@@ -69,7 +67,7 @@ public sealed partial class EventOddsParser : IEventOddsParser
ArgumentNullException.ThrowIfNull(html);
var capturedAt = _serverTime.ExtractServerTime(html)
- ?? DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
+ ?? MoscowTime.Now;
var config = AngleSharpConfig.Default;
using var context = BrowsingContext.New(config);
diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs
index 3626ebf..0636281 100644
--- a/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs
+++ b/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text.RegularExpressions;
+using Marathon.Domain.ValueObjects;
namespace Marathon.Infrastructure.Scraping.Parsers;
@@ -13,8 +14,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
///
public static partial class MoscowDateParser
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
// Matches "HH:MM"
[GeneratedRegex(@"^\s*(\d{1,2}):(\d{2})\s*$", RegexOptions.CultureInvariant)]
private static partial Regex TimeOnlyRegex();
@@ -70,7 +69,7 @@ public static partial class MoscowDateParser
var scheduled = new DateTimeOffset(
today.Year, today.Month, today.Day,
hour, minute, 0,
- MoscowOffset);
+ MoscowTime.Offset);
// If the computed time is already in the past (same day but earlier),
// that's fine — the event may have already started (live) or the listing
@@ -98,7 +97,7 @@ public static partial class MoscowDateParser
if (candidate < DateOnly.FromDateTime(anchorDate))
year++; // e.g., anchor is Dec 2026 and event is in Jan 2027
- return new DateTimeOffset(year, month, day, hour, minute, 0, MoscowOffset);
+ return new DateTimeOffset(year, month, day, hour, minute, 0, MoscowTime.Offset);
}
return null;
diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs
index 1961bc4..8f221c4 100644
--- a/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs
+++ b/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs
@@ -21,8 +21,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
///
public sealed partial class ResultsParser : IResultsParser
{
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
private readonly ILogger _logger;
// Matches score patterns like "2:1", "2:1 (1:1)", "2:1 (0:0) (2:1)"
@@ -101,7 +99,7 @@ public sealed partial class ResultsParser : IResultsParser
if (string.IsNullOrWhiteSpace(eventIdRaw))
return null;
- var completedAt = DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
+ var completedAt = MoscowTime.Now;
return new EventResult(
new DomainEventId(eventIdRaw),
diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs b/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs
index 6006086..75f9e9d 100644
--- a/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs
+++ b/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
+using Marathon.Domain.ValueObjects;
using Microsoft.Extensions.Logging;
namespace Marathon.Infrastructure.Scraping.Parsers;
@@ -17,8 +18,6 @@ public sealed partial class ServerTimeProvider : IServerTimeProvider
RegexOptions.CultureInvariant)]
private static partial Regex ServerTimeRegex();
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
private readonly ILogger _logger;
public ServerTimeProvider(ILogger logger)
@@ -47,6 +46,6 @@ public sealed partial class ServerTimeProvider : IServerTimeProvider
var minute = int.Parse(match.Groups[5].Value);
var second = int.Parse(match.Groups[6].Value);
- return new DateTimeOffset(year, month, day, hour, minute, second, MoscowOffset);
+ return new DateTimeOffset(year, month, day, hour, minute, second, MoscowTime.Offset);
}
}
diff --git a/src/Marathon.UI/Components/AnomalyCard.razor b/src/Marathon.UI/Components/AnomalyCard.razor
index a933d73..91246b0 100644
--- a/src/Marathon.UI/Components/AnomalyCard.razor
+++ b/src/Marathon.UI/Components/AnomalyCard.razor
@@ -215,14 +215,7 @@
_ => kind.ToString(),
};
- private string SportLabel(int code) => code switch
- {
- 6 => L["Sport.Basketball"],
- 11 => L["Sport.Football"],
- 22723 => L["Sport.Tennis"],
- 43658 => L["Sport.Hockey"],
- _ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code),
- };
+ private string SportLabel(int code) => SportLabels.Resolve(L, code);
private static string FormatRate(decimal? r) => r is { } v
? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
diff --git a/src/Marathon.UI/Components/ExportDialog.razor b/src/Marathon.UI/Components/ExportDialog.razor
index 357b39b..34883fe 100644
--- a/src/Marathon.UI/Components/ExportDialog.razor
+++ b/src/Marathon.UI/Components/ExportDialog.razor
@@ -113,7 +113,7 @@
protected override void OnInitialized()
{
- var moscow = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(3));
+ var moscow = MoscowTime.Now;
_from = InitialFrom ?? moscow.AddDays(-1).Date;
_to = InitialTo ?? moscow.AddDays(1).Date;
_kind = InitialKind;
@@ -140,10 +140,9 @@
try
{
// Use Moscow offset to match domain ScheduledAt invariant.
- var moscow = TimeSpan.FromHours(3);
var range = new AppDateRange(
- new DateTimeOffset(_from.Value.Date, moscow),
- new DateTimeOffset(_to.Value.Date.AddDays(1).AddSeconds(-1), moscow));
+ new DateTimeOffset(_from.Value.Date, MoscowTime.Offset),
+ MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(_to.Value.Date)));
var path = await ExportUseCase.ExecuteAsync(range, _kind, CancellationToken.None);
Dialog.Close(DialogResult.Ok(path));
diff --git a/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor b/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor
index e4948e7..0292305 100644
--- a/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor
+++ b/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor
@@ -252,8 +252,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- var moscow = TimeSpan.FromHours(3);
- await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) });
+ await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) });
}
}
@@ -261,8 +260,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- var moscow = TimeSpan.FromHours(3);
- await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) });
+ await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) });
}
}
@@ -283,14 +281,7 @@
_ => L["Anomaly.Severity.Low"],
};
- private string SportLabel(int code) => code switch
- {
- 6 => L["Sport.Basketball"],
- 11 => L["Sport.Football"],
- 22723 => L["Sport.Tennis"],
- 43658 => L["Sport.Hockey"],
- _ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code),
- };
+ private string SportLabel(int code) => SportLabels.Resolve(L, code);
private static string FormatDate(DateTimeOffset? value)
=> value?.ToString("yyyy-MM-dd") ?? string.Empty;
diff --git a/src/Marathon.UI/Pages/Events/Detail.razor b/src/Marathon.UI/Pages/Events/Detail.razor
index 4f25fa6..ed39878 100644
--- a/src/Marathon.UI/Pages/Events/Detail.razor
+++ b/src/Marathon.UI/Pages/Events/Detail.razor
@@ -306,14 +306,7 @@
_ => "—",
};
- private string SportLabel(int code) => code switch
- {
- 6 => L["Sport.Basketball"],
- 11 => L["Sport.Football"],
- 22723 => L["Sport.Tennis"],
- 43658 => L["Sport.Hockey"],
- _ => $"Sport {code}",
- };
+ private string SportLabel(int code) => SportLabels.Resolve(L, code);
private string BetTypeLabel(BetType t) => t switch
{
diff --git a/src/Marathon.UI/Pages/Results/ResultsList.razor b/src/Marathon.UI/Pages/Results/ResultsList.razor
index faa63c0..7d534ee 100644
--- a/src/Marathon.UI/Pages/Results/ResultsList.razor
+++ b/src/Marathon.UI/Pages/Results/ResultsList.razor
@@ -164,8 +164,6 @@
@code {
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
private DateTimeOffset _from;
private DateTimeOffset _to;
private string _searchInput = string.Empty;
@@ -181,7 +179,7 @@
protected override async Task OnInitializedAsync()
{
- var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowOffset);
+ var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset);
_from = todayMoscow.AddDays(-30);
_to = todayMoscow.AddDays(1).AddSeconds(-1);
@@ -246,7 +244,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- _from = new DateTimeOffset(v.Date, MoscowOffset);
+ _from = new DateTimeOffset(v.Date, MoscowTime.Offset);
await LoadAsync();
}
}
@@ -255,7 +253,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- _to = new DateTimeOffset(v.Date, MoscowOffset).AddDays(1).AddSeconds(-1);
+ _to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1);
await LoadAsync();
}
}
@@ -311,14 +309,7 @@
_ => "draw",
};
- private string SportLabel(int code) => code switch
- {
- 6 => L["Sport.Basketball"],
- 11 => L["Sport.Football"],
- 22723 => L["Sport.Tennis"],
- 43658 => L["Sport.Hockey"],
- _ => $"Sport {code}",
- };
+ private string SportLabel(int code) => SportLabels.Resolve(L, code);
public void Dispose()
{
diff --git a/src/Marathon.UI/Pages/Results/ResultsLoader.razor b/src/Marathon.UI/Pages/Results/ResultsLoader.razor
index c707d13..bc30faf 100644
--- a/src/Marathon.UI/Pages/Results/ResultsLoader.razor
+++ b/src/Marathon.UI/Pages/Results/ResultsLoader.razor
@@ -204,8 +204,6 @@
@code {
- private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
-
private DateTimeOffset _from;
private DateTimeOffset _to;
private LoaderMode _mode = LoaderMode.AllInRange;
@@ -234,7 +232,7 @@
protected override async Task OnInitializedAsync()
{
- var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowOffset);
+ var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset);
_from = todayMoscow.AddDays(-7);
_to = todayMoscow.AddDays(1).AddSeconds(-1);
await ReloadCandidatesAsync();
@@ -347,7 +345,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- _from = new DateTimeOffset(v.Date, MoscowOffset);
+ _from = new DateTimeOffset(v.Date, MoscowTime.Offset);
await ReloadCandidatesAsync();
}
}
@@ -356,7 +354,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- _to = new DateTimeOffset(v.Date, MoscowOffset).AddDays(1).AddSeconds(-1);
+ _to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1);
await ReloadCandidatesAsync();
}
}
@@ -400,14 +398,7 @@
_ => o.ToString(),
};
- private string SportLabel(int code) => code switch
- {
- 6 => L["Sport.Basketball"],
- 11 => L["Sport.Football"],
- 22723 => L["Sport.Tennis"],
- 43658 => L["Sport.Hockey"],
- _ => $"Sport {code}",
- };
+ private string SportLabel(int code) => SportLabels.Resolve(L, code);
private static string FormatDate(DateTimeOffset value) => value.ToString("yyyy-MM-dd");
diff --git a/src/Marathon.UI/Pages/Shared/EventListShell.razor b/src/Marathon.UI/Pages/Shared/EventListShell.razor
index 7713e5c..9bffc48 100644
--- a/src/Marathon.UI/Pages/Shared/EventListShell.razor
+++ b/src/Marathon.UI/Pages/Shared/EventListShell.razor
@@ -437,8 +437,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- var moscow = TimeSpan.FromHours(3);
- await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) });
+ await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) });
}
}
@@ -446,8 +445,7 @@
{
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
{
- var moscow = TimeSpan.FromHours(3);
- await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) });
+ await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) });
}
}
@@ -519,14 +517,7 @@
private static string FormatDate(DateTimeOffset value)
=> value.ToString("yyyy-MM-dd");
- private string SportLabel(int code) => code switch
- {
- 6 => L["Sport.Basketball"],
- 11 => L["Sport.Football"],
- 22723 => L["Sport.Tennis"],
- 43658 => L["Sport.Hockey"],
- _ => $"Sport {code}",
- };
+ private string SportLabel(int code) => SportLabels.Resolve(L, code);
private readonly record struct RatesSnap(decimal? Win1, decimal? Draw, decimal? Win2);
diff --git a/src/Marathon.UI/Services/EventBrowsingState.cs b/src/Marathon.UI/Services/EventBrowsingState.cs
index f3f6b00..9e15f5a 100644
--- a/src/Marathon.UI/Services/EventBrowsingState.cs
+++ b/src/Marathon.UI/Services/EventBrowsingState.cs
@@ -1,3 +1,5 @@
+using Marathon.Domain.ValueObjects;
+
namespace Marathon.UI.Services;
///
@@ -54,8 +56,7 @@ public sealed class EventBrowsingState
public static PageFilter Default(DateTime nowUtc)
{
// Default window: -1d .. +7d in Moscow time, the same TZ events use.
- var moscow = TimeSpan.FromHours(3);
- var midnight = new DateTimeOffset(nowUtc.Date, TimeSpan.Zero).ToOffset(moscow);
+ var midnight = new DateTimeOffset(nowUtc.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset);
return new PageFilter(
From: midnight.AddDays(-1),
To: midnight.AddDays(7),
diff --git a/src/Marathon.UI/Services/SportLabels.cs b/src/Marathon.UI/Services/SportLabels.cs
new file mode 100644
index 0000000..3917ffc
--- /dev/null
+++ b/src/Marathon.UI/Services/SportLabels.cs
@@ -0,0 +1,26 @@
+using System.Globalization;
+using Marathon.UI.Resources;
+using Microsoft.Extensions.Localization;
+
+namespace Marathon.UI.Services;
+
+///
+/// Single source of truth for the sport-code → display-label mapping that
+/// every list/detail page in the RCL needs. Resolves the four canonical
+/// data-sport-treeId values from the marathonbet.by spike (6 = basketball,
+/// 11 = football, 22723 = tennis, 43658 = hockey) against shared
+/// localization resources, with a stable invariant fallback for unknown
+/// codes so the UI degrades gracefully if the bookmaker adds a new sport.
+///
+public static class SportLabels
+{
+ public static string Resolve(IStringLocalizer localizer, int code) =>
+ code switch
+ {
+ 6 => localizer["Sport.Basketball"],
+ 11 => localizer["Sport.Football"],
+ 22723 => localizer["Sport.Tennis"],
+ 43658 => localizer["Sport.Hockey"],
+ _ => string.Format(CultureInfo.InvariantCulture, "Sport {0}", code),
+ };
+}