Skip to content

Commit 7543c68

Browse files
authored
Merge branch 'main' into dependabot/nuget/Microsoft.Build.Locator-1.9.1
2 parents f8a3e79 + 7ce812e commit 7543c68

File tree

29 files changed

+387
-110
lines changed

29 files changed

+387
-110
lines changed

.config/dotnet-tools.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"isRoot": true,
44
"tools": {
55
"dotnet-sonarscanner": {
6-
"version": "10.1.1",
6+
"version": "10.1.2",
77
"commands": [
88
"dotnet-sonarscanner"
99
]
1010
},
1111
"dotnet-coverage": {
12-
"version": "17.9.3",
12+
"version": "17.14.2",
1313
"commands": [
1414
"dotnet-coverage"
1515
]

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,5 @@ MigrationBackup/
353353
**/coverage.opencover.xml
354354
**/.snapshots/*.received.*
355355

356+
#ignore stupid macos-files
357+
.DS_Store

docs/code-first.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,44 @@ The consuming target project should at least reference a suitable version of [Mi
114114
</data>
115115
```
116116
> executing this will OVERWRITE any existing file at the moment. In the future, TypealizR will be aware of existing files and provide a way to synchronize code with those files, in order to not loose any customizations done within them. Follow [the discussion](https://github.com/earloc/TypealizR/discussions/78) and let´s define together, what workflows would be needed to make code-first-i18n a real game-changer in the future!
117+
118+
### comments
119+
In order to also generate comments within `resx`-files (which may give potential translators some context), leverage the [remarks](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags#remarks)-element within a structured XML-comment:
120+
121+
```csharp
122+
[CodeFirstTypealized]
123+
public interface ILocalizables
124+
{
125+
/// <summary>
126+
/// Hello, fellow developer!
127+
/// </summary>
128+
/// <remarks>
129+
/// a simple greeting at application startup
130+
/// </remarks>
131+
public LocalizedString Hello { get; }
132+
133+
/// <summary>
134+
/// Hey <paramref name="userName"/>, welcome to <paramref name="planetName"/> 👍!
135+
/// </summary>
136+
/// <remarks>
137+
/// a demo greeting conversation, shown at demo-time ;)
138+
/// </remarks>
139+
public LocalizedString Greet(string userName, string planetName);
140+
}
141+
142+
```
143+
144+
Exporting this, will result in the following resx:
145+
146+
```xml
147+
```xml
148+
<data name="Hello">
149+
<value>Hello, fellow developer!</value>
150+
<comment>a simple greeting at application startup</comment>
151+
</data>
152+
<data name="Greet">
153+
<value>Hey {0}, welcome to {1} 👍!</value>
154+
<comment>a demo greeting conversation, shown at demo-time ;)</comment>
155+
</data>
156+
```
157+
```
Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
using Microsoft.Extensions.Localization;
2-
using TypealizR.CodeFirst.Abstractions;
3-
4-
namespace CLI.CodeFirst;
5-
6-
[CodeFirstTypealized]
7-
public interface ILocalizables
8-
{
9-
/// <summary>
10-
/// Hello, fellow developer!
11-
/// </summary>
12-
public LocalizedString Hello { get; }
13-
14-
/// <summary>
15-
/// Hey <paramref name="userName"/>, welcome to <paramref name="planetName"/> 👍!
16-
/// </summary>
17-
/// <param name="userName"></param>
18-
/// <param name="planetName"></param>
19-
/// <returns></returns>
20-
public LocalizedString Greet(string userName, string planetName);
21-
}
1+
using Microsoft.Extensions.Localization;
2+
using TypealizR.CodeFirst.Abstractions;
3+
4+
namespace CLI.CodeFirst;
5+
6+
[CodeFirstTypealized]
7+
public interface ILocalizables
8+
{
9+
/// <summary>
10+
/// Hello, fellow developer!
11+
/// </summary>
12+
public LocalizedString Hello { get; }
13+
14+
/// <summary>
15+
/// Hey <paramref name="userName"/>, welcome to <paramref name="planetName"/> 👍!
16+
/// </summary>
17+
/// <param name="userName"></param>
18+
/// <param name="planetName"></param>
19+
/// <returns></returns>
20+
public LocalizedString Greet(string userName, string planetName);
21+
}

src/Playground.CodeFirst.Console/ILocalizablesWithDefaults.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,33 @@ internal interface ILocalizablesWithDefaults
99
///<summary>
1010
/// 42
1111
///</summary>
12+
///<remarks>
13+
/// The famous answer to the lesser known question...
14+
/// </remarks>
1215
LocalizedString WhatIsTheMeaningOfLifeTheUniverseAndEverything { get; }
1316

1417
/// <summary>
1518
/// Greetings to you, {0}
1619
/// </summary>
20+
///<remarks>
21+
/// What someone says to someone else when they meet
22+
/// </remarks>
1723
LocalizedString Hello(string world);
1824

1925
///<summary>
2026
/// Goodbye, <paramref name="user"/>
2127
///</summary>
28+
///<remarks>
29+
/// What someone says to someone else when they depart
30+
/// </remarks>
2231
LocalizedString Farewell(string user);
2332

2433
/// <summary>
2534
/// <paramref name="right"/> greets <paramref name="left"/>, and <paramref name="left"/> answers: "Hi!".
2635
/// </summary>
36+
/// <remarks>
37+
/// A sample conversation
38+
/// </remarks>
2739
LocalizedString Greet(string left, string right);
2840

2941

src/TypealizR.CLI/Commands/CodeFirst/ExportCommand.cs

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.CommandLine.Invocation;
33
using Microsoft.Build.Locator;
44
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
56
using Microsoft.CodeAnalysis.CSharp.Syntax;
67
using Microsoft.CodeAnalysis.MSBuild;
78
using TypealizR.CLI.Abstractions;
@@ -144,22 +145,29 @@ private static async Task ExportAsync(IndentableWriter writer, string baseDirect
144145
}
145146
}
146147

147-
private static IEnumerable<BaseNamespaceDeclarationSyntax> FindNamespaces(Compilation compilation, CancellationToken cancellationToken) => compilation.SyntaxTrees
148-
.Where(x => x.GetRoot() is CompilationUnitSyntax)
149-
.Select(x => (CompilationUnitSyntax)x.GetRoot(cancellationToken))
150-
.SelectMany(x => x.Members.OfType<BaseNamespaceDeclarationSyntax>())
148+
private static IEnumerable<BaseNamespaceDeclarationSyntax> FindNamespaces(Compilation compilation, CancellationToken cancellationToken)
149+
=> compilation.SyntaxTrees
150+
.Where(x => x.GetRoot() is CompilationUnitSyntax)
151+
.Select(x => (CompilationUnitSyntax)x.GetRoot(cancellationToken))
152+
.SelectMany(x => x.Members.OfType<BaseNamespaceDeclarationSyntax>())
151153
;
152154

153-
private static IEnumerable<InterfaceInfo> FindInterfaces(Compilation compilation, IEnumerable<BaseNamespaceDeclarationSyntax> allNamespaces, CancellationToken cancellationToken) => allNamespaces
154-
.SelectMany(x => GetAllInterfaceDeclarations(x.Members))
155-
.Select(x => new { Declaration = x, Model = compilation.GetSemanticModel(x.SyntaxTree) })
156-
.Select(x => new { x.Declaration, Symbol = x.Model.GetDeclaredSymbol(x.Declaration, cancellationToken) })
157-
.Where(x => x.Symbol is not null)
158-
.Select(x => new InterfaceInfo(x.Declaration, x.Symbol!))
159-
.Where(x => x.Symbol
160-
.GetAttributes()
161-
.Any(x => x.AttributeClass is not null && x.AttributeClass!.Name.StartsWith(MarkerAttributeName, StringComparison.Ordinal))
162-
)
155+
private static IEnumerable<InterfaceInfo> FindInterfaces
156+
(
157+
Compilation compilation,
158+
IEnumerable<BaseNamespaceDeclarationSyntax> allNamespaces,
159+
CancellationToken cancellationToken
160+
)
161+
=> allNamespaces
162+
.SelectMany(x => GetAllInterfaceDeclarations(x.Members))
163+
.Select(x => new { Declaration = x, Model = compilation.GetSemanticModel(x.SyntaxTree) })
164+
.Select(x => new { x.Declaration, Symbol = x.Model.GetDeclaredSymbol(x.Declaration, cancellationToken) })
165+
.Where(x => x.Symbol is not null)
166+
.Select(x => new InterfaceInfo(x.Declaration, x.Symbol!))
167+
.Where(x => x.Symbol
168+
.GetAttributes()
169+
.Any(x => x.AttributeClass is not null && x.AttributeClass!.Name.StartsWith(MarkerAttributeName, StringComparison.Ordinal))
170+
)
163171
;
164172

165173
private static IEnumerable<InterfaceDeclarationSyntax> GetAllInterfaceDeclarations(SyntaxList<MemberDeclarationSyntax> members)
@@ -180,7 +188,12 @@ private static IEnumerable<InterfaceDeclarationSyntax> GetAllInterfaceDeclaratio
180188
}
181189
}
182190

183-
private static IEnumerable<TypeInfo> FindClasses(Compilation compilation, IEnumerable<BaseNamespaceDeclarationSyntax> allNamespaces, IEnumerable<InterfaceInfo> markedInterfacesIdentifier, CancellationToken cancellationToken)
191+
private static IEnumerable<TypeInfo> FindClasses(
192+
Compilation compilation,
193+
IEnumerable<BaseNamespaceDeclarationSyntax> allNamespaces,
194+
IEnumerable<InterfaceInfo> markedInterfacesIdentifier,
195+
CancellationToken cancellationToken
196+
)
184197
{
185198
var interfaces = markedInterfacesIdentifier.ToDictionary(x => x.Symbol, SymbolEqualityComparer.Default);
186199

@@ -219,30 +232,35 @@ private static IEnumerable<ClassDeclarationSyntax> GetAllClassDeclarations(Synta
219232
}
220233
}
221234

222-
private static VariableDeclaratorSyntax? FindKeyOf(TypeInfo type, PropertyDeclarationSyntax propertySyntax) => type.Declaration.Members
223-
.OfType<FieldDeclarationSyntax>()
224-
.Where(x => x.Modifiers.Any(y => y.Text == "const"))
225-
.Select(x => x.Declaration.Variables.SingleOrDefault())
226-
.Where(x => x is not null).Select(x => x!)
227-
.FirstOrDefault(x => x.Identifier.Text == $"{propertySyntax.Identifier.Text}{TypealizR._.FallBackKeySuffix}")
228-
;
229-
230-
private static VariableDeclaratorSyntax? FindKeyOf(TypeInfo type, MethodDeclarationSyntax methodSyntax) => type.Declaration.Members
231-
.OfType<FieldDeclarationSyntax>()
232-
.Where(x => x.Modifiers.Any(y => y.Text == "const"))
233-
.Select(x => x.Declaration.Variables.SingleOrDefault())
234-
.Where(x => x is not null).Select(x => x!)
235-
.FirstOrDefault(x => x.Identifier.Text == $"{methodSyntax.Identifier.Text}{TypealizR._.FallBackKeySuffix}")
236-
;
235+
private static VariableDeclaratorSyntax? FindKeyOf(TypeInfo type, PropertyDeclarationSyntax propertySyntax)
236+
=> type.Declaration.Members
237+
.OfType<FieldDeclarationSyntax>()
238+
.Where(x => x.Modifiers.Any(y => y.Text == "const"))
239+
.Select(x => x.Declaration.Variables.SingleOrDefault())
240+
.Where(x => x is not null).Select(x => x!)
241+
.FirstOrDefault(x => x.Identifier.Text == $"{propertySyntax.Identifier.Text}{TypealizR._.FallBackKeySuffix}")
242+
;
243+
244+
private static VariableDeclaratorSyntax? FindKeyOf(TypeInfo type, MethodDeclarationSyntax methodSyntax)
245+
=> type.Declaration.Members
246+
.OfType<FieldDeclarationSyntax>()
247+
.Where(x => x.Modifiers.Any(y => y.Text == "const"))
248+
.Select(x => x.Declaration.Variables.SingleOrDefault())
249+
.Where(x => x is not null).Select(x => x!)
250+
.FirstOrDefault(x => x.Identifier.Text == $"{methodSyntax.Identifier.Text}{TypealizR._.FallBackKeySuffix}")
251+
;
237252

238253
private static void AddProperty(IndentableWriter writer, TypeInfo type, ResxBuilder builder, PropertyDeclarationSyntax property)
239254
{
240255
var key = FindKeyOf(type, property);
241256
var sanitizedValue = key?.Initializer?.Value?.ToResourceKey() ?? "";
242-
257+
243258
if (key is not null && !string.IsNullOrEmpty(sanitizedValue))
244259
{
245-
builder.Add(property.Identifier.Text, sanitizedValue);
260+
var commentTrivia = property.Identifier.GetAllTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia));
261+
var commentText = commentTrivia.ToFullString()?.Replace("//", "", StringComparison.InvariantCulture);
262+
263+
builder.Add(property.Identifier.Text, sanitizedValue, commentText);
246264
writer.WriteLine($"✔️ {property.Identifier.Text}");
247265
}
248266
else
@@ -255,9 +273,13 @@ private static void AddMethod(IndentableWriter writer, TypeInfo type, ResxBuilde
255273
{
256274
var key = FindKeyOf(type, method);
257275
var sanitizedValue = key?.Initializer?.Value?.ToResourceKey() ?? "";
276+
258277
if (key is not null && !string.IsNullOrEmpty(sanitizedValue))
259278
{
260-
builder.Add(method.Identifier.Text, sanitizedValue);
279+
var commentTrivia = method.DescendantTrivia(null, true).FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia));
280+
var commentText = commentTrivia.ToFullString()?.Replace("//", "", StringComparison.InvariantCulture);
281+
282+
builder.Add(method.Identifier.Text, sanitizedValue, commentText);
261283
writer.WriteLine($"✔️ {method.Identifier.Text}()");
262284
}
263285
else

src/TypealizR.CLI/Resources/ResxBuilder.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ internal class ResxBuilder
55
{
66
private const string resHeader = "resheader";
77
private const string value = "value";
8-
private readonly Dictionary<string, string> entries = [];
9-
public ResxBuilder Add(string key, string value)
8+
private const string comment = "comment";
9+
10+
private readonly Dictionary<string, (string Value, string? Comment)> entries = [];
11+
public ResxBuilder Add(string key, string value, string? comment)
1012
{
11-
entries.Add(key, value);
13+
entries.Add(key, (value, comment?.Trim()));
1214
return this;
1315
}
1416

@@ -31,7 +33,8 @@ public string Build()
3133
entries.Select(x =>
3234
new XElement("data",
3335
new XAttribute("name", x.Key),
34-
new XElement(value, x.Value)
36+
new XElement(value, x.Value.Value),
37+
!string.IsNullOrEmpty(x.Value.Comment) ? new XElement(comment, x.Value.Comment) : null
3538
)
3639
).ToArray()
3740
)

src/TypealizR.Tests/CLI.Tests/ExportCommand.Tests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ public class ExportCommand_Tests
77
{
88
private static string ProjectFile(string x) => $"../../../../{x}/{x}.csproj";
99

10-
[Fact]
11-
public async Task Export_Generates_ResxFiles()
10+
[Theory]
11+
[InlineData("ILocalizables.resx")]
12+
[InlineData("ILocalizablesWithDefaults.resx")]
13+
[InlineData("Some+Inner+ISampleInnerface.resx")]
14+
public async Task Export_Generates_ResxFiles(string fileName)
1215
{
1316
var storage = new InMemoryStorage();
1417
var sut = new App(
@@ -19,8 +22,7 @@ public async Task Export_Generates_ResxFiles()
1922
.RunAsync();
2023
result.ShouldBe(0);
2124

22-
storage.Files.Keys.ShouldContain(x => x.EndsWith("ILocalizables.resx"));
23-
storage.Files.Keys.ShouldContain(x => x.EndsWith("ILocalizablesWithDefaults.resx"));
24-
storage.Files.Keys.ShouldContain(x => x.EndsWith("Some+Inner+ISampleInnerface.resx"));
25+
var file = storage.Files.First(x => x.Key.EndsWith(fileName, StringComparison.InvariantCulture));
26+
await Verify(file.Value).UseParameters(fileName);
2527
}
2628
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<root>
2+
<resheader name="resmimetype">
3+
<value>text/microsoft-resx</value>
4+
</resheader>
5+
<resheader name="version">
6+
<value>2.0</value>
7+
</resheader>
8+
<resheader name="reader">
9+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
10+
</resheader>
11+
<resheader name="writer">
12+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
13+
</resheader>
14+
<data name="WhatIsTheMeaningOfLifeTheUniverseAndEverything">
15+
<value>42</value>
16+
</data>
17+
<data name="Hello">
18+
<value>Hello {0}</value>
19+
</data>
20+
<data name="Farewell">
21+
<value>Goodbye, {0}</value>
22+
</data>
23+
<data name="Greet">
24+
<value>{1} greets {0}, and {0} answers: ""Hi!"".</value>
25+
</data>
26+
<data name="Goodbye">
27+
<value>Goodbye, {0} and thx for all the fish!</value>
28+
</data>
29+
</root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<root>
2+
<resheader name="resmimetype">
3+
<value>text/microsoft-resx</value>
4+
</resheader>
5+
<resheader name="version">
6+
<value>2.0</value>
7+
</resheader>
8+
<resheader name="reader">
9+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
10+
</resheader>
11+
<resheader name="writer">
12+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
13+
</resheader>
14+
<data name="WhatIsTheMeaningOfLifeTheUniverseAndEverything">
15+
<value>42</value>
16+
<comment>The famous answer to the lesser known question...</comment>
17+
</data>
18+
<data name="Hello">
19+
<value>Greetings to you, {0}</value>
20+
<comment>What someone says to someone else when they meet</comment>
21+
</data>
22+
<data name="Farewell">
23+
<value>Goodbye, {0}</value>
24+
<comment>What someone says to someone else when they depart</comment>
25+
</data>
26+
<data name="Greet">
27+
<value>{1} greets {0}, and {0} answers: ""Hi!"".</value>
28+
<comment>A sample conversation</comment>
29+
</data>
30+
</root>

0 commit comments

Comments
 (0)