Skip to content

Commit 67000de

Browse files
authored
Add localized strings generator (#13)
1 parent 1bcc976 commit 67000de

File tree

7 files changed

+677
-0
lines changed

7 files changed

+677
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
6+
<LangVersion>13.0</LangVersion>
7+
<Nullable>enable</Nullable>
8+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
9+
</PropertyGroup>
10+
11+
<PropertyGroup>
12+
<!-- Ensure the source generator can be loaded on any architecture -->
13+
<PlatformTarget>AnyCPU</PlatformTarget>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
18+
<PrivateAssets>all</PrivateAssets>
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
</PackageReference>
21+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Resources;
8+
using System.Text;
9+
using System.Xml.Linq;
10+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Text;
14+
15+
namespace FluentLauncher.Infra.Settings.SourceGenerators;
16+
17+
internal record struct ClassInfo(string Namespace, string ClassName);
18+
19+
internal record struct ReswFileInfo(string FilePath)
20+
{
21+
public string Filename => Path.GetFileNameWithoutExtension(FilePath);
22+
public string ResourceMapName => Filename.Split('.')[0];
23+
public string Qualifier => Filename.Substring(ResourceMapName.Length, Filename.Length - ResourceMapName.Length - ".resw".Length);
24+
}
25+
26+
[Generator(LanguageNames.CSharp)]
27+
public class LocalizedStringsGenerator : IIncrementalGenerator
28+
{
29+
public LocalizedStringsGenerator()
30+
{
31+
#if DEBUG
32+
if (!Debugger.IsAttached)
33+
{
34+
//Debugger.Launch();
35+
}
36+
#endif
37+
}
38+
39+
public void Initialize(IncrementalGeneratorInitializationContext context)
40+
{
41+
// Find all classes with the [GeneratedLocalizedStrings] attribute
42+
var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName(
43+
"FluentLauncher.Infra.LocalizedStrings.GeneratedLocalizedStringsAttribute",
44+
static (node, _) => node is ClassDeclarationSyntax,
45+
static (ctx, token) =>
46+
{
47+
// Extract class info
48+
ITypeSymbol localizedStringClassSymbol = (ITypeSymbol)ctx.TargetSymbol;
49+
string containingNamespace = localizedStringClassSymbol.ContainingNamespace.ToDisplayString();
50+
string className = localizedStringClassSymbol.Name;
51+
return new ClassInfo(containingNamespace, className);
52+
})
53+
.Collect();
54+
55+
// Find all .resw files, group by resource map name and keep the neutral one (or the first in alphabetical order)
56+
var reswFilesProvider = context.AdditionalTextsProvider
57+
// Find all .resw files
58+
.Where(file => file.Path.EndsWith(".resw", StringComparison.OrdinalIgnoreCase))
59+
.Select((file, token) =>
60+
{
61+
var reswFile = new ReswFileInfo(file.Path);
62+
return (reswFile.ResourceMapName, reswFile);
63+
})
64+
.GroupBy(
65+
static item => item.Left,
66+
static item => item.Right
67+
)
68+
.Select((group, token) => group.Right
69+
.ToList()
70+
.OrderBy(file => file.Filename)
71+
.First()
72+
)
73+
.Collect();
74+
75+
context.RegisterSourceOutput(classDeclarations.Combine(reswFilesProvider), Execute);
76+
}
77+
78+
private static void Execute(SourceProductionContext context, (ImmutableArray<ClassInfo> classInfos, ImmutableArray<ReswFileInfo> reswFiles) input)
79+
{
80+
var (classes, reswFiles) = input;
81+
if (classes.IsDefaultOrEmpty || reswFiles.IsDefaultOrEmpty)
82+
return;
83+
84+
foreach (var classInfo in classes)
85+
{
86+
var namespaceName = classInfo.Namespace;
87+
var className = classInfo.ClassName;
88+
89+
// Parse and generate properties for each .resw file
90+
IEnumerable<string> defaultStringIds = []; // Strings in Resources.resw
91+
var otherStringIds = new Dictionary<string, IEnumerable<string>>();
92+
93+
foreach (var reswFile in reswFiles)
94+
{
95+
string resourceMapName = reswFile.ResourceMapName;
96+
if (reswFile.Filename.Equals("Resources", StringComparison.OrdinalIgnoreCase))
97+
defaultStringIds = ParseReswFile(reswFile);
98+
else
99+
otherStringIds[resourceMapName] = ParseReswFile(reswFile);
100+
}
101+
102+
// Generate the class in the detected namespace
103+
string source = GenerateClass(namespaceName, className, defaultStringIds, otherStringIds);
104+
105+
// Add the generated source to the compilation
106+
context.AddSource($"{namespaceName}.{className}.g.cs", SourceText.From(source, Encoding.UTF8));
107+
}
108+
}
109+
110+
private static IEnumerable<string> ParseReswFile(ReswFileInfo reswFile)
111+
{
112+
using var reader = new StreamReader(reswFile.FilePath);
113+
114+
IEnumerable<string>? stringIds = System.Xml.Linq.XDocument.Load(reader).Root?
115+
.Elements("data")
116+
.Select(node => node.Attribute("name")?.Value.Replace(".", "/"))
117+
.Where(name => !string.IsNullOrWhiteSpace(name))!;
118+
119+
return stringIds ?? [];
120+
//properties.Add($"public static string {propertyName} => s_resourceMap.GetValue(\"{namespaceName}/{name}\").ValueAsString;");
121+
}
122+
123+
private static string GenerateClass(
124+
string namespaceName,
125+
string className,
126+
IEnumerable<string> defaultStringIds,
127+
Dictionary<string, IEnumerable<string>> otherStringIds)
128+
{
129+
var propertyBuilder = new StringBuilder();
130+
131+
propertyBuilder.AppendLine("// Default resource map (Resources.resw)");
132+
foreach (var id in defaultStringIds)
133+
{
134+
string propertyName = id.Replace('/', '_').Replace(' ', '_');
135+
propertyBuilder.AppendLine($" public static string {propertyName} => s_resourceMap.GetValue(\"/Resources/{id}\").ValueAsString;");
136+
}
137+
138+
propertyBuilder.AppendLine("\n // Other resource maps");
139+
foreach (var item in otherStringIds)
140+
{
141+
string resourceMapName = item.Key;
142+
IEnumerable<string> stringIds = item.Value;
143+
propertyBuilder.AppendLine($" public static class {resourceMapName}")
144+
.AppendLine(" {");
145+
146+
foreach (string id in stringIds)
147+
{
148+
string propertyName = id.Replace('/', '_').Replace(' ', '_');
149+
propertyBuilder.AppendLine($" public static string {propertyName} => s_resourceMap.GetValue(\"/{resourceMapName}/{id}\").ValueAsString;");
150+
}
151+
152+
propertyBuilder.AppendLine(" }");
153+
}
154+
155+
return $$"""
156+
using global::Microsoft.Windows.ApplicationModel.Resources;
157+
158+
namespace {{namespaceName}}
159+
{
160+
static partial class {{className}}
161+
{
162+
private static ResourceManager s_resourceManager;
163+
private static ResourceMap s_resourceMap;
164+
165+
static {{className}}()
166+
{
167+
s_resourceManager = new ResourceManager();
168+
s_resourceMap = s_resourceManager.MainResourceMap;
169+
}
170+
171+
{{propertyBuilder}}
172+
}
173+
}
174+
""";
175+
}
176+
}

0 commit comments

Comments
 (0)