From 3fc15d9789e26bf45d2eb97a030b812f77ce33ea Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 12 Apr 2025 21:13:33 +0200 Subject: [PATCH] Configure language & start language plugins (#400) --- app/MindWork AI Studio.sln.DotSettings | 1 + .../Components/ChatComponent.razor.cs | 6 +- .../Components/InnerScrolling.razor.cs | 9 +- .../Components/MSGComponentBase.cs | 66 +++++++-- .../Settings/SettingsPanelApp.razor | 19 ++- .../Layout/MainLayout.razor.cs | 52 ++++--- app/MindWork AI Studio/Pages/Chat.razor | 4 +- app/MindWork AI Studio/Pages/Chat.razor.cs | 9 +- app/MindWork AI Studio/Pages/Home.razor | 5 +- app/MindWork AI Studio/Pages/Home.razor.cs | 12 +- app/MindWork AI Studio/Pages/Plugins.razor | 1 + app/MindWork AI Studio/Pages/Plugins.razor.cs | 43 ++---- app/MindWork AI Studio/Pages/Writer.razor.cs | 12 +- .../plugin.lua | 14 ++ .../plugin.lua | 14 ++ .../Settings/ConfigurationSelectData.cs | 16 ++ .../Settings/DataModel/DataApp.cs | 10 ++ .../Settings/DataModel/LangBehavior.cs | 7 + .../DataModel/LangBehaviourExtensions.cs | 12 ++ .../Settings/SettingsManager.cs | 49 +++++- app/MindWork AI Studio/Tools/Event.cs | 1 + app/MindWork AI Studio/Tools/FNVHash.cs | 62 ++++++++ .../Tools/PluginSystem/IAvailablePlugin.cs | 6 + .../Tools/PluginSystem/ILang.cs | 21 +++ .../Tools/PluginSystem/ILanguagePlugin.cs | 13 +- .../Tools/PluginSystem/NoPluginLanguage.cs | 26 ++++ .../PluginSystem/PluginFactory.HotReload.cs | 6 + .../PluginSystem/PluginFactory.Internal.cs | 18 ++- .../Tools/PluginSystem/PluginFactory.cs | 140 ++++++++++++++++-- .../Tools/PluginSystem/PluginLanguage.cs | 106 ++++++++----- .../Tools/PluginSystem/PluginMetadata.cs | 8 +- 31 files changed, 612 insertions(+), 156 deletions(-) create mode 100644 app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs create mode 100644 app/MindWork AI Studio/Tools/FNVHash.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/ILang.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings index 6cbaf40c..4a94c5cc 100644 --- a/app/MindWork AI Studio.sln.DotSettings +++ b/app/MindWork AI Studio.sln.DotSettings @@ -2,6 +2,7 @@ AI EDI ERI + FNV GWDG HF LLM diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 0759d5b4..f12be751 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -840,8 +840,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable #region Overrides of MSGComponentBase public override string ComponentName => nameof(ChatComponent); - - public override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + + protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { switch (triggeredEvent) { @@ -860,7 +860,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable } } - public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default + protected override Task ProcessIncomingMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default { switch (triggeredEvent) { diff --git a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs index 6dd0143e..3238ffd9 100644 --- a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs +++ b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs @@ -46,8 +46,8 @@ public partial class InnerScrolling : MSGComponentBase #region Overrides of MSGComponentBase public override string ComponentName => nameof(InnerScrolling); - - public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + + protected override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { switch (triggeredEvent) { @@ -59,11 +59,6 @@ public partial class InnerScrolling : MSGComponentBase return Task.CompletedTask; } - public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default - { - return Task.FromResult(default(TResult)); - } - #endregion private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; "; diff --git a/app/MindWork AI Studio/Components/MSGComponentBase.cs b/app/MindWork AI Studio/Components/MSGComponentBase.cs index 4dddb57d..4e904c02 100644 --- a/app/MindWork AI Studio/Components/MSGComponentBase.cs +++ b/app/MindWork AI Studio/Components/MSGComponentBase.cs @@ -1,10 +1,11 @@ using AIStudio.Settings; +using AIStudio.Tools.PluginSystem; using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver +public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver, ILang { [Inject] protected SettingsManager SettingsManager { get; init; } = null!; @@ -12,12 +13,37 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus [Inject] protected MessageBus MessageBus { get; init; } = null!; + [Inject] + private ILogger Logger { get; init; } = null!; + + private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage; + #region Overrides of ComponentBase - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { + this.Lang = await this.SettingsManager.GetActiveLanguagePlugin(); + this.MessageBus.RegisterComponent(this); - base.OnInitialized(); + await base.OnInitializedAsync(); + } + + #endregion + + #region Implementation of ILang + + /// + public string T(string fallbackEN) + { + var type = this.GetType(); + var ns = $"{type.Namespace!}::{type.Name}".ToUpperInvariant().Replace(".", "::"); + var key = $"root::{ns}::T{fallbackEN.ToFNV32()}"; + + if(this.Lang.TryGetText(key, out var text, logWarning: false)) + return text; + + this.Logger.LogWarning($"Missing translation key '{key}' for content '{fallbackEN}'."); + return fallbackEN; } #endregion @@ -26,7 +52,7 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus public abstract string ComponentName { get; } - public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) + public async Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) { switch (triggeredEvent) { @@ -34,19 +60,34 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus this.StateHasChanged(); break; + case Event.PLUGINS_RELOADED: + this.Lang = await this.SettingsManager.GetActiveLanguagePlugin(); + await this.InvokeAsync(this.StateHasChanged); + break; + default: - return this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data); + await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data); + break; } - - return Task.CompletedTask; } - - public abstract Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data); - - public abstract Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data); + + public async Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) + { + return await this.ProcessIncomingMessageWithResult(sendingComponent, triggeredEvent, data); + } #endregion + protected virtual Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) + { + return Task.CompletedTask; + } + + protected virtual Task ProcessIncomingMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) + { + return Task.FromResult(default); + } + #region Implementation of IDisposable public void Dispose() @@ -71,7 +112,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus // Append the color theme changed event to the list of events: var eventsList = new List(events) { - Event.COLOR_THEME_CHANGED + Event.COLOR_THEME_CHANGED, + Event.PLUGINS_RELOADED, }; this.MessageBus.ApplyFilters(this, components, eventsList.ToArray()); diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index e67aaf5f..da313dda 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -3,14 +3,25 @@ @inherits SettingsPanelBase + + @if (PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager)) + { + + + @if (this.SettingsManager.ConfigurationData.App.LanguageBehavior is LangBehavior.MANUAL) + { + + } + } + - + - - @if(this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE) + + @if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE) { var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList(); if (availablePreviewFeatures.Count > 0) @@ -18,7 +29,7 @@ } } - + \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 08e963cb..b80de4a5 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -83,26 +83,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis // Ensure that all settings are loaded: await this.SettingsManager.LoadSettings(); - // - // We cannot process the plugins before the settings are loaded, - // and we know our data directory. - // - if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager)) - { - // Ensure that all internal plugins are present: - await PluginFactory.EnsureInternalPlugins(); - - // Load (but not start) all plugins, without waiting for them: - var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - _ = PluginFactory.LoadAll(pluginLoadingTimeout.Token); - - // Set up hot reloading for plugins: - PluginFactory.SetUpHotReloading(); - } - // Register this component with the message bus: this.MessageBus.RegisterComponent(this); - this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR ]); + this.MessageBus.ApplyFilters(this, [], + [ + Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR, + Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED + ]); // Set the snackbar for the update service: UpdateService.SetBlazorDependencies(this.Snackbar); @@ -115,6 +102,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis // Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133: MudGlobal.TooltipDefaults.Duration = TimeSpan.Zero; + // Send a message to start the plugin system: + await this.MessageBus.SendMessage(this, Event.STARTUP_PLUGIN_SYSTEM); + await this.themeProvider.WatchSystemPreference(this.SystemeThemeChanged); await this.UpdateThemeConfiguration(); this.LoadNavItems(); @@ -179,6 +169,32 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis error.Show(this.Snackbar); break; + + case Event.STARTUP_PLUGIN_SYSTEM: + if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager)) + { + _ = Task.Run(async () => + { + // Set up the plugin system: + PluginFactory.Setup(); + + // Ensure that all internal plugins are present: + await PluginFactory.EnsureInternalPlugins(); + + // Load (but not start) all plugins, without waiting for them: + var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await PluginFactory.LoadAll(pluginLoadingTimeout.Token); + + // Set up hot reloading for plugins: + PluginFactory.SetUpHotReloading(); + }); + } + + break; + + case Event.PLUGINS_RELOADED: + await this.InvokeAsync(this.StateHasChanged); + break; } } diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index b77edf18..b95a9d62 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -7,11 +7,11 @@ @if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty) { - @($"Chat in Workspace \"{this.currentWorkspaceName}\"") + @(T("Chat in Workspace") + $" \"{this.currentWorkspaceName}\"") } else { - @("Temporary Chat") + @(T("Short-Term Chat")) } diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index 3ee7ecc9..f3c5c8b5 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -83,8 +83,8 @@ public partial class Chat : MSGComponentBase #region Overrides of MSGComponentBase public override string ComponentName => nameof(Chat); - - public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + + protected override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { switch (triggeredEvent) { @@ -96,10 +96,5 @@ public partial class Chat : MSGComponentBase return Task.CompletedTask; } - public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default - { - return Task.FromResult(default(TResult)); - } - #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor index de74a626..0b035995 100644 --- a/app/MindWork AI Studio/Pages/Home.razor +++ b/app/MindWork AI Studio/Pages/Home.razor @@ -1,8 +1,11 @@ @attribute [Route(Routes.HOME)] +@inherits MSGComponentBase
- Let's get started + + @T("Let's get started") + diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 026fafe5..4468e651 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -6,10 +6,10 @@ using Changelog = AIStudio.Components.Changelog; namespace AIStudio.Pages; -public partial class Home : ComponentBase +public partial class Home : MSGComponentBase { [Inject] - private HttpClient HttpClient { get; set; } = null!; + private HttpClient HttpClient { get; init; } = null!; private string LastChangeContent { get; set; } = string.Empty; @@ -22,7 +22,13 @@ public partial class Home : ComponentBase } #endregion - + + #region Overrides of MSGComponentBase + + public override string ComponentName => nameof(Home); + + #endregion + private async Task ReadLastChangeAsync() { var latest = Changelog.LOGS.MaxBy(n => n.Build); diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor index 6d5bf8ce..8306d6d1 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor +++ b/app/MindWork AI Studio/Pages/Plugins.razor @@ -1,4 +1,5 @@ @using AIStudio.Tools.PluginSystem +@inherits MSGComponentBase @attribute [Route(Routes.PLUGINS)]
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs index 9504c0ff..88c90433 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor.cs +++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs @@ -1,22 +1,14 @@ -using AIStudio.Settings; +using AIStudio.Components; using AIStudio.Tools.PluginSystem; -using Microsoft.AspNetCore.Components; - namespace AIStudio.Pages; -public partial class Plugins : ComponentBase, IMessageBusReceiver +public partial class Plugins : MSGComponentBase { private const string GROUP_ENABLED = "Enabled"; private const string GROUP_DISABLED = "Disabled"; private const string GROUP_INTERNAL = "Internal"; - [Inject] - private MessageBus MessageBus { get; init; } = null!; - - [Inject] - private SettingsManager SettingsManager { get; init; } = null!; - private TableGroupDefinition groupConfig = null!; #region Overrides of ComponentBase @@ -45,7 +37,13 @@ public partial class Plugins : ComponentBase, IMessageBusReceiver } #endregion - + + #region Overrides of MSGComponentBase + + public override string ComponentName => nameof(Plugins); + + #endregion + private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta) { if (this.SettingsManager.IsPluginEnabled(pluginMeta)) @@ -56,27 +54,4 @@ public partial class Plugins : ComponentBase, IMessageBusReceiver await this.SettingsManager.StoreSettings(); await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - - #region Implementation of IMessageBusReceiver - - public string ComponentName => nameof(Plugins); - - public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) - { - switch (triggeredEvent) - { - case Event.PLUGINS_RELOADED: - this.InvokeAsync(this.StateHasChanged); - break; - } - - return Task.CompletedTask; - } - - public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) - { - return Task.FromResult(default); - } - - #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Writer.razor.cs b/app/MindWork AI Studio/Pages/Writer.razor.cs index 42b23abc..a36c0410 100644 --- a/app/MindWork AI Studio/Pages/Writer.razor.cs +++ b/app/MindWork AI Studio/Pages/Writer.razor.cs @@ -24,7 +24,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable private string userInput = string.Empty; private string userDirection = string.Empty; private string suggestion = string.Empty; - + #region Overrides of ComponentBase protected override async Task OnInitializedAsync() @@ -43,16 +43,6 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable public override string ComponentName => nameof(Writer); - public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default - { - return Task.CompletedTask; - } - - public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default - { - return Task.FromResult(default(TResult)); - } - #endregion private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 634c8e74..b301dbd7 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -44,6 +44,20 @@ DEPRECATION_MESSAGE = "" -- code followed by the ISO 3166-1 country code: IETF_TAG = "de-DE" +-- The language name in the user's language: +LANG_NAME = "Deutsch (Deutschland)" + UI_TEXT_CONTENT = { HOME = CONTENT_HOME, + AISTUDIO = { + PAGES = { + HOME = { + T2331588413 = "Lass uns anfangen", + }, + + CHAT = { + T3718856736 = "Vorläufiger Chat", + } + }, + } } diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 78f66474..2687f722 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -44,6 +44,20 @@ DEPRECATION_MESSAGE = "" -- code followed by the ISO 3166-1 country code: IETF_TAG = "en-US" +-- The language name in the user's language: +LANG_NAME = "English (United States)" + UI_TEXT_CONTENT = { HOME = CONTENT_HOME, + AISTUDIO = { + PAGES = { + HOME = { + T2331588413 = "Let's get started", + }, + + CHAT = { + T3718856736 = "Short-Term Chat", + }, + }, + } } diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs index 5a2b8f11..a47bde9d 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs @@ -6,6 +6,7 @@ using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.EMail; using AIStudio.Provider; using AIStudio.Settings.DataModel; +using AIStudio.Tools.PluginSystem; using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles; using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles; @@ -25,6 +26,21 @@ public readonly record struct ConfigurationSelectData(string Name, T Value); /// public static class ConfigurationSelectDataFactory { + public static IEnumerable> GetLangBehaviorData() + { + foreach (var behavior in Enum.GetValues()) + yield return new(behavior.Name(), behavior); + } + + public static IEnumerable> GetLanguagesData() + { + foreach (var runningPlugin in PluginFactory.RunningPlugins) + { + if(runningPlugin is ILanguagePlugin languagePlugin) + yield return new(languagePlugin.LangName, runningPlugin.Id); + } + } + public static IEnumerable> GetLoadingChatProviderBehavior() { yield return new("When possible, use the LLM provider which was used for each chat in the first place", LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index 7e89404a..af76fa26 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -2,6 +2,16 @@ namespace AIStudio.Settings.DataModel; public sealed class DataApp { + /// + /// The language behavior. + /// + public LangBehavior LanguageBehavior { get; set; } = LangBehavior.AUTO; + + /// + /// The language plugin ID to use. + /// + public Guid LanguagePluginId { get; set; } = Guid.Empty; + /// /// The preferred theme to use. /// diff --git a/app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs b/app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs new file mode 100644 index 00000000..492d6bca --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Settings.DataModel; + +public enum LangBehavior +{ + AUTO, + MANUAL, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs new file mode 100644 index 00000000..c71caf92 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Settings.DataModel; + +public static class LangBehaviorExtensions +{ + public static string Name(this LangBehavior langBehavior) => langBehavior switch + { + LangBehavior.AUTO => "Choose the language automatically, based on your system language.", + LangBehavior.MANUAL => "Choose the language manually.", + + _ => "Unknown option" + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index dcbe48cb..24bd1ded 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using AIStudio.Provider; using AIStudio.Settings.DataModel; using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Services; // ReSharper disable NotAccessedPositionalProperty.Local @@ -13,7 +14,7 @@ namespace AIStudio.Settings; /// /// The settings manager. /// -public sealed class SettingsManager(ILogger logger) +public sealed class SettingsManager(ILogger logger, RustService rustService) { private const string SETTINGS_FILENAME = "settings.json"; @@ -24,6 +25,7 @@ public sealed class SettingsManager(ILogger logger) }; private readonly ILogger logger = logger; + private readonly RustService rustService = rustService; /// /// The directory where the configuration files are stored. @@ -143,8 +145,53 @@ public sealed class SettingsManager(ILogger logger) return minimumLevel; } + /// + /// Checks if the given plugin is enabled. + /// + /// The plugin to check. + /// True, when the plugin is enabled, false otherwise. public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id); + /// + /// Returns the active language plugin. + /// + /// The active language plugin. + public async Task GetActiveLanguagePlugin() + { + switch (this.ConfigurationData.App.LanguageBehavior) + { + case LangBehavior.AUTO: + var languageCode = await this.rustService.ReadUserLanguage(); + var languagePlugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x is ILanguagePlugin langPlug && langPlug.IETFTag == languageCode); + if (languagePlugin is null) + return PluginFactory.BaseLanguage; + + if (languagePlugin is ILanguagePlugin langPlugin) + return langPlugin; + + this.logger.LogError("The language plugin is not a language plugin."); + return PluginFactory.BaseLanguage; + + case LangBehavior.MANUAL: + var pluginId = this.ConfigurationData.App.LanguagePluginId; + var plugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x.Id == pluginId); + if (plugin is null) + { + this.logger.LogWarning($"The chosen language plugin (id='{pluginId}') is not available."); + return PluginFactory.BaseLanguage; + } + + if (plugin is ILanguagePlugin chosenLangPlugin) + return chosenLangPlugin; + + this.logger.LogError("The chosen language plugin is not a language plugin."); + return PluginFactory.BaseLanguage; + } + + this.logger.LogError("The language behavior is unknown."); + return PluginFactory.BaseLanguage; + } + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) { diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 57758589..213adf29 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -8,6 +8,7 @@ public enum Event STATE_HAS_CHANGED, CONFIGURATION_CHANGED, COLOR_THEME_CHANGED, + STARTUP_PLUGIN_SYSTEM, PLUGINS_RELOADED, SHOW_ERROR, diff --git a/app/MindWork AI Studio/Tools/FNVHash.cs b/app/MindWork AI Studio/Tools/FNVHash.cs new file mode 100644 index 00000000..cc47645e --- /dev/null +++ b/app/MindWork AI Studio/Tools/FNVHash.cs @@ -0,0 +1,62 @@ +// ReSharper disable MemberCanBePrivate.Global +namespace AIStudio.Tools; + +/// +/// Implements the Fowler–Noll–Vo hash function for 32-bit and 64-bit hashes. +/// +public static class FNVHash +{ + private const uint FNV_OFFSET_BASIS_32_BIT = 2_166_136_261; + private const ulong FNV_OFFSET_BASIS_64_BIT = 14_695_981_039_346_656_037; + + private const uint FNV_PRIME_32_BIT = 16_777_619; + private const ulong FNV_PRIME_64_BIT = 1_099_511_628_211; + + /// + /// Computes the 32bit FNV-1a hash of a string. + /// + /// The string to hash. + /// The 32bit FNV-1a hash of the string. + public static uint ToFNV32(this string text) => ToFNV32(text.AsSpan()); + + /// + /// Computes the 32bit FNV-1a hash of a string. + /// + /// The string to hash. + /// The 32bit FNV-1a hash of the string. + public static uint ToFNV32(this ReadOnlySpan text) + { + var hash = FNV_OFFSET_BASIS_32_BIT; + foreach (var c in text) + { + hash ^= c; + hash *= FNV_PRIME_32_BIT; + } + + return hash; + } + + /// + /// Computes the 64bit FNV-1a hash of a string. + /// + /// The string to hash. + /// The 64bit FNV-1a hash of the string. + public static ulong ToFNV64(this string text) => ToFNV64(text.AsSpan()); + + /// + /// Computes the 64bit FNV-1a hash of a string. + /// + /// The string to hash. + /// The 64bit FNV-1a hash of the string. + public static ulong ToFNV64(this ReadOnlySpan text) + { + var hash = FNV_OFFSET_BASIS_64_BIT; + foreach (var c in text) + { + hash ^= c; + hash *= FNV_PRIME_64_BIT; + } + + return hash; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs new file mode 100644 index 00000000..a992d303 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools.PluginSystem; + +public interface IAvailablePlugin : IPluginMetadata +{ + public string LocalPath { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILang.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILang.cs new file mode 100644 index 00000000..6c5277ea --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/ILang.cs @@ -0,0 +1,21 @@ +namespace AIStudio.Tools.PluginSystem; + +/// +/// Represents a contract to access text from a language plugin. +/// +public interface ILang +{ + /// + /// Tries to get a text from the language plugin. + /// + /// + /// The given fallback text is used to determine the key for + /// the language plugin. Base for the key is the namespace of + /// the using component and the fallback text in English (US). + /// The given text getting hashed. When the key does not exist, + /// the fallback text will be returned. + /// + /// The fallback text in English (US). + /// The text from the language plugin or the fallback text. + public string T(string fallbackEN); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs index a33bf3f5..4f214d68 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs @@ -16,6 +16,17 @@ public interface ILanguagePlugin /// /// The key to use to get the text. /// The desired text. + /// When true, a warning will be logged if the key does not exist. /// True if the key exists, false otherwise. - public bool TryGetText(string key, out string value); + public bool TryGetText(string key, out string value, bool logWarning = false); + + /// + /// Gets the IETF tag of the language plugin. + /// + public string IETFTag { get; } + + /// + /// Gets the name of the language. + /// + public string LangName { get; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs new file mode 100644 index 00000000..26d35849 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs @@ -0,0 +1,26 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem; + +public sealed class NoPluginLanguage : PluginBase, ILanguagePlugin +{ + public static readonly NoPluginLanguage INSTANCE = new(); + + private NoPluginLanguage() : base(true, LuaState.Create(), PluginType.LANGUAGE, string.Empty) + { + } + + #region Implementation of ILanguagePlugin + + public bool TryGetText(string key, out string value, bool logWarning = false) + { + value = string.Empty; + return true; + } + + public string IETFTag => string.Empty; + + public string LangName => string.Empty; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs index c2d75bf3..0a32c17d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs @@ -4,6 +4,12 @@ public static partial class PluginFactory { public static void SetUpHotReloading() { + if (!IS_INITIALIZED) + { + LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it."); + return; + } + LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'."); try { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs index 2c14adb6..0a9e9dac 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs @@ -10,6 +10,12 @@ public static partial class PluginFactory { public static async Task EnsureInternalPlugins() { + if (!IS_INITIALIZED) + { + LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it."); + return; + } + LOG.LogInformation("Start ensuring internal plugins."); foreach (var plugin in Enum.GetValues()) { @@ -40,15 +46,15 @@ public static partial class PluginFactory } // Ensure that the additional resources exist: - foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath)) + foreach (var contentFilePath in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath)) { - if(content.IsDirectory) + if(contentFilePath.IsDirectory) { LOG.LogError("The plugin contains a directory. This is not allowed."); continue; } - await CopyInternalPluginFile(content, metaData); + await CopyInternalPluginFile(contentFilePath, metaData); } } catch @@ -57,9 +63,9 @@ public static partial class PluginFactory } } - private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData) + private static async Task CopyInternalPluginFile(IFileInfo resourceFilePath, InternalPluginData metaData) { - await using var inputStream = resourceInfo.CreateReadStream(); + await using var inputStream = resourceFilePath.CreateReadStream(); var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory()); @@ -73,7 +79,7 @@ public static partial class PluginFactory if (!Directory.Exists(pluginPath)) Directory.CreateDirectory(pluginPath); - var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name); + var pluginFilePath = Path.Join(pluginPath, resourceFilePath.Name); await using var outputStream = File.Create(pluginFilePath); await inputStream.CopyToAsync(outputStream); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 18d2023b..57ac7ea1 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -9,29 +9,45 @@ namespace AIStudio.Tools.PluginSystem; public static partial class PluginFactory { - private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory"); - - private static readonly string DATA_DIR = SettingsManager.DataDirectory!; - - private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); - - private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); + private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static readonly List AVAILABLE_PLUGINS = []; + private static readonly List RUNNING_PLUGINS = []; - private static readonly FileSystemWatcher HOT_RELOAD_WATCHER; - - private static readonly List AVAILABLE_PLUGINS = []; + private static bool IS_INITIALIZED; + private static string DATA_DIR = string.Empty; + private static string PLUGINS_ROOT = string.Empty; + private static string INTERNAL_PLUGINS_ROOT = string.Empty; + private static FileSystemWatcher HOT_RELOAD_WATCHER = null!; + private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE; /// /// A list of all available plugins. /// public static IReadOnlyCollection AvailablePlugins => AVAILABLE_PLUGINS; + + /// + /// A list of all running plugins. + /// + public static IReadOnlyCollection RunningPlugins => RUNNING_PLUGINS; - static PluginFactory() + public static ILanguagePlugin BaseLanguage => BASE_LANGUAGE_PLUGIN; + + /// + /// Set up the plugin factory. We will read the data directory from the settings manager. + /// Afterward, we will create the plugins directory and the internal plugin directory. + /// + public static void Setup() { + DATA_DIR = SettingsManager.DataDirectory!; + PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); + INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); + if (!Directory.Exists(PLUGINS_ROOT)) Directory.CreateDirectory(PLUGINS_ROOT); HOT_RELOAD_WATCHER = new(PLUGINS_ROOT); + IS_INITIALIZED = true; } /// @@ -48,6 +64,12 @@ public static partial class PluginFactory /// public static async Task LoadAll(CancellationToken cancellationToken = default) { + if (!IS_INITIALIZED) + { + LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it."); + return; + } + LOG.LogInformation("Start loading plugins."); if (!Directory.Exists(PLUGINS_ROOT)) { @@ -96,8 +118,11 @@ public static partial class PluginFactory } LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')"); - AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin)); + AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath)); } + + // Start or restart all plugins: + await RestartAllPlugins(cancellationToken); } private static async Task Load(string pluginPath, string code, CancellationToken cancellationToken = default) @@ -148,9 +173,100 @@ public static partial class PluginFactory _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.") }; } + + private static async Task RestartAllPlugins(CancellationToken cancellationToken = default) + { + LOG.LogInformation("Try to start or restart all plugins."); + RUNNING_PLUGINS.Clear(); + + // + // Get the base language plugin. This is the plugin that will be used to fill in missing keys. + // + var baseLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id; + var baseLanguagePluginMetaData = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == baseLanguagePluginId); + if (baseLanguagePluginMetaData is null) + { + LOG.LogError($"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation."); + return; + } + + var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken); + if (startedBasePlugin is NoPlugin noPlugin) + { + LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}"); + return; + } + + if (startedBasePlugin is PluginLanguage languagePlugin) + { + BASE_LANGUAGE_PLUGIN = languagePlugin; + RUNNING_PLUGINS.Add(languagePlugin); + LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'"); + } + else + { + LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}"); + return; + } + + // + // Iterate over all available plugins and try to start them. + // + foreach (var availablePlugin in AVAILABLE_PLUGINS) + { + if(cancellationToken.IsCancellationRequested) + break; + + if (availablePlugin.Id == baseLanguagePluginId) + continue; + + if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin)) + if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) + RUNNING_PLUGINS.Add(plugin); + + // Inform all components that the plugins have been reloaded or started: + await MessageBus.INSTANCE.SendMessage(null, Event.PLUGINS_RELOADED); + } + } + + private static async Task Start(IAvailablePlugin meta, CancellationToken cancellationToken = default) + { + var pluginMainFile = Path.Join(meta.LocalPath, "plugin.lua"); + if(!File.Exists(pluginMainFile)) + { + LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: The plugin file does not exist."); + return new NoPlugin($"The plugin file does not exist: {pluginMainFile}"); + } + + var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken); + var plugin = await Load(meta.LocalPath, code, cancellationToken); + if (plugin is NoPlugin noPlugin) + { + LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: {noPlugin.Issues.First()}"); + return noPlugin; + } + + if (plugin.IsValid) + { + // + // When this is a language plugin, we need to set the base language plugin. + // + if (plugin is PluginLanguage languagePlugin && BASE_LANGUAGE_PLUGIN != NoPluginLanguage.INSTANCE) + languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN); + + LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'"); + return plugin; + } + + LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}"); + return new NoPlugin($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}"); + } public static void Dispose() { + if(!IS_INITIALIZED) + return; + HOT_RELOAD_WATCHER.Dispose(); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs index 384d81eb..1c201a11 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs @@ -4,9 +4,12 @@ namespace AIStudio.Tools.PluginSystem; public sealed class PluginLanguage : PluginBase, ILanguagePlugin { + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + private readonly Dictionary content = []; private readonly List otherLanguagePlugins = []; private readonly string langCultureTag; + private readonly string langName; private ILanguagePlugin? baseLanguage; @@ -15,6 +18,9 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin if(!this.TryInitIETFTag(out var issue, out this.langCultureTag)) this.pluginIssues.Add(issue); + if(!this.TryInitLangName(out issue, out this.langName)) + this.pluginIssues.Add(issue); + if (this.TryInitUITextContent(out issue, out var readContent)) this.content = readContent; else @@ -36,39 +42,6 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin /// /// The language plugin to add. public void AddOtherLanguagePlugin(ILanguagePlugin languagePlugin) => this.otherLanguagePlugins.Add(languagePlugin); - - /// - /// Tries to get a text from the language plugin. - /// - /// - /// When the key neither in the base language nor in this language exist, - /// the value will be an empty string. Please note that the key is case-sensitive. - /// Furthermore, the keys are in the format "root::key". That means that - /// the keys are hierarchical and separated by "::". - /// - /// The key to use to get the text. - /// The desired text. - /// True if the key exists, false otherwise. - public bool TryGetText(string key, out string value) - { - // First, we check if the key is part of the main language pack: - if (this.content.TryGetValue(key, out value!)) - return true; - - // Second, we check if the key is part of the other language packs, such as the assistant plugins: - foreach (var otherLanguagePlugin in this.otherLanguagePlugins) - if(otherLanguagePlugin.TryGetText(key, out value)) - return true; - - // Finally, we check if the key is part of the base language pack. This is the case, - // when a language plugin does not cover all keys. In this case, the base language plugin - // will be used to fill in the missing keys: - if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value)) - return true; - - value = string.Empty; - return false; - } /// /// Tries to initialize the IETF tag. @@ -127,4 +100,71 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin message = string.Empty; return true; } + + private bool TryInitLangName(out string message, out string readLangName) + { + if (!this.state.Environment["LANG_NAME"].TryRead(out readLangName)) + { + message = "The field LANG_NAME does not exist or is not a valid string."; + readLangName = string.Empty; + return false; + } + + if (string.IsNullOrWhiteSpace(readLangName)) + { + message = "The field LANG_NAME is empty. Use a valid language name."; + readLangName = string.Empty; + return false; + } + + message = string.Empty; + return true; + } + + #region Implementation of ILanguagePlugin + + /// + /// Tries to get a text from the language plugin. + /// + /// + /// When the key neither in the base language nor in this language exist, + /// the value will be an empty string. Please note that the key is case-sensitive. + /// Furthermore, the keys are in the format "root::key". That means that + /// the keys are hierarchical and separated by "::". + /// + /// The key to use to get the text. + /// The desired text. + /// When true, a warning will be logged if the key does not exist. + /// True if the key exists, false otherwise. + public bool TryGetText(string key, out string value, bool logWarning = false) + { + // First, we check if the key is part of the main language pack: + if (this.content.TryGetValue(key, out value!)) + return true; + + // Second, we check if the key is part of the other language packs, such as the assistant plugins: + foreach (var otherLanguagePlugin in this.otherLanguagePlugins) + if(otherLanguagePlugin.TryGetText(key, out value)) + return true; + + // Finally, we check if the key is part of the base language pack. This is the case, + // when a language plugin does not cover all keys. In this case, the base language plugin + // will be used to fill in the missing keys: + if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value)) + return true; + + if(logWarning) + LOGGER.LogWarning($"Missing translation key '{key}'."); + + value = string.Empty; + return false; + } + + /// + public string IETFTag => this.langCultureTag; + + /// + public string LangName => this.langName; + + #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs index 2bfdab1e..e98644cb 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs @@ -1,6 +1,6 @@ namespace AIStudio.Tools.PluginSystem; -public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata +public sealed class PluginMetadata(PluginBase plugin, string localPath) : IAvailablePlugin { #region Implementation of IPluginMetadata @@ -47,4 +47,10 @@ public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata public bool IsInternal { get; } = plugin.IsInternal; #endregion + + #region Implementation of IAvailablePlugin + + public string LocalPath { get; } = localPath; + + #endregion } \ No newline at end of file