From 01565b32d97d27a81122b284b15a0f40720a98d6 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Mon, 13 Apr 2026 23:53:59 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20TUI=20(en/ru)=20=D1=81=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D1=88=D0=B5=D0=B9=20L?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены 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 --- AGENT.tui.md | 61 ++++++++++ AGENTS.md | 3 +- CLAUDE.md | 4 + LazyBear.MCP/Program.cs | 4 + LazyBear.MCP/TUI/Components/App.razor | 35 ++++-- LazyBear.MCP/TUI/Components/LogsTab.razor | 56 ++++----- LazyBear.MCP/TUI/Components/OverviewTab.razor | 36 ++---- LazyBear.MCP/TUI/Components/SettingsTab.razor | 31 ++--- LazyBear.MCP/TUI/Components/_Imports.razor | 1 + LazyBear.MCP/TUI/Localization/Locale.cs | 7 ++ .../TUI/Localization/LocalizationService.cs | 25 ++++ LazyBear.MCP/TUI/Localization/TuiResources.cs | 107 ++++++++++++++++++ 12 files changed, 279 insertions(+), 91 deletions(-) create mode 100644 AGENT.tui.md create mode 100644 LazyBear.MCP/TUI/Localization/Locale.cs create mode 100644 LazyBear.MCP/TUI/Localization/LocalizationService.cs create mode 100644 LazyBear.MCP/TUI/Localization/TuiResources.cs diff --git a/AGENT.tui.md b/AGENT.tui.md new file mode 100644 index 0000000..d1e7247 --- /dev/null +++ b/AGENT.tui.md @@ -0,0 +1,61 @@ +## AGENT.tui.md + +Read this file whenever you touch anything in `LazyBear.MCP/TUI/`. + +### TUI Structure + +- Entry point: `TUI/Components/App.razor` — owns all keyboard input and tab routing. +- Keyboard input: `TUI/GlobalKeyboardService.cs` — single source; dedicated blocking thread on `Console.ReadKey(intercept: true)`. Do not add other console readers. +- Localization: `TUI/Localization/` — `LocalizationService` singleton + `TuiResources` record with `En`/`Ru` static instances. +- Components: `OverviewTab`, `LogsTab`, `SettingsTab` — pure display; receive state as parameters, fire no keyboard events. +- Models: `TUI/Models/` — `OverviewRow`, `SettingsEntry`. +- Palette: `TUI/UiPalette.cs` — all colors. Do not hardcode `Spectre.Console.Color` values in components. + +### Keyboard Rules + +- All keys are handled in `App.razor.HandleKeyDown()`. Do not attach `@onkeydown` to `` is captured as `AdditionalAttributes` and not called for Tab/arrow keys. +- `FocusManager` intercepts Tab globally — do not call `FocusNextAsync()` in `OnAfterRenderAsync` unconditionally; it shifts focus on every re-render. +- No public global key-intercept API exists in 0.5.0. +- `Select.Value` = committed selection (updated on Enter). `Select.FocusedValue` = highlighted item during navigation. Use `FocusedValue` to reflect external state immediately. +- `StateHasChanged()` from a background thread must go through `InvokeAsync(() => { /* mutate state */ StateHasChanged(); })`. +- Every `StateHasChanged()` triggers a full terminal redraw. Batch log-driven re-renders to avoid visible lag. + +### Localization Rules + +- All UI strings live in `TUI/Localization/TuiResources.cs` only. No hardcoded strings in `.razor` files or other `.cs` files. +- To add a string: add a property to `TuiResources`, fill both `En` and `Ru` static instances. Build will fail if a `required init` is missing — by design. +- Log level names (`Info`, `Warn`, `Error`) stay in English in all locales — they are technical identifiers, not UI labels. +- Internal filter keys in `App.razor` (`"All"`, `"Info"`, `"Warn"`, `"Error"`) are English regardless of locale; `LogsTab` maps `"All"` → `Loc.FilterAll` for display. +- Language toggle: `L` key cycles through locales. `LocalizationService.SwitchNext()` fires `OnChanged`; `App.razor` re-renders via `OnLocaleChanged()`. +- Locale indicator shown in panel title: `"LazyBear MCP [EN]"` / `"LazyBear MCP [RU]"`. +- Do not inject `LocalizationService` into child tab components — `App.razor` passes the current `TuiResources` as `Loc` parameter. Child components are stateless regarding locale. + +### Component Contract + +- Child tab components (`OverviewTab`, `LogsTab`, `SettingsTab`) accept `[Parameter] TuiResources Loc` — always pass it from `App.razor`. +- Child components must not subscribe to events or inject services. Keep them as pure render components. +- `SelectedIndexChanged` callbacks exist for forward-compatibility; actual selection state is managed exclusively in `App.razor`. + +### DI Registration Pattern + +Services that must be both injectable and run as hosted services: +```csharp +services.AddSingleton(); +services.AddHostedService(sp => sp.GetRequiredService()); +``` +`AddHostedService()` alone creates a transient instance — unusable as injectable singleton. + +### Working Rules + +- Read `App.razor` before touching any keyboard or tab logic. +- Match the style of the file being edited. +- After changes, run `dotnet build`. +- Do not add new hardcoded strings to `.razor` files — add to `TuiResources` instead. +- Do not add new `Console.KeyAvailable` or `Console.ReadKey` calls outside `GlobalKeyboardService`. diff --git a/AGENTS.md b/AGENTS.md index fde2e49..1c01994 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,7 @@ - Project file structure and metadata indexed in memory via MCP memory system. ### Documentation -- RazorConsole gotchas and TUI keyboard notes: `docs/tui_log.md`. Read before touching TUI or input handling. +- **TUI work:** read `AGENT.tui.md` first — keyboard, localization, RazorConsole gotchas, component contract. +- RazorConsole gotchas and session notes: `docs/tui_log.md`. - RazorConsole library docs: `docs/razorconsole/` (`overview.md`, `components.md`, `custom-translators.md`). - OpenCode question policy: `docs/opencode/question-policy.md`. diff --git a/CLAUDE.md b/CLAUDE.md index 4dd5f85..40d8bef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,10 @@ Each client (`K8sClientProvider`, `JiraClientProvider`, `ConfluenceClientProvide `InMemoryLogSink` maintains a 500-entry circular `ConcurrentQueue`. All .NET logs flow through `InMemoryLoggerProvider` → sink → `OnLog` event → TUI Logs tab live view. +### TUI Agent Reference + +**Read `AGENT.tui.md` before making any changes to `TUI/`.** It covers keyboard handling, localization rules, RazorConsole gotchas, and component contract. + ### TUI Keyboard Input `GlobalKeyboardService` (`TUI/GlobalKeyboardService.cs`) is the **single source of keyboard events** for the entire TUI. It runs a dedicated background thread with a blocking `Console.ReadKey(intercept: true)` — no polling. `App.razor` subscribes to `OnKeyPressed`, converts `ConsoleKeyInfo` to `KeyboardEventArgs`, and dispatches via `InvokeAsync`. diff --git a/LazyBear.MCP/Program.cs b/LazyBear.MCP/Program.cs index 54c6eaf..bd41227 100644 --- a/LazyBear.MCP/Program.cs +++ b/LazyBear.MCP/Program.cs @@ -6,6 +6,7 @@ using LazyBear.MCP.Services.Mcp; using LazyBear.MCP.Services.ToolRegistry; using LazyBear.MCP.TUI; using LazyBear.MCP.TUI.Components; +using LazyBear.MCP.TUI.Localization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -36,6 +37,9 @@ var host = Host.CreateDefaultBuilder(args) // Глобальный читатель клавиш — единственный источник клавишных событий для TUI services.AddSingleton(); services.AddHostedService(sp => sp.GetRequiredService()); + + // Локализация TUI (en/ru, переключение клавишей L) + services.AddSingleton(); }) .ConfigureLogging(logging => { diff --git a/LazyBear.MCP/TUI/Components/App.razor b/LazyBear.MCP/TUI/Components/App.razor index 6aeed23..f961e9b 100644 --- a/LazyBear.MCP/TUI/Components/App.razor +++ b/LazyBear.MCP/TUI/Components/App.razor @@ -3,18 +3,19 @@ @inject ToolRegistryService Registry @inject InMemoryLogSink LogSink @inject GlobalKeyboardService KeyboardService +@inject LocalizationService Localization @implements IDisposable - - + @@ -36,7 +37,8 @@ + ViewportRows="@GetOverviewViewportRows()" + Loc="@Localization.Current" /> } else if (_activeTab == Tab.Logs) { @@ -45,14 +47,16 @@ SelectedIndexChanged="@OnLogSelectionChanged" SelectedFilter="@_logFilters[_logFilterIndex]" ViewportRows="@GetLogsViewportRows()" - IsStickyToBottom="@_logsStickToBottom" /> + IsStickyToBottom="@_logsStickToBottom" + Loc="@Localization.Current" /> } else { + ViewportRows="@GetSettingsViewportRows()" + Loc="@Localization.Current" /> } @@ -87,6 +91,7 @@ Registry.StateChanged += OnRegistryChanged; LogSink.OnLog += OnNewLog; KeyboardService.OnKeyPressed += OnConsoleKeyPressed; + Localization.OnChanged += OnLocaleChanged; } // Конвертация ConsoleKeyInfo → KeyboardEventArgs для переиспользования существующей логики @@ -130,6 +135,9 @@ }); } + private void OnLocaleChanged() => + InvokeAsync(StateHasChanged); + private void HandleKeyDown(KeyboardEventArgs args) { if (string.Equals(args.Key, "Tab", StringComparison.Ordinal)) @@ -138,6 +146,12 @@ return; } + if (args.Key is "l" or "L") + { + Localization.SwitchNext(); + return; // StateHasChanged вызовет OnLocaleChanged + } + switch (_activeTab) { case Tab.Overview: @@ -447,7 +461,7 @@ toolName, isModuleEnabled ? $"{module.ModuleName} / {toolName}" - : $"{module.ModuleName} / {toolName} (module is OFF, tool state is preserved)", + : $"{module.ModuleName} / {toolName} {Localization.Current.ModuleOffPreserved}", isConfigured, isModuleEnabled, false, @@ -527,11 +541,11 @@ private static bool IsInfoLevel(LogEntry entry) => entry.Level is LogLevel.Information or LogLevel.Debug or LogLevel.Trace; - private static string GetTabLabel(Tab tab) => tab switch + private string GetTabLabel(Tab tab) => tab switch { - Tab.Overview => "Overview", - Tab.Logs => "Logs", - _ => "Settings" + Tab.Overview => Localization.Current.TabOverview, + Tab.Logs => Localization.Current.TabLogs, + _ => Localization.Current.TabSettings }; public void Dispose() @@ -539,5 +553,6 @@ Registry.StateChanged -= OnRegistryChanged; LogSink.OnLog -= OnNewLog; KeyboardService.OnKeyPressed -= OnConsoleKeyPressed; + Localization.OnChanged -= OnLocaleChanged; } } diff --git a/LazyBear.MCP/TUI/Components/LogsTab.razor b/LazyBear.MCP/TUI/Components/LogsTab.razor index 7690931..cdd4a3b 100644 --- a/LazyBear.MCP/TUI/Components/LogsTab.razor +++ b/LazyBear.MCP/TUI/Components/LogsTab.razor @@ -1,15 +1,15 @@ @using LazyBear.MCP.Services.Logging - - + + @foreach (var filter in Filters) { var isActive = string.Equals(filter, SelectedFilter, StringComparison.Ordinal); - @@ -22,7 +22,7 @@ @if (Entries.Count == 0) { - + } else @@ -43,6 +43,7 @@ @code { + // Внутренние ключи фильтров — не локализуются (используются в логике App.razor) private static readonly string[] Filters = ["All", "Info", "Warn", "Error"]; [Parameter, EditorRequired] public IReadOnlyList Entries { get; set; } = Array.Empty(); @@ -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)] + "..."; } } diff --git a/LazyBear.MCP/TUI/Components/OverviewTab.razor b/LazyBear.MCP/TUI/Components/OverviewTab.razor index f692cb3..fd29163 100644 --- a/LazyBear.MCP/TUI/Components/OverviewTab.razor +++ b/LazyBear.MCP/TUI/Components/OverviewTab.razor @@ -1,12 +1,12 @@ - - + + @if (Rows.Count == 0) { - + } else @@ -30,6 +30,7 @@ [Parameter] public int SelectedIndex { get; set; } [Parameter] public EventCallback SelectedIndexChanged { get; set; } [Parameter] public int ViewportRows { get; set; } = 3; + [Parameter] public TuiResources Loc { get; set; } = TuiResources.En; private int[] GetOptions() => Enumerable.Range(0, Rows.Count).ToArray(); @@ -39,42 +40,27 @@ { if (Rows.Count == 0) { - return "No integration modules available."; + return Loc.OverviewEmpty; } var selected = Rows[Math.Clamp(SelectedIndex, 0, Rows.Count - 1)]; - var state = selected.IsModuleEnabled ? "ON" : "OFF"; - return $"{selected.ModuleName}: {selected.Description} | Module {state} | Tools {selected.ConfiguredTools}/{selected.TotalTools}"; + var state = selected.IsModuleEnabled ? Loc.StateOn : Loc.StateOff; + return $"{selected.ModuleName}: {selected.Description} | {Loc.FooterModule} {state} | {Loc.FooterTools} {selected.ConfiguredTools}/{selected.TotalTools}"; } - private static Spectre.Console.Color GetRowForeground(OverviewRow row) => - row.IsModuleEnabled ? UiPalette.Text : UiPalette.TextMuted; - private string FormatRow(int index) { var row = Rows[index]; - var status = row.IsModuleEnabled ? "[ON] " : "[OFF]"; + var status = row.IsModuleEnabled ? $"[{Loc.StateOn}] " : $"[{Loc.StateOff}]"; var text = $"{row.ModuleName,-12} {status} {row.ConfiguredTools,2}/{row.TotalTools,-2} {row.Description}"; return Fit(text, Math.Max(Console.WindowWidth - 12, 32)); } 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)] + "..."; } } diff --git a/LazyBear.MCP/TUI/Components/SettingsTab.razor b/LazyBear.MCP/TUI/Components/SettingsTab.razor index bd78713..93769db 100644 --- a/LazyBear.MCP/TUI/Components/SettingsTab.razor +++ b/LazyBear.MCP/TUI/Components/SettingsTab.razor @@ -1,12 +1,12 @@ - - + + @if (Entries.Count == 0) { - + } else @@ -30,6 +30,7 @@ [Parameter] public int SelectedIndex { get; set; } [Parameter] public EventCallback SelectedIndexChanged { get; set; } [Parameter] public int ViewportRows { get; set; } = 5; + [Parameter] public TuiResources Loc { get; set; } = TuiResources.En; private int[] GetOptions() => Enumerable.Range(0, Entries.Count).ToArray(); @@ -39,7 +40,7 @@ { if (Entries.Count == 0) { - return "Runtime enable/disable settings are unavailable."; + return Loc.SettingsUnavailable; } var selected = Entries[Math.Clamp(SelectedIndex, 0, Entries.Count - 1)]; @@ -51,7 +52,9 @@ var entry = Entries[index]; var indent = new string(' ', entry.Depth * 4); var checkbox = entry.IsChecked ? "[x]" : "[ ]"; - var disabledSuffix = entry.Kind == SettingsEntryKind.Tool && !entry.IsModuleEnabled ? " (module off)" : string.Empty; + var disabledSuffix = entry.Kind == SettingsEntryKind.Tool && !entry.IsModuleEnabled + ? $" {Loc.ModuleOff}" + : string.Empty; string text; if (entry.Kind == SettingsEntryKind.Module) @@ -69,21 +72,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)] + "..."; } } diff --git a/LazyBear.MCP/TUI/Components/_Imports.razor b/LazyBear.MCP/TUI/Components/_Imports.razor index 044f3f2..2efc1c9 100644 --- a/LazyBear.MCP/TUI/Components/_Imports.razor +++ b/LazyBear.MCP/TUI/Components/_Imports.razor @@ -4,5 +4,6 @@ @using RazorConsole.Core @using RazorConsole.Core.Rendering @using LazyBear.MCP.TUI +@using LazyBear.MCP.TUI.Localization @using LazyBear.MCP.TUI.Models @using LazyBear.MCP.TUI.Components diff --git a/LazyBear.MCP/TUI/Localization/Locale.cs b/LazyBear.MCP/TUI/Localization/Locale.cs new file mode 100644 index 0000000..890b60e --- /dev/null +++ b/LazyBear.MCP/TUI/Localization/Locale.cs @@ -0,0 +1,7 @@ +namespace LazyBear.MCP.TUI.Localization; + +public enum Locale +{ + En = 0, + Ru = 1 +} diff --git a/LazyBear.MCP/TUI/Localization/LocalizationService.cs b/LazyBear.MCP/TUI/Localization/LocalizationService.cs new file mode 100644 index 0000000..641862f --- /dev/null +++ b/LazyBear.MCP/TUI/Localization/LocalizationService.cs @@ -0,0 +1,25 @@ +namespace LazyBear.MCP.TUI.Localization; + +/// +/// Синглтон, хранящий текущую локаль TUI. Переключение — клавиша L. +/// Компоненты подписываются на OnChanged для перерисовки при смене языка. +/// +public sealed class LocalizationService +{ + private static readonly TuiResources[] All = [TuiResources.En, TuiResources.Ru]; + private static readonly string[] Labels = ["EN", "RU"]; + + private int _index; + + public TuiResources Current => All[_index]; + public string Label => Labels[_index]; + public Locale Locale => (Locale)_index; + + public event Action? OnChanged; + + public void SwitchNext() + { + _index = (_index + 1) % All.Length; + OnChanged?.Invoke(); + } +} diff --git a/LazyBear.MCP/TUI/Localization/TuiResources.cs b/LazyBear.MCP/TUI/Localization/TuiResources.cs new file mode 100644 index 0000000..fd2bbd8 --- /dev/null +++ b/LazyBear.MCP/TUI/Localization/TuiResources.cs @@ -0,0 +1,107 @@ +namespace LazyBear.MCP.TUI.Localization; + +/// +/// Все строки TUI для одной локали. +/// При добавлении новой строки: добавить свойство сюда и перевод в оба статических экземпляра. +/// +public sealed record TuiResources +{ + // ── Подсказка и вкладки ────────────────────────────────────────────────── + public string HintBar { get; init; } = ""; + public string TabOverview { get; init; } = ""; + public string TabLogs { get; init; } = ""; + public string TabSettings { get; init; } = ""; + + // ── Overview ───────────────────────────────────────────────────────────── + public string OverviewTitle { get; init; } = ""; + public string OverviewHint { get; init; } = ""; + public string OverviewEmpty { get; init; } = ""; + public string StateOn { get; init; } = ""; + public string StateOff { get; init; } = ""; + public string FooterModule { get; init; } = ""; + public string FooterTools { get; init; } = ""; + + // ── Logs ───────────────────────────────────────────────────────────────── + public string LogsTitle { get; init; } = ""; + public string LogsHint { get; init; } = ""; + public string LogsEmpty { get; init; } = ""; + public string LogsPlaceholder { get; init; } = ""; + public string LogsSticky { get; init; } = ""; + public string LogsManual { get; init; } = ""; + public string FilterLabel { get; init; } = ""; + public string FilterAll { get; init; } = ""; + + // ── Settings ───────────────────────────────────────────────────────────── + public string SettingsTitle { get; init; } = ""; + public string SettingsHint { get; init; } = ""; + public string SettingsEmpty { get; init; } = ""; + public string SettingsUnavailable { get; init; } = ""; + public string ModuleOff { get; init; } = ""; + public string ModuleOffPreserved { get; init; } = ""; + + // ── Локали ─────────────────────────────────────────────────────────────── + + public static readonly TuiResources En = new() + { + HintBar = "Tab: tabs | Arrows: navigate | Space: toggle | Enter: open | L: language", + TabOverview = "Overview", + TabLogs = "Logs", + TabSettings = "Settings", + + OverviewTitle = "Module Overview", + OverviewHint = "Up/Down: select module. Enter: open settings.", + OverviewEmpty = "No modules registered.", + StateOn = "ON", + StateOff = "OFF", + FooterModule = "Module", + FooterTools = "Tools", + + LogsTitle = "Runtime Logs", + LogsHint = "Left/Right: filter | Up/Down: scroll | PageUp/Down: page", + LogsEmpty = "No log entries yet.", + LogsPlaceholder = "Incoming log entries will appear here.", + LogsSticky = "sticky", + LogsManual = "manual", + FilterLabel = "Filter", + FilterAll = "All", + + SettingsTitle = "Tool Registry", + SettingsHint = "Up/Down: select | Left/Right: expand/collapse | Space: toggle", + SettingsEmpty = "No modules available.", + SettingsUnavailable = "Runtime enable/disable settings are unavailable.", + ModuleOff = "(module off)", + ModuleOffPreserved = "(module is OFF, tool state is preserved)" + }; + + public static readonly TuiResources Ru = new() + { + HintBar = "Tab: вкладки | Стрелки: навигация | Space: вкл/выкл | Enter: открыть | L: язык", + TabOverview = "Обзор", + TabLogs = "Логи", + TabSettings = "Настройки", + + OverviewTitle = "Модули", + OverviewHint = "Вверх/вниз: выбор модуля. Enter: открыть настройки.", + OverviewEmpty = "Нет зарегистрированных модулей.", + StateOn = "ВКЛ", + StateOff = "ВЫКЛ", + FooterModule = "Модуль", + FooterTools = "Инструменты", + + LogsTitle = "Логи", + LogsHint = "Лево/право: фильтр | Вверх/вниз: прокрутка | PageUp/Down: страница", + LogsEmpty = "Записей пока нет.", + LogsPlaceholder = "Новые записи будут появляться здесь.", + LogsSticky = "следить", + LogsManual = "вручную", + FilterLabel = "Фильтр", + FilterAll = "Все", + + SettingsTitle = "Реестр инструментов", + SettingsHint = "Вверх/вниз: выбор | Лево/право: развернуть/свернуть | Space: вкл/выкл", + SettingsEmpty = "Нет доступных модулей.", + SettingsUnavailable = "Настройки включения/выключения недоступны.", + ModuleOff = "(модуль выкл)", + ModuleOffPreserved = "(модуль ВЫКЛ, состояние инструментов сохранено)" + }; +}