diff --git a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor
index 38e9255f..ffb25555 100644
--- a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor
+++ b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor
@@ -87,4 +87,38 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
{
}
+
+ @if (this.localizedContent.Count > 0)
+ {
+
+
+ Localized Content (@this.localizedContent.Count entries of @this.addedContent.Count)
+
+
+
+
+
+
+
+
+
+
+ Key
+ Text
+
+
+
+
+ @context.Key
+
+
+
+ @context.Value
+
+
+
+
+
+
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs
index 592ff8b4..e644d1b2 100644
--- a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs
+++ b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs
@@ -1,8 +1,13 @@
+using System.Diagnostics;
+using System.Text;
+
using AIStudio.Dialogs.Settings;
using AIStudio.Tools.PluginSystem;
using Microsoft.Extensions.FileProviders;
+using SharedTools;
+
namespace AIStudio.Assistants.I18N;
public partial class AssistantI18N : AssistantBaseCore
@@ -17,22 +22,55 @@ public partial class AssistantI18N : AssistantBaseCore
""";
protected override string SystemPrompt =>
- """
- TODO
+ $"""
+ # Assignment
+ You are an expert in professional translations from English (US) to {this.SystemPromptLanguage()}.
+ You translate the texts without adding any new information. When necessary, you correct
+ spelling and grammar.
+
+ # Context
+ The texts to be translated come from the open source app "MindWork AI Studio". The goal
+ is to localize the app so that it can be offered in other languages. You will always
+ receive one text at a time. A text may be, for example, for a button, a label, or an
+ explanation within the app. The app "AI Studio" is a desktop app for macOS, Linux,
+ and Windows. Users can use Large Language Models (LLMs) in practical ways in their
+ daily lives with it. The app offers the regular chat mode for which LLMs have become
+ known. However, AI Studio also offers so-called assistants, where users no longer
+ have to prompt.
+
+ # Target Audience
+ The app is intended for everyone, not just IT specialists or scientists. When translating,
+ make sure the texts are easy for everyone to understand.
""";
protected override bool AllowProfiles => false;
protected override bool ShowResult => false;
- protected override IReadOnlyList FooterButtons => [];
+ protected override bool ShowCopyResult => false;
+
+ protected override bool ShowSendTo => false;
+
+ protected override IReadOnlyList FooterButtons =>
+ [
+ new ButtonData
+ {
+ Text = "Copy Lua code to clipboard",
+ Icon = Icons.Material.Filled.Extension,
+ Color = Color.Default,
+ AsyncAction = async () => await this.RustService.CopyText2Clipboard(this.Snackbar, this.finalLuaCode.ToString()),
+ DisabledActionParam = () => this.finalLuaCode.Length == 0,
+ },
+ ];
protected override string SubmitText => "Localize AI Studio & generate the Lua code";
- protected override Func SubmitAction => this.LocalizeTextConntent;
+ protected override Func SubmitAction => this.LocalizeTextContent;
protected override bool SubmitDisabled => !this.localizationPossible;
+ protected override bool ShowDedicatedProgress => true;
+
protected override void ResetForm()
{
if (!this.MightPreselectValues())
@@ -68,6 +106,8 @@ public partial class AssistantI18N : AssistantBaseCore
private ILanguagePlugin? selectedLanguagePlugin;
private Dictionary addedContent = [];
private Dictionary removedContent = [];
+ private Dictionary localizedContent = [];
+ private StringBuilder finalLuaCode = new();
#region Overrides of AssistantBase
@@ -80,6 +120,12 @@ public partial class AssistantI18N : AssistantBaseCore
#endregion
+ private string SystemPromptLanguage() => this.selectedTargetLanguage switch
+ {
+ CommonLanguages.OTHER => this.customTargetLanguage,
+ _ => $"{this.selectedTargetLanguage.Name()}",
+ };
+
private async Task OnLanguagePluginChanged(Guid pluginId)
{
this.selectedLanguagePluginId = pluginId;
@@ -89,9 +135,17 @@ public partial class AssistantI18N : AssistantBaseCore
private async Task OnChangedLanguage()
{
if (PluginFactory.RunningPlugins.FirstOrDefault(n => n is PluginLanguage && n.Id == this.selectedLanguagePluginId) is not PluginLanguage comparisonPlugin)
+ {
this.loadingIssue = $"Was not able to load the language plugin for comparison ({this.selectedLanguagePluginId}). Please select a valid, loaded & running language plugin.";
+ this.localizationPossible = false;
+ this.selectedLanguagePlugin = null;
+ }
else if (comparisonPlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
+ {
this.loadingIssue = $"The selected language plugin for comparison uses the IETF tag '{comparisonPlugin.IETFTag}' which does not match the selected target language '{this.selectedTargetLanguage.ToIETFTag()}'. Please select a valid, loaded & running language plugin which matches the target language.";
+ this.localizationPossible = false;
+ this.selectedLanguagePlugin = null;
+ }
else
{
this.selectedLanguagePlugin = comparisonPlugin;
@@ -195,7 +249,99 @@ public partial class AssistantI18N : AssistantBaseCore
return null;
}
- private async Task LocalizeTextConntent()
+ private async Task LocalizeTextContent()
{
+ await this.form!.Validate();
+ if (!this.inputIsValid)
+ return;
+
+ if(this.selectedLanguagePlugin is null)
+ return;
+
+ if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
+ return;
+
+ this.localizedContent.Clear();
+ if (this.selectedTargetLanguage is not CommonLanguages.EN_US)
+ {
+ // Phase 1: Translate added content
+ await this.Phase1TranslateAddedContent();
+ }
+ else
+ {
+ // Case: no translation needed
+ this.localizedContent = this.addedContent.ToDictionary();
+ }
+
+ if(this.cancellationTokenSource!.IsCancellationRequested)
+ return;
+
+ //
+ // Now, we have localized the added content. Next, we must merge
+ // the localized content with the existing content. However, we
+ // must skip the removed content. We use the localizedContent
+ // dictionary for the final result:
+ //
+ foreach (var keyValuePair in this.selectedLanguagePlugin.Content)
+ {
+ if (this.cancellationTokenSource!.IsCancellationRequested)
+ break;
+
+ if (this.localizedContent.ContainsKey(keyValuePair.Key))
+ continue;
+
+ if (this.removedContent.ContainsKey(keyValuePair.Key))
+ continue;
+
+ this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value);
+ }
+
+ if(this.cancellationTokenSource!.IsCancellationRequested)
+ return;
+
+ // Phase 2: Create the Lua code
+ this.Phase2CreateLuaCode();
+ }
+
+ private async Task Phase1TranslateAddedContent()
+ {
+ var stopwatch = new Stopwatch();
+ var minimumTime = TimeSpan.FromMilliseconds(500);
+ foreach (var keyValuePair in this.addedContent)
+ {
+ if(this.cancellationTokenSource!.IsCancellationRequested)
+ break;
+
+ //
+ // We measure the time for each translation.
+ // We do not want to make more than 120 requests
+ // per minute, i.e., 2 requests per second.
+ //
+ stopwatch.Reset();
+ stopwatch.Start();
+
+ //
+ // Translate one text at a time:
+ //
+ this.CreateChatThread();
+ var time = this.AddUserRequest(keyValuePair.Value);
+ this.localizedContent.Add(keyValuePair.Key, await this.AddAIResponseAsync(time));
+
+ if (this.cancellationTokenSource!.IsCancellationRequested)
+ break;
+
+ //
+ // Ensure that we do not exceed the rate limit of 2 requests per second:
+ //
+ stopwatch.Stop();
+ if (stopwatch.Elapsed < minimumTime)
+ await Task.Delay(minimumTime - stopwatch.Elapsed);
+ }
+ }
+
+ private void Phase2CreateLuaCode()
+ {
+ this.finalLuaCode.Clear();
+ LuaTable.Create(ref this.finalLuaCode, "UI_TEXT_CONTENT", this.localizedContent, this.cancellationTokenSource!.Token);
}
}
\ No newline at end of file