Fixed the I18N assistant

This commit is contained in:
Thorsten Sommer 2026-07-03 13:46:05 +02:00
parent afb96fd3bb
commit 7c1e023fb2
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
6 changed files with 114 additions and 60 deletions

View File

@ -129,6 +129,11 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
/// </summary> /// </summary>
protected bool IsAssistantComponentDisposed => this.isDisposed; protected bool IsAssistantComponentDisposed => this.isDisposed;
/// <summary>
/// Gets whether this component has attached an assistant session snapshot.
/// </summary>
protected bool HasAssistantSession => this.assistantSessionId is not null;
/// <summary> /// <summary>
/// Gets the assistant-specific identifier used to distinguish session slots. /// Gets the assistant-specific identifier used to distinguish session slots.
/// </summary> /// </summary>

View File

@ -2,8 +2,8 @@
@using AIStudio.Settings @using AIStudio.Settings
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogI18N> @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogI18N>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" SelectionUpdated="_ => this.OnChangedLanguage()" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" SelectionUpdated="_ => this.OnChangedLanguage()" Disabled="@this.IsProcessing" />
<ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.selectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(async void (id) => await this.OnLanguagePluginChanged(id))" OptionHelp="@T("Select the language plugin used for comparision.")"/> <ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.selectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(async void (id) => await this.OnLanguagePluginChanged(id))" OptionHelp="@T("Select the language plugin used for comparision.")" Disabled="@(() => this.IsProcessing)"/>
@if (this.isLoading) @if (this.isLoading)
{ {
<MudText Typo="Typo.body1" Class="mb-6"> <MudText Typo="Typo.body1" Class="mb-6">
@ -20,7 +20,7 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.AddedContentText @this.AddedContentText
</MudText> </MudText>
<MudTable Items="@this.addedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6"> <MudTable Items="@this.AddedContentRows" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent> <ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/> <MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent> </ToolBarContent>
@ -50,7 +50,7 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.RemovedContentText @this.RemovedContentText
</MudText> </MudText>
<MudTable Items="@this.removedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6"> <MudTable Items="@this.RemovedContentRows" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent> <ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/> <MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent> </ToolBarContent>
@ -94,7 +94,7 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue))
<MudText Typo="Typo.h6"> <MudText Typo="Typo.h6">
@this.LocalizedContentText @this.LocalizedContentText
</MudText> </MudText>
<MudTable Items="@this.localizedContent" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6"> <MudTable Items="@this.LocalizedContentRows" Hover="@true" Filter="@this.FilterFunc" Class="border-dashed border rounded-lg mb-6">
<ToolBarContent> <ToolBarContent>
<MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/> <MudTextField @bind-Value="@this.searchString" Immediate="true" Placeholder="@T("Search")" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"/>
</ToolBarContent> </ToolBarContent>

View File

@ -118,6 +118,7 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
private Dictionary<string, string> removedContent = []; private Dictionary<string, string> removedContent = [];
private Dictionary<string, string> localizedContent = []; private Dictionary<string, string> localizedContent = [];
private StringBuilder finalLuaCode = new(); private StringBuilder finalLuaCode = new();
private string? activeSystemPromptLanguage;
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
private static readonly AssistantSessionStateKey<bool> IS_LOADING_STATE_KEY = new(nameof(isLoading)); private static readonly AssistantSessionStateKey<bool> IS_LOADING_STATE_KEY = new(nameof(isLoading));
@ -170,26 +171,34 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
if (this.HasAssistantSession)
return;
await this.OnLanguagePluginChanged(this.selectedLanguagePluginId); await this.OnLanguagePluginChanged(this.selectedLanguagePluginId);
await this.LoadData();
} }
#endregion #endregion
private string SystemPromptLanguage() => this.selectedTargetLanguage switch private string SystemPromptLanguage() => this.activeSystemPromptLanguage ?? (this.selectedTargetLanguage switch
{ {
CommonLanguages.OTHER => this.customTargetLanguage, CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedTargetLanguage.Name()}", _ => $"{this.selectedTargetLanguage.Name()}",
}; });
private async Task OnLanguagePluginChanged(Guid pluginId) private async Task OnLanguagePluginChanged(Guid pluginId)
{ {
if (this.IsProcessing)
return;
this.selectedLanguagePluginId = pluginId; this.selectedLanguagePluginId = pluginId;
await this.OnChangedLanguage(); await this.OnChangedLanguage();
} }
private async Task OnChangedLanguage() private async Task OnChangedLanguage()
{ {
if (this.IsProcessing)
return;
this.finalLuaCode.Clear(); this.finalLuaCode.Clear();
this.localizedContent.Clear(); this.localizedContent.Clear();
this.localizationPossible = false; this.localizationPossible = false;
@ -308,6 +317,21 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
private int NumTotalItems => (this.selectedLanguagePlugin?.Content.Count ?? 0) + this.addedContent.Count - this.removedContent.Count; private int NumTotalItems => (this.selectedLanguagePlugin?.Content.Count ?? 0) + this.addedContent.Count - this.removedContent.Count;
/// <summary>
/// Gets a stable row snapshot for the added-content table.
/// </summary>
private KeyValuePair<string, string>[] AddedContentRows => this.addedContent.ToArray();
/// <summary>
/// Gets a stable row snapshot for the removed-content table.
/// </summary>
private KeyValuePair<string, string>[] RemovedContentRows => this.removedContent.ToArray();
/// <summary>
/// Gets a stable row snapshot for the localized-content table.
/// </summary>
private KeyValuePair<string, string>[] LocalizedContentRows => this.localizedContent.ToArray();
private string AddedContentText => string.Format(T("Added Content ({0} entries)"), this.addedContent.Count); private string AddedContentText => string.Format(T("Added Content ({0} entries)"), this.addedContent.Count);
private string RemovedContentText => string.Format(T("Removed Content ({0} entries)"), this.removedContent.Count); private string RemovedContentText => string.Format(T("Removed Content ({0} entries)"), this.removedContent.Count);
@ -326,68 +350,87 @@ public partial class AssistantI18N : AssistantBaseCore<SettingsDialogI18N>
if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag()) if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag())
return; return;
var addedContentSnapshot = this.addedContent.ToArray();
var removedContentSnapshot = this.removedContent.ToArray();
var removedContentKeys = removedContentSnapshot.Select(keyValuePair => keyValuePair.Key).ToHashSet(StringComparer.Ordinal);
var selectedLanguageContentSnapshot = this.selectedLanguagePlugin.Content.ToArray();
var baseLanguageContentSnapshot = PluginFactory.BaseLanguage.Content.ToArray();
this.localizedContent.Clear(); this.localizedContent.Clear();
if (this.selectedTargetLanguage is not CommonLanguages.EN_US) this.activeSystemPromptLanguage = this.SystemPromptLanguage();
try
{ {
// Phase 1: Translate added content if (this.selectedTargetLanguage is not CommonLanguages.EN_US)
await this.Phase1TranslateAddedContent(); {
// Phase 1: Translate added content
await this.Phase1TranslateAddedContent(addedContentSnapshot);
}
else
{
// Case: no translation needed
this.localizedContent = addedContentSnapshot.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value, StringComparer.Ordinal);
}
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 selectedLanguageContentSnapshot)
{
if (this.CancellationTokenSource!.IsCancellationRequested)
break;
if (this.localizedContent.ContainsKey(keyValuePair.Key))
continue;
if (removedContentKeys.Contains(keyValuePair.Key))
continue;
this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value);
}
if(this.CancellationTokenSource!.IsCancellationRequested)
return;
//
// Phase 2: Create the Lua code. We want to use the base language
// for the comments, though:
//
var commentContent = addedContentSnapshot.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value, StringComparer.Ordinal);
foreach (var keyValuePair in baseLanguageContentSnapshot)
{
if (this.CancellationTokenSource!.IsCancellationRequested)
break;
if (removedContentKeys.Contains(keyValuePair.Key))
continue;
commentContent.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
this.Phase2CreateLuaCode(commentContent);
} }
else finally
{ {
// Case: no translation needed this.activeSystemPromptLanguage = null;
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. We want to use the base language
// for the comments, though:
//
var commentContent = new Dictionary<string, string>(this.addedContent);
foreach (var keyValuePair in PluginFactory.BaseLanguage.Content)
{
if (this.CancellationTokenSource!.IsCancellationRequested)
break;
if (this.removedContent.ContainsKey(keyValuePair.Key))
continue;
commentContent.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
this.Phase2CreateLuaCode(commentContent);
} }
private async Task Phase1TranslateAddedContent() /// <summary>
/// Translates the added text content from a stable snapshot.
/// </summary>
/// <param name="addedContentSnapshot">The added text entries captured when the job started.</param>
/// <returns>A task that completes when all added text entries were translated or cancellation was requested.</returns>
private async Task Phase1TranslateAddedContent(KeyValuePair<string, string>[] addedContentSnapshot)
{ {
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
var minimumTime = TimeSpan.FromMilliseconds(500); var minimumTime = TimeSpan.FromMilliseconds(500);
foreach (var keyValuePair in this.addedContent) foreach (var keyValuePair in addedContentSnapshot)
{ {
if(this.CancellationTokenSource!.IsCancellationRequested) if(this.CancellationTokenSource!.IsCancellationRequested)
break; break;

View File

@ -93,7 +93,7 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
private AssistantSessionIndicatorData? AssistantSessionIndicator => this.AssistantSessionSnapshot?.Status switch private AssistantSessionIndicatorData? AssistantSessionIndicator => this.AssistantSessionSnapshot?.Status switch
{ {
AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING => new(Icons.Material.Filled.ChangeCircle, Color.Info, this.T("Assistant is still running.")), AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING => new(Icons.Material.Filled.ChangeCircle, Color.Info, this.T("Assistant is still running.")),
AssistantSessionStatus.COMPLETED => new(Icons.Material.Filled.TaskAlt, Color.Success, this.T("Result is ready.")), AssistantSessionStatus.COMPLETED => new(Icons.Material.Filled.TaskAlt, Color.Success, this.T("The result is ready.")),
AssistantSessionStatus.FAILED => new(Icons.Material.Filled.Error, Color.Error, this.T("Assistant failed. Open it to review the result.")), AssistantSessionStatus.FAILED => new(Icons.Material.Filled.Error, Color.Error, this.T("Assistant failed. Open it to review the result.")),
AssistantSessionStatus.CANCELED => new(Icons.Material.Filled.Cancel, Color.Warning, this.T("Assistant was canceled. Open it to review the result.")), AssistantSessionStatus.CANCELED => new(Icons.Material.Filled.Cancel, Color.Warning, this.T("Assistant was canceled. Open it to review the result.")),
_ => null, _ => null,

View File

@ -2,7 +2,7 @@
@inherits EnumSelectionBase @inherits EnumSelectionBase
<MudStack Row="@true" Class="mb-3"> <MudStack Row="@true" Class="mb-3">
<MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection"> <MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection" Disabled="@this.Disabled">
@foreach (var value in Enum.GetValues<T>()) @foreach (var value in Enum.GetValues<T>())
{ {
<MudSelectItem Value="@value"> <MudSelectItem Value="@value">
@ -12,6 +12,6 @@
</MudSelect> </MudSelect>
@if (this.AllowOther && this.Value.Equals(this.OtherValue)) @if (this.AllowOther && this.Value.Equals(this.OtherValue))
{ {
<MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherValueChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/> <MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherValueChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true" Disabled="@this.Disabled"/>
} }
</MudStack> </MudStack>

View File

@ -39,6 +39,12 @@ public partial class EnumSelection<T> : EnumSelectionBase where T : struct, Enum
[Parameter] [Parameter]
public string Icon { get; set; } = Icons.Material.Filled.ArrowDropDown; public string Icon { get; set; } = Icons.Material.Filled.ArrowDropDown;
/// <summary>
/// Gets or sets whether the selection controls are disabled.
/// </summary>
[Parameter]
public bool Disabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets the custom name function for selecting the display name of an enum value. /// Gets or sets the custom name function for selecting the display name of an enum value.
/// </summary> /// </summary>