Implementando localização com geradores de código-fonte

Recentemente, tive o problema de localizar meu aplicativo e pensei em resolvê-lo.





A primeira que vem à mente é a maneira mais óbvia e simples - um dicionário, mas foi imediatamente rejeitado, uma vez que não há como verificar se uma string existe no dicionário no momento da compilação.





Uma solução muito mais elegante é criar uma hierarquia de classes como esta:





public class Locale 
{
	public string Name {get; set;}
  public UI UI {get; set;}
}
public class UI 
{
	public Buttons Buttons {get; set;}
	public Messages Messages {get; set;}
}
public class Buttons 
{
	public string CloseButton {get; set;}
  public string DeleteButton {get; set;}
}
public class Messages 
{
	public string ErrorMessage {get; set;}
}
      
      



Em seguida, você pode simplesmente serializar / desserializar xml'ku.





Existe apenas um "mas". Pode levar muito tempo para criar essa hierarquia de classes, especialmente se o projeto for grande. Então, por que não gerá-lo a partir de um arquivo xml? Isso é o que faremos.





Vamos começar

Primeiro, vamos criar um projeto para nosso gerador e adicionar os pacotes necessários a ele.





dotnet new classlib -o LocalizationSourceGenerator -f netstandard2.0
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.Analyzers
      
      



Importante! A estrutura de destino do projeto deve ser netstandard2.0





A seguir, vamos adicionar a classe do nosso gerador





Deve implementar a interface ISourceGenerator e ser marcado com o atributo Generator





A seguir, vamos adicionar a interface ILocalizationGenerator e a classe XmlLocalizationGenerator que a implementa:





ILocalizationGenerator.cs
public interface ILocalizationGenerator
{
	string GenerateLocalization(string template);
}
      
      



XmlLocalizationGenerator.cs
public class XmlLocalizationGenerator : ILocalizationGenerator
{
	//  
	private List<string> classes = new List<string>();
  
  public string GenerateLocalization(string template)
  {
  	//  xml    
    XmlDocument document = new XmlDocument();
    document.LoadXml(template);
    var root = document.DocumentElement;
    //      
    string namespaceName = root.HasAttribute("namespace") ? 
    											 root.GetAttribute("namespace") : 
                           "Localization";
    GenClass(root); //  
    var sb = new StringBuilder();
   	sb.AppendLine($"namespace {namespaceName}\n{{");
		//     
	  foreach(var item in classes) 
	  {
			sb.AppendLine(item);
		}
    sb.Append('}');
  	return sb.ToString();
  }
  public void GenClass(XmlElement element)
  {
  	var sb = new StringBuilder();
    sb.Append($"public class {element.Name}");
    sb.AppendLine("{");
    //       
    foreach (XmlNode item in element.ChildNodes)
    {
    	//       
      //     -  -
    	if (item.ChildNodes.Count == 0 
      || (item.ChildNodes.Count == 1 
      && item.FirstChild.NodeType==XmlNodeType.Text))
      {
      	sb.AppendLine($"public string {item.Name} {{get; set;}}");
      }
      else
      {
      	//    
        //   
      	sb.AppendLine($"public {item.Name} {item.Name} {{get; set;}}");
        GenClass(item); 
      }
    }
    sb.AppendLine("}");
    classes.Add(sb.ToString());
  }
}
      
      



Pouco resta a fazer. É necessário implementar a classe do próprio gerador





LocalizationSourceGenerator.cs
[Generator]
public class LocalizationSourceGenerator : ISourceGenerator
{
	public void Execute(GeneratorExecutionContext context)
 	{
  	//      
  	var templateFile = context
    									 .AdditionalFiles
                       .FirstOrDefault(
                       		x => Path.GetExtension(x.Path) == ".xml")
                          ?.Path;
    if (!string.IsNullOrWhiteSpace(templateFile))
    {
    	ILocalizationGenerator generator = new XmlLocalizationGenerator();
      var s = generator.GenerateLocalization(File.ReadAllText(templateFile));
      //    ""
      //      
      context.AddSource("Localization",s );
    }
  }

  public void Initialize(GeneratorInitializationContext context)
  {
  	//       ,
    //   
  }
}
      
      



Isso é tudo! Agora você só precisa verificar nosso gerador. Para fazer isso, crie um projeto de aplicativo de console





dotnet new console -o Test
      
      



Adicione o modelo e o arquivo de localização





template.xml
<Locale namespace="Program.Localization">
	<UI>
		<Buttons>
			<SendButton/> 
		</Buttons> 
	</UI> 
	<Name/>
</Locale>
      
      



ru.xml
<Locale>
	<UI>
		<Buttons>
			<SendButton></SendButton>
		</Buttons>
	</UI>
	<Name></Name>
</Locale>
      
      



Vamos editar o arquivo do projeto





Test.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>  
  </PropertyGroup>  
  <ItemGroup>
    <ProjectReference 
					ReferenceOutputAssembly="false"
					OutputItemType="Analyzer" 
					Include="----" />
		<!--      -->
    <AdditionalFiles Include="template.xml"/>  
  </ItemGroup>
</Project>
      
      



E o código do programa





Program.cs
using System;
using System.IO; 
using System.Xml.Serialization;   
using Program.Localization; //  
namespace Program
{ 
	public class Program
	{
		public static void Main()
		{ 
      // Locale    
			var xs = new XmlSerializer(typeof(Locale));
			var locale = xs.Deserialize(File.OpenRead("ru.xml")) as Locale;
			Console.WriteLine(locale.Name);
			Console.WriteLine(locale.UI.Buttons.SendButton);
			
		}
		
	}
}
      
      



Dotnet-And-Happiness / LocalizationSourceGenerator (github.com) - repositório gerador








All Articles