From 99089f853e04e71623c6a2c95096e7f30c00a9da Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 25 Aug 2026 16:53:19 +0200 Subject: [PATCH] Fixed and guarded the I18N key collection against missing keys (#936) --- app/Build/Commands/CollectI18NKeysCommand.cs | 55 ++++++++++++--- .../Assistants/I18N/allTexts.lua | 18 +++-- .../Dialogs/ProviderDialog.razor.cs | 4 +- app/MindWork AI Studio/Pages/Plugins.razor.cs | 2 +- .../plugin.lua | 18 +++-- .../plugin.lua | 18 +++-- .../Tools/Services/GlobalShortcutService.cs | 9 ++- .../wwwroot/changelog/v26.8.2.md | 3 +- .../AnalyzerReleases.Shipped.md | 3 +- .../SourceCodeRules/Identifier.cs | 1 + .../DirectI18NGetTextAnalyzer.cs | 67 +++++++++++++++++++ 11 files changed, 164 insertions(+), 34 deletions(-) create mode 100644 app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs diff --git a/app/Build/Commands/CollectI18NKeysCommand.cs b/app/Build/Commands/CollectI18NKeysCommand.cs index 760a018a..2b4dbd96 100644 --- a/app/Build/Commands/CollectI18NKeysCommand.cs +++ b/app/Build/Commands/CollectI18NKeysCommand.cs @@ -22,9 +22,13 @@ public sealed partial class CollectI18NKeysCommand T(@" """; - private const string END_TAG = """ - ") - """; + private const string END_TAG1 = """ + ") + """; + + private const string END_TAG2 = """ + ", + """; private static readonly (string Tag, int Length)[] START_TAGS = [ @@ -32,6 +36,12 @@ public sealed partial class CollectI18NKeysCommand (START_TAG2, START_TAG2.Length), (START_TAG3, START_TAG3.Length) ]; + + private static readonly string[] END_TAGS = + [ + END_TAG1, + END_TAG2 + ]; [Command("collect-i18n", Description = "Collect I18N keys")] public async Task CollectI18NKeys() @@ -49,6 +59,7 @@ public sealed partial class CollectI18NKeysCommand var allFiles = Directory.EnumerateFiles(cwd, "*", SearchOption.AllDirectories); var counter = 0; + var warnings = new List(); var allI18NContent = new Dictionary(); foreach (var filePath in allFiles) { @@ -66,7 +77,7 @@ public sealed partial class CollectI18NKeysCommand continue; var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8); - var matches = this.FindAllTextTags(content); + var matches = this.FindAllTextTags(content, filePath, warnings); if (matches.Count == 0) continue; @@ -89,7 +100,9 @@ public sealed partial class CollectI18NKeysCommand } Console.WriteLine($" {counter:###,###} files processed, {allI18NContent.Count:###,###} keys found."); - + foreach (var warning in warnings) + Console.WriteLine(warning); + Console.Write("- Creating Lua code ..."); var luaCode = this.ExportToLuaAssignments(allI18NContent); @@ -163,7 +176,7 @@ public sealed partial class CollectI18NKeysCommand return sb.ToString(); } - private List FindAllTextTags(ReadOnlySpan fileContent) + private List FindAllTextTags(ReadOnlySpan fileContent, string filePath, List warnings) { (int Index, int Len) FindNextStart(ReadOnlySpan content) { @@ -182,6 +195,19 @@ public sealed partial class CollectI18NKeysCommand return (bestIndex, bestLength); } + + int FindNextEnd(ReadOnlySpan content) + { + var bestIndex = -1; + foreach (var tag in END_TAGS) + { + var index = content.IndexOf(tag); + if (index != -1 && (bestIndex == -1 || index < bestIndex)) + bestIndex = index; + } + + return bestIndex; + } var matches = new List(); var startIdx = FindNextStart(fileContent); @@ -196,15 +222,26 @@ public sealed partial class CollectI18NKeysCommand while(content[0] == '"') content = content[1..]; - var endIdx = content.IndexOf(END_TAG); + var endIdx = FindNextEnd(content); if (endIdx == -1) break; var match = content[..endIdx]; while (match[^1] == '"') match = match[..^1]; - - matches.Add(match.ToString()); + + var text = match.ToString(); + + // + // We read the raw source text, whereas the app hashes the unescaped string at + // runtime. Thus, any escape sequence makes both hashes differ, so that the text + // never finds its translation. Since we cannot detect this at runtime, we warn + // about it here: + // + if(text.Contains('\\')) + warnings.Add($"- Warning: The text '{text}' in the file '{filePath}' contains an escape sequence. Its key does not match the key the app looks up at runtime, so this text stays untranslated. Please use a raw string literal instead."); + + matches.Add(text); startIdx = FindNextStart(content); } diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 1ffeda02..295ecf4e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6238,6 +6238,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2646845972"] = "Add" -- Additional API parameters UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2728244552"] = "Additional API parameters" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2765821959"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." + -- No models loaded or available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2810182573"] = "No models loaded or available." @@ -6271,9 +6274,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Show Expert -- Audio input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3404621481"] = "Audio input" --- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \\\"temperature\\\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." - -- Reasoning (thinking) behavior UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3546126752"] = "Reasoning (thinking) behavior" @@ -8851,6 +8851,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2222816203"] = "Plugins" -- Edit Assistant Plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2477579768"] = "Edit Assistant Plugin" +-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin? +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2608443050"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?" + -- Enabled Plugins UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins" @@ -8905,9 +8908,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4157246824"] = "The assistant plugin -- Open website UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Open website" --- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \"{2}\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin? -UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \\\"{2}\\\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?" - -- The plugin archive was exported to '{0}'. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T659549952"] = "The plugin archive was exported to '{0}'." @@ -10567,9 +10567,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] -- The global shortcut could not be registered. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "The global shortcut could not be registered. The previous shortcut remains active." +-- Global shortcut +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2637055764"] = "Global shortcut" + -- The global shortcut change was cancelled. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "The global shortcut change was cancelled. The previous shortcut remains active." +-- Toggle voice recording +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T40517664"] = "Toggle voice recording" + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "The configured transcription provider could not be created." diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index ae8362c3..f491da1c 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -631,7 +631,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId } catch (JsonException) { - return T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."); + return T("""Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."""); } } @@ -764,7 +764,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId if (objectStack.Count != 0) { - errorMessage = T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."); + errorMessage = T("""Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."""); return false; } diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs index 7ff391e1..4fca17fd 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor.cs +++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs @@ -175,7 +175,7 @@ public partial class Plugins : MSGComponentBase { x => x.Message, string.Format( - this.T("The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \"{2}\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"), + this.T("The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"), pluginName, actualLevel.GetName(), this.AssistantPluginAuditSettings.MinimumLevel.GetName()) 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 9e7cf65d..46cf1592 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 @@ -6240,6 +6240,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2646845972"] = "Hinzufügen -- Additional API parameters UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2728244552"] = "Zusätzliche API-Parameter" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2765821959"] = "Ungültiges JSON: Fügen Sie die Parameter in korrektem JSON-Format hinzu, z. B. "temperature": 0.5. Entfernen Sie abschließende Kommas. Die üblichen umgebenden geschweiften Klammern {} dürfen jedoch nicht verwendet werden." + -- No models loaded or available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2810182573"] = "Keine Modelle geladen oder verfügbar." @@ -6273,9 +6276,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Experten-Ei -- Audio input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3404621481"] = "Audioeingabe" --- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Ungültiges JSON: Fügen Sie die Parameter in korrektem JSON-Format hinzu, z. B. \"temperature\": 0.5. Entfernen Sie abschließende Kommas. Die üblichen umgebenden geschweiften Klammern {} dürfen jedoch nicht verwendet werden." - -- Reasoning (thinking) behavior UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3546126752"] = "Verhalten bezüglich Schlussfolgerungen (Denken)" @@ -8853,6 +8853,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2222816203"] = "Plugins" -- Edit Assistant Plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2477579768"] = "Plugin für „Assistent bearbeiten“" +-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin? +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2608443050"] = "Das Assistenten-Plugin „{0}“ wurde mit der Stufe „{1}“ geprüft, die unter der erforderlichen Mindeststufe „{2}“ liegt. Ihre aktuellen Einstellungen erlauben die Aktivierung dennoch, dies kann jedoch potenziell gefährlich sein. Möchten Sie dieses Plugin wirklich aktivieren?" + -- Enabled Plugins UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Aktivierte Plugins" @@ -8907,9 +8910,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4157246824"] = "Das Assistenten-Plug -- Open website UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Website öffnen" --- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin? -UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "Das Assistenten-Plugin „{0}“ wurde mit der Stufe „{1}“ geprüft, die unter der erforderlichen Mindeststufe „{2}“ liegt. Ihre aktuellen Einstellungen erlauben die Aktivierung dennoch, dies kann jedoch potenziell gefährlich sein. Möchten Sie dieses Plugin wirklich aktivieren?" - -- The plugin archive was exported to '{0}'. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T659549952"] = "Das Plugin-Archiv wurde nach „{0}“ exportiert." @@ -10569,9 +10569,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] -- The global shortcut could not be registered. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "Die globale Tastenkombination konnte nicht registriert werden. Die vorherige Tastenkombination bleibt aktiv." +-- Global shortcut +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2637055764"] = "Globale Tastenkombination" + -- The global shortcut change was cancelled. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "Die Änderung der globalen Tastenkombination wurde abgebrochen. Die vorherige Tastenkombination bleibt aktiv." +-- Toggle voice recording +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T40517664"] = "Sprachaufnahme umschalten" + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "Der konfigurierte Transkriptionsanbieter konnte nicht erstellt werden." 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 228a1c3f..1c57776f 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 @@ -6240,6 +6240,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2646845972"] = "Add" -- Additional API parameters UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2728244552"] = "Additional API parameters" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2765821959"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." + -- No models loaded or available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2810182573"] = "No models loaded or available." @@ -6273,9 +6276,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Show Expert -- Audio input UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3404621481"] = "Audio input" --- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \\\"temperature\\\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." - -- Reasoning (thinking) behavior UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3546126752"] = "Reasoning (thinking) behavior" @@ -8853,6 +8853,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2222816203"] = "Plugins" -- Edit Assistant Plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2477579768"] = "Edit Assistant Plugin" +-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin? +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2608443050"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?" + -- Enabled Plugins UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins" @@ -8907,9 +8910,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4157246824"] = "The assistant plugin -- Open website UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Open website" --- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin? -UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?" - -- The plugin archive was exported to '{0}'. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T659549952"] = "The plugin archive was exported to '{0}'." @@ -10569,9 +10569,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] -- The global shortcut could not be registered. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "The global shortcut could not be registered. The previous shortcut remains active." +-- Global shortcut +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2637055764"] = "Global shortcut" + -- The global shortcut change was cancelled. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "The global shortcut change was cancelled. The previous shortcut remains active." +-- Toggle voice recording +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T40517664"] = "Toggle voice recording" + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "The configured transcription provider could not be created." diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index fd04a0d5..9e24f786 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -269,11 +269,14 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private async Task GetShortcutDescription(Shortcut shortcutId) { + // Message bus receivers run without being awaited, so I18N.Init in MainLayout may still be + // pending during startup or a plugin reload. Resolve the active language directly to avoid + // using no language or the previously active language for the shortcut description: var language = await this.settingsManager.GetActiveLanguagePlugin(); return shortcutId switch { - Shortcut.VOICE_RECORDING_TOGGLE => I18N.I.GetText(language, "Toggle voice recording", typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)), - _ => I18N.I.GetText(language, "Global shortcut", typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)), + Shortcut.VOICE_RECORDING_TOGGLE => TB("Toggle voice recording", language), + _ => TB("Global shortcut", language), }; } @@ -320,6 +323,8 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)); + private static string TB(string fallbackEN, ILanguagePlugin language) => I18N.I.GetText(language, fallbackEN, typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)); + private async Task GetShortcutState(Shortcut shortcutId, ShortcutSyncSource source) { var shortcut = this.GetShortcutValue(shortcutId); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 2375e90a..c10a8d02 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -7,4 +7,5 @@ - Fixed AI Studio reading a document to the end even after you closed its preview. Closing the dialog now stops that work immediately. - Fixed AI Studio holding on to finished chats, presentation images, and plugin data. It releases them now, so memory no longer grows the longer you keep the app running. - Fixed assistants handing an outdated result to the chat. When you sent several results without opening the chat in between, you now receive the one you sent last. -- Fixed rare issues when multiple configurations provided introduction texts or mandatory information under the same ID. \ No newline at end of file +- Fixed rare issues when multiple configurations provided introduction texts or mandatory information under the same ID. +- Fixed the description of the global voice recording shortcut always appearing in English. On Linux, your desktop asks you to confirm such a shortcut and shows this description; it now appears in your language. \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index 5aeec96c..1fc28a60 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -14,4 +14,5 @@ MWAIS0008 | Naming | Error | LocalConstantsAnalyzer MWAIS0009 | Usage | Error | StaticServiceProviderCacheAnalyzer MWAIS0010 | Usage | Error | CanonicalJsonConfigurationAnalyzer - MWAIS0011 | Usage | Error | CanonicalJsonShapeAnalyzer \ No newline at end of file + MWAIS0011 | Usage | Error | CanonicalJsonShapeAnalyzer + MWAIS0012 | Usage | Error | DirectI18NGetTextAnalyzer diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs index cf53127f..2ca33b49 100644 --- a/app/SourceCodeRules/SourceCodeRules/Identifier.cs +++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs @@ -13,4 +13,5 @@ public static class Identifier public const string STATIC_SERVICE_PROVIDER_CACHE_ANALYZER = $"{Tools.ID_PREFIX}0009"; public const string CANONICAL_JSON_CONFIGURATION_ANALYZER = $"{Tools.ID_PREFIX}0010"; public const string CANONICAL_JSON_SHAPE_ANALYZER = $"{Tools.ID_PREFIX}0011"; + public const string DIRECT_I18N_GET_TEXT_ANALYZER = $"{Tools.ID_PREFIX}0012"; } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs new file mode 100644 index 00000000..be800ffa --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/DirectI18NGetTextAnalyzer.cs @@ -0,0 +1,67 @@ +using System.Collections.Immutable; +using System.Linq; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace SourceCodeRules.UsageAnalyzers; + +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public sealed class DirectI18NGetTextAnalyzer : DiagnosticAnalyzer +{ + private const string DIAGNOSTIC_ID = Identifier.DIRECT_I18N_GET_TEXT_ANALYZER; + + private const string TITLE = "Direct translation lookup is not allowed"; + + private const string MESSAGE_FORMAT = "Call GetText only from a T or TB wrapper whose first string parameter is forwarded as the fallback text"; + + private const string DESCRIPTION = "Translation calls must use collector-compatible T or TB wrappers so that every fallback text is included in the generated I18N resources."; + + private const string CATEGORY = "Usage"; + + private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); + + public override ImmutableArray SupportedDiagnostics => [RULE]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + return; + + var targetMethod = method.ReducedFrom ?? method; + if (targetMethod.Name != "GetText" || targetMethod.ContainingType.Name != "ILangExtensions" || targetMethod.ContainingNamespace.ToDisplayString() != "AIStudio.Tools.PluginSystem") + return; + + if (context.SemanticModel.GetOperation(invocation) is IInvocationOperation operation + && IsCollectorCompatibleWrapper(context.ContainingSymbol as IMethodSymbol, operation)) + return; + + context.ReportDiagnostic(Diagnostic.Create(RULE, invocation.GetLocation())); + } + + private static bool IsCollectorCompatibleWrapper(IMethodSymbol? containingMethod, IInvocationOperation invocation) + { + if (containingMethod?.Name is not ("T" or "TB") + || containingMethod.ReturnType.SpecialType != SpecialType.System_String + || containingMethod.Parameters.Length == 0 + || containingMethod.Parameters[0].Type.SpecialType != SpecialType.System_String) + return false; + + var fallbackArgument = invocation.Arguments.FirstOrDefault(argument => argument.Parameter?.Name == "fallbackEN"); + return fallbackArgument?.Value is IParameterReferenceOperation parameterReference + && SymbolEqualityComparer.Default.Equals(parameterReference.Parameter, containingMethod.Parameters[0]); + } +} \ No newline at end of file