feat: добавить локализацию TUI (en/ru) с переключением клавишей L

- Добавлены TuiResources (sealed record), Locale, LocalizationService
- Все строки интерфейса вынесены из .razor-файлов в TuiResources
- App.razor: клавиша L циклически переключает локаль, заголовок показывает [EN]/[RU]
- Дочерние компоненты получают Loc как параметр (stateless)
- Создан AGENT.tui.md с правилами работы с TUI для агентов
- Обновлены AGENTS.md и CLAUDE.md со ссылками на AGENT.tui.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 23:53:59 +03:00
parent 4819fbca6c
commit 01565b32d9
12 changed files with 279 additions and 91 deletions

View File

@@ -1,15 +1,15 @@
@using LazyBear.MCP.Services.Logging
<Rows>
<Markup Content="Runtime Logs" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
<Markup Content="Left/Right change level. Up/Down move. PageUp/PageDown/Home/End scroll." Foreground="@UiPalette.TextMuted" />
<Markup Content="@Loc.LogsTitle" Foreground="@UiPalette.Text" Decoration="@Spectre.Console.Decoration.Bold" />
<Markup Content="@Loc.LogsHint" Foreground="@UiPalette.TextMuted" />
<Markup Content=" " />
<Columns>
@foreach (var filter in Filters)
{
var isActive = string.Equals(filter, SelectedFilter, StringComparison.Ordinal);
<Markup Content="@($" {filter} ")"
<Markup Content="@($" {FilterDisplay(filter)} ")"
Foreground="@(isActive ? UiPalette.SelectionForeground : UiPalette.Text)"
Background="@(isActive ? UiPalette.AccentSoft : UiPalette.SurfaceMuted)"
Decoration="@(isActive ? Spectre.Console.Decoration.Bold : Spectre.Console.Decoration.None)" />
@@ -22,7 +22,7 @@
@if (Entries.Count == 0)
{
<Border BorderColor="@UiPalette.Frame" BoxBorder="@Spectre.Console.BoxBorder.Rounded" Padding="@(new Spectre.Console.Padding(0, 0, 0, 0))">
<Markup Content="No log entries yet." Foreground="@UiPalette.TextDim" />
<Markup Content="@Loc.LogsEmpty" Foreground="@UiPalette.TextDim" />
</Border>
}
else
@@ -43,6 +43,7 @@
</Rows>
@code {
// Внутренние ключи фильтров — не локализуются (используются в логике App.razor)
private static readonly string[] Filters = ["All", "Info", "Warn", "Error"];
[Parameter, EditorRequired] public IReadOnlyList<LogEntry> Entries { get; set; } = Array.Empty<LogEntry>();
@@ -51,21 +52,26 @@
[Parameter] public string SelectedFilter { get; set; } = "All";
[Parameter] public int ViewportRows { get; set; } = 5;
[Parameter] public bool IsStickyToBottom { get; set; }
[Parameter] public TuiResources Loc { get; set; } = TuiResources.En;
private int[] GetOptions() => Enumerable.Range(0, Entries.Count).ToArray();
private int GetNormalizedIndex() => Entries.Count == 0 ? 0 : Math.Clamp(SelectedIndex, 0, Entries.Count - 1);
// "All" локализуется; уровни логов (Info/Warn/Error) остаются на английском в любой локали
private string FilterDisplay(string filter) =>
filter == "All" ? Loc.FilterAll : filter;
private string GetDetailsHeader()
{
if (Entries.Count == 0)
{
return $"Filter: {SelectedFilter}";
return $"{Loc.FilterLabel}: {FilterDisplay(SelectedFilter)}";
}
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
var position = Math.Clamp(SelectedIndex, 0, Entries.Count - 1) + 1;
var sticky = IsStickyToBottom ? "sticky" : "manual";
var sticky = IsStickyToBottom ? Loc.LogsSticky : Loc.LogsManual;
return $"{position}/{Entries.Count} | {selected.Timestamp:HH:mm:ss} | {selected.Level} | {selected.ShortCategory} | {sticky}";
}
@@ -73,7 +79,7 @@
{
if (Entries.Count == 0)
{
return "Incoming log entries will appear here.";
return Loc.LogsPlaceholder;
}
var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)];
@@ -84,25 +90,17 @@
return Fit(details, Math.Max(Console.WindowWidth - 12, 32));
}
private static Spectre.Console.Color GetLevelColor(LogEntry entry) => entry.Level switch
{
LogLevel.Error or LogLevel.Critical => UiPalette.Danger,
LogLevel.Warning => UiPalette.Warning,
LogLevel.Information => UiPalette.Text,
_ => UiPalette.TextMuted
};
private string FormatEntry(int index)
{
var entry = Entries[index];
var level = entry.Level switch
{
LogLevel.Error => "ERR",
LogLevel.Critical => "CRT",
LogLevel.Warning => "WRN",
LogLevel.Error => "ERR",
LogLevel.Critical => "CRT",
LogLevel.Warning => "WRN",
LogLevel.Information => "INF",
LogLevel.Debug => "DBG",
_ => "TRC"
LogLevel.Debug => "DBG",
_ => "TRC"
};
var text = $"{entry.Timestamp:HH:mm:ss} {level,-3} {entry.ShortCategory,-18} {entry.Message}";
@@ -111,21 +109,9 @@
private static string Fit(string text, int width)
{
if (width <= 0)
{
return string.Empty;
}
if (text.Length <= width)
{
return text.PadRight(width);
}
if (width <= 3)
{
return text[..width];
}
if (width <= 0) return string.Empty;
if (text.Length <= width) return text.PadRight(width);
if (width <= 3) return text[..width];
return text[..(width - 3)] + "...";
}
}