Fixed and guarded the I18N key collection against missing keys (#936)

This commit is contained in:
Thorsten Sommer 2026-08-25 16:53:19 +02:00 committed by GitHub
parent 415ed35422
commit 99089f853e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 164 additions and 34 deletions

View File

@ -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<string>();
var allI18NContent = new Dictionary<string, string>();
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<string> FindAllTextTags(ReadOnlySpan<char> fileContent)
private List<string> FindAllTextTags(ReadOnlySpan<char> fileContent, string filePath, List<string> warnings)
{
(int Index, int Len) FindNextStart(ReadOnlySpan<char> content)
{
@ -182,6 +195,19 @@ public sealed partial class CollectI18NKeysCommand
return (bestIndex, bestLength);
}
int FindNextEnd(ReadOnlySpan<char> 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<string>();
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);
}

View File

@ -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."

View File

@ -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;
}

View File

@ -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())

View File

@ -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."

View File

@ -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."

View File

@ -269,11 +269,14 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
private async Task<string> 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<ShortcutState> GetShortcutState(Shortcut shortcutId, ShortcutSyncSource source)
{
var shortcut = this.GetShortcutValue(shortcutId);

View File

@ -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.
- 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.

View File

@ -14,4 +14,5 @@
MWAIS0008 | Naming | Error | LocalConstantsAnalyzer
MWAIS0009 | Usage | Error | StaticServiceProviderCacheAnalyzer
MWAIS0010 | Usage | Error | CanonicalJsonConfigurationAnalyzer
MWAIS0011 | Usage | Error | CanonicalJsonShapeAnalyzer
MWAIS0011 | Usage | Error | CanonicalJsonShapeAnalyzer
MWAIS0012 | Usage | Error | DirectI18NGetTextAnalyzer

View File

@ -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";
}

View File

@ -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<DiagnosticDescriptor> 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]);
}
}