Repository navigation
Extract strings from LocalizedString and LocalizedHtmlString constructors and Create calls #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
MikeAlhayek
wants to merge
5
commits into
OrchardCoreContrib:main
from
MikeAlhayek:localized-string-extractor
Closed
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
721a7c7
Extract strings from new LocalizedString("Foo", "Foo")
MikeAlhayek d860165
Also extract strings from new LocalizedHtmlString("Foo", "Foo")
MikeAlhayek 40a61bc
Support the single argument LocalizedString and LocalizedHtmlString form
MikeAlhayek c2d0eb2
Support LocalizedString.Create and address review feedback
MikeAlhayek b870a83
Cover Create calls with format arguments
MikeAlhayek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
src/OrchardCoreContrib.PoExtractor.DotNet.CS/ExpressionSyntaxExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
||
| namespace OrchardCoreContrib.PoExtractor.DotNet.CS; | ||
|
|
||
| internal static class ExpressionSyntaxExtensions | ||
| { | ||
| /// <summary> | ||
| /// Gets the value of a string literal, or of string literals joined with the + operator. | ||
| /// </summary> | ||
| /// <param name="expression">The <see cref="ExpressionSyntax"/>.</param> | ||
| /// <param name="value">The string value.</param> | ||
| public static bool TryGetString(this ExpressionSyntax expression, out string value) | ||
| { | ||
| if (expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) | ||
| { | ||
| value = literal.Token.ValueText; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| if (expression is BinaryExpressionSyntax binary && | ||
| binary.IsKind(SyntaxKind.AddExpression) && | ||
| binary.Left.TryGetString(out var left) && | ||
| binary.Right.TryGetString(out var right)) | ||
| { | ||
| value = left + right; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| value = null; | ||
|
|
||
| return false; | ||
| } | ||
| } |
69 changes: 69 additions & 0 deletions
69
src/OrchardCoreContrib.PoExtractor.DotNet.CS/LocalizedStringExtractor.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using System; | ||
| using System.Linq; | ||
|
|
||
| namespace OrchardCoreContrib.PoExtractor.DotNet.CS; | ||
|
|
||
| /// <summary> | ||
| /// Extracts <see cref="LocalizableStringOccurence"/> with the singular text from the C# AST node | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The localizable string is identified by the constructor call - new LocalizedString("TEXT TO TRANSLATE", "TEXT TO TRANSLATE") | ||
| /// or new LocalizedHtmlString("TEXT TO TRANSLATE", "TEXT TO TRANSLATE"). The single argument form | ||
| /// new LocalizedString("TEXT TO TRANSLATE") or new LocalizedHtmlString("TEXT TO TRANSLATE") is also supported. | ||
| /// The factory method call - LocalizedString.Create("TEXT TO TRANSLATE") or LocalizedHtmlString.Create("TEXT TO TRANSLATE") | ||
| /// is also supported, including the LocalizedStringExtensions.Create("TEXT TO TRANSLATE") and | ||
| /// LocalizedHtmlStringExtensions.Create("TEXT TO TRANSLATE") forms. | ||
| /// </remarks> | ||
| /// <remarks> | ||
| /// Creates a new instance of a <see cref="LocalizedStringExtractor"/>. | ||
| /// </remarks> | ||
| /// <param name="metadataProvider">The <see cref="IMetadataProvider{TNode}"/>.</param> | ||
| public class LocalizedStringExtractor(IMetadataProvider<SyntaxNode> metadataProvider) : LocalizableStringExtractor<SyntaxNode>(metadataProvider) | ||
| { | ||
| /// <inheritdoc/> | ||
| public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(node); | ||
|
|
||
| result = null; | ||
|
|
||
| var argumentList = node switch | ||
| { | ||
| ObjectCreationExpressionSyntax creation when IsTypeName(creation.Type, LocalizedStringTypes.TypeNames) => creation.ArgumentList, | ||
| InvocationExpressionSyntax invocation when IsFactoryMethod(invocation.Expression) => invocation.ArgumentList, | ||
| _ => null | ||
| }; | ||
|
|
||
| if (argumentList != null && | ||
| argumentList.Arguments.Count > 0 && | ||
| argumentList.Arguments[0].Expression.TryGetString(out var value)) | ||
| { | ||
| result = CreateLocalizedString(value, null, node); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private static bool IsFactoryMethod(ExpressionSyntax expression) | ||
| => expression is MemberAccessExpressionSyntax memberAccess && | ||
| memberAccess.Name.Identifier.Text == LocalizedStringTypes.FactoryMethodName && | ||
| IsTypeName(memberAccess.Expression, LocalizedStringTypes.FactoryTypeNames); | ||
|
|
||
| private static bool IsTypeName(ExpressionSyntax expression, string[] typeNames) | ||
| { | ||
| var name = expression switch | ||
| { | ||
| IdentifierNameSyntax identifierName => identifierName.Identifier.Text, | ||
| QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.Text, | ||
| AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.Text, | ||
| MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text, | ||
| _ => null | ||
| }; | ||
|
|
||
| return name != null && typeNames.Contains(name); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
src/OrchardCoreContrib.PoExtractor.DotNet.VB/LocalizedStringExtractor.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.VisualBasic; | ||
| using Microsoft.CodeAnalysis.VisualBasic.Syntax; | ||
| using System; | ||
| using System.Linq; | ||
|
|
||
| namespace OrchardCoreContrib.PoExtractor.DotNet.VB; | ||
|
|
||
| /// <summary> | ||
| /// Extracts <see cref="LocalizableStringOccurence"/> with the singular text from the VB AST node | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The localizable string is identified by the constructor call - New LocalizedString("TEXT TO TRANSLATE", "TEXT TO TRANSLATE") | ||
| /// or New LocalizedHtmlString("TEXT TO TRANSLATE", "TEXT TO TRANSLATE"). The single argument form | ||
| /// New LocalizedString("TEXT TO TRANSLATE") or New LocalizedHtmlString("TEXT TO TRANSLATE") is also supported. | ||
| /// The factory method call - LocalizedString.Create("TEXT TO TRANSLATE") or LocalizedHtmlString.Create("TEXT TO TRANSLATE") | ||
| /// is also supported, including the LocalizedStringExtensions.Create("TEXT TO TRANSLATE") and | ||
| /// LocalizedHtmlStringExtensions.Create("TEXT TO TRANSLATE") forms. | ||
| /// </remarks> | ||
| /// <remarks> | ||
| /// Creates a new instance of a <see cref="LocalizedStringExtractor"/>. | ||
| /// </remarks> | ||
| /// <param name="metadataProvider">The <see cref="IMetadataProvider{TNode}"/>.</param> | ||
| public class LocalizedStringExtractor(IMetadataProvider<SyntaxNode> metadataProvider) : LocalizableStringExtractor<SyntaxNode>(metadataProvider) | ||
| { | ||
| /// <inheritdoc/> | ||
| public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(node); | ||
|
|
||
| result = null; | ||
|
|
||
| var argumentList = node switch | ||
| { | ||
| ObjectCreationExpressionSyntax creation when IsTypeName(creation.Type, LocalizedStringTypes.TypeNames) => creation.ArgumentList, | ||
| InvocationExpressionSyntax invocation when IsFactoryMethod(invocation.Expression) => invocation.ArgumentList, | ||
| _ => null | ||
| }; | ||
|
|
||
| if (argumentList != null && | ||
| argumentList.Arguments.Count > 0 && | ||
| argumentList.Arguments[0].GetExpression() is LiteralExpressionSyntax literal && | ||
| literal.IsKind(SyntaxKind.StringLiteralExpression)) | ||
| { | ||
| result = CreateLocalizedString(literal.Token.ValueText, null, node); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private static bool IsFactoryMethod(ExpressionSyntax expression) | ||
| => expression is MemberAccessExpressionSyntax memberAccess && | ||
| IsName(memberAccess.Name.Identifier.ValueText, LocalizedStringTypes.FactoryMethodName) && | ||
| IsTypeName(memberAccess.Expression, LocalizedStringTypes.FactoryTypeNames); | ||
|
|
||
| private static bool IsTypeName(ExpressionSyntax expression, string[] typeNames) | ||
| { | ||
| var name = expression switch | ||
| { | ||
| IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, | ||
| QualifiedNameSyntax qualifiedName => qualifiedName.Right.Identifier.ValueText, | ||
| MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText, | ||
| _ => null | ||
| }; | ||
|
|
||
| return name != null && typeNames.Any(typeName => IsName(name, typeName)); | ||
| } | ||
|
|
||
| // Visual Basic identifiers are case-insensitive. | ||
| private static bool IsName(string name, string expected) | ||
| => string.Equals(name, expected, StringComparison.OrdinalIgnoreCase); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
src/OrchardCoreContrib.PoExtractor.DotNet/LocalizedStringTypes.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| namespace OrchardCoreContrib.PoExtractor.DotNet; | ||
|
|
||
| /// <summary> | ||
| /// Represents a class that contains the names of the localized string types and their factory methods. | ||
| /// </summary> | ||
| public static class LocalizedStringTypes | ||
| { | ||
| /// <summary> | ||
| /// Gets the name of the type that represents a localized string. | ||
| /// </summary> | ||
| public static readonly string LocalizedStringTypeName = "LocalizedString"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the name of the type that represents a localized HTML string. | ||
| /// </summary> | ||
| public static readonly string LocalizedHtmlStringTypeName = "LocalizedHtmlString"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the name of the class that contains the LocalizedString factory method. | ||
| /// </summary> | ||
| public static readonly string LocalizedStringExtensionsTypeName = "LocalizedStringExtensions"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the name of the class that contains the LocalizedHtmlString factory method. | ||
| /// </summary> | ||
| public static readonly string LocalizedHtmlStringExtensionsTypeName = "LocalizedHtmlStringExtensions"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the name of the factory method that creates a localized string from its name. | ||
| /// </summary> | ||
| public static readonly string FactoryMethodName = "Create"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the names of the localized string types that can be created with a constructor. | ||
| /// </summary> | ||
| public static readonly string[] TypeNames = | ||
| [ | ||
| LocalizedStringTypeName, | ||
| LocalizedHtmlStringTypeName | ||
| ]; | ||
|
|
||
| /// <summary> | ||
| /// Gets the names of the types that contain the factory method. | ||
| /// </summary> | ||
| public static readonly string[] FactoryTypeNames = | ||
| [ | ||
| LocalizedStringTypeName, | ||
| LocalizedHtmlStringTypeName, | ||
| LocalizedStringExtensionsTypeName, | ||
| LocalizedHtmlStringExtensionsTypeName | ||
| ]; | ||
| } | ||
92 changes: 92 additions & 0 deletions
92
test/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests/LocalizedStringExtractorTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders; | ||
|
|
||
| namespace OrchardCoreContrib.PoExtractor.DotNet.CS.Tests; | ||
|
|
||
| public class LocalizedStringExtractorTests | ||
| { | ||
| [Theory] | ||
| [InlineData("""new LocalizedString("Thing", "Thing");""", "Thing")] | ||
| [InlineData("""new LocalizedString("Thing");""", "Thing")] | ||
| [InlineData("""new LocalizedHtmlString("Thing");""", "Thing")] | ||
| [InlineData("""new LocalizedString("Thing", "Other thing");""", "Thing")] | ||
| [InlineData("""new LocalizedString("Thing", "Thing", true);""", "Thing")] | ||
| [InlineData("""new Microsoft.Extensions.Localization.LocalizedString("Thing", "Thing");""", "Thing")] | ||
| [InlineData("""new global::Microsoft.Extensions.Localization.LocalizedString("Thing", "Thing");""", "Thing")] | ||
| [InlineData("""new LocalizedString("my " + "text", "my text");""", "my text")] | ||
| [InlineData("""new LocalizedHtmlString("Thing", "Thing");""", "Thing")] | ||
| [InlineData("""new LocalizedHtmlString("Thing {0}", "Thing {0}", false, 1);""", "Thing {0}")] | ||
| [InlineData("""new Microsoft.AspNetCore.Mvc.Localization.LocalizedHtmlString("Thing", "Thing");""", "Thing")] | ||
| [InlineData("""LocalizedString.Create("Thing");""", "Thing")] | ||
| [InlineData("""LocalizedHtmlString.Create("Thing");""", "Thing")] | ||
| [InlineData("""LocalizedString.Create("my " + "text");""", "my text")] | ||
| [InlineData("""Microsoft.Extensions.Localization.LocalizedString.Create("Thing");""", "Thing")] | ||
| [InlineData("""global::Microsoft.AspNetCore.Mvc.Localization.LocalizedHtmlString.Create("Thing");""", "Thing")] | ||
| [InlineData("""LocalizedStringExtensions.Create("Thing");""", "Thing")] | ||
| [InlineData("""LocalizedHtmlStringExtensions.Create("Thing");""", "Thing")] | ||
| [InlineData("""LocalizedString.Create("Hello {0}", name);""", "Hello {0}")] | ||
| [InlineData("""LocalizedHtmlString.Create("<b>{0}</b> and {1}", first, second);""", "<b>{0}</b> and {1}")] | ||
| [InlineData("""LocalizedString.Create("Manage things", typeof(Permissions));""", "Manage things")] | ||
| [InlineData( | ||
| """ | ||
| new LocalizedString(@"This is a multi-line | ||
| string.", "value"); | ||
| """, | ||
| """ | ||
| This is a multi-line | ||
| string. | ||
| """)] | ||
| public void ExtractString(string source, string expected) | ||
| { | ||
| // Arrange | ||
| var metadataProvider = new CSharpMetadataProvider("DummyBasePath"); | ||
| var extractor = new LocalizedStringExtractor(metadataProvider); | ||
|
|
||
| var node = GetNode(source); | ||
|
|
||
| // Act | ||
| var extracted = extractor.TryExtract(node, out var result); | ||
|
|
||
| // Assert | ||
| Assert.True(extracted); | ||
| Assert.Equal(expected, result.Text); | ||
| Assert.Null(result.TextPlural); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("""new LocalizedString(nameof(Thing), "Thing");""")] | ||
| [InlineData("""new LocalizedString(name, "Thing");""")] | ||
| [InlineData("""new LocalizedString();""")] | ||
| [InlineData("""new LocalizedHtmlString();""")] | ||
| [InlineData("""new LocalizedHtmlString(nameof(Thing), "Thing");""")] | ||
| [InlineData("""new Thing("Thing", "Thing");""")] | ||
| [InlineData("""new LocalizedStringExtensions("Thing");""")] | ||
| [InlineData("""LocalizedString.Create(nameof(Thing));""")] | ||
| [InlineData("""LocalizedString.Create(name);""")] | ||
| [InlineData("""LocalizedString.Create();""")] | ||
| [InlineData("""LocalizedString.Parse("Thing");""")] | ||
| [InlineData("""Thing.Create("Thing");""")] | ||
| public void ExtractString_NotLocalizedStringWithLiteralName_ReturnsFalse(string source) | ||
| { | ||
| // Arrange | ||
| var metadataProvider = new CSharpMetadataProvider("DummyBasePath"); | ||
| var extractor = new LocalizedStringExtractor(metadataProvider); | ||
|
|
||
| var node = GetNode(source); | ||
|
|
||
| // Act | ||
| var extracted = extractor.TryExtract(node, out var result); | ||
|
|
||
| // Assert | ||
| Assert.False(extracted); | ||
| Assert.Null(result); | ||
| } | ||
|
|
||
| private static ExpressionSyntax GetNode(string source) | ||
| => CSharpSyntaxTree.ParseText(source, path: "DummyPath") | ||
| .GetRoot() | ||
| .DescendantNodes() | ||
| .OfType<ExpressionSyntax>() | ||
| .First(node => node is ObjectCreationExpressionSyntax or InvocationExpressionSyntax); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have these been introduced or is it Claude :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To allow
LocalizedString.Create("...")andLocalizedHtmlString.Create("...")so we are not typing the same string twice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about the
Createmethod, AFAIK,LocalizedStringdoesn't have such a methodUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not. I added it in OC. OrchardCMS/OrchardCore#19933
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We talked about it during triage and that is a sound approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrchardCMS/OrchardCore#19933 (comment)