Implementar la localización con generadores de código fuente

Recientemente me encontré con el problema de localizar mi aplicación y pensé en solucionarlo.





Lo primero que me viene a la mente es la forma más obvia y simple: un diccionario, pero fue rechazado de inmediato, ya que no hay forma de verificar si existe una cadena en el diccionario en el momento de la compilación.





Una solución mucho más elegante es crear una jerarquía de clases 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;}
}
      
      



Entonces puede simplemente serializar / deserializar xml'ku.





Solo hay un "pero". Puede llevar mucho tiempo crear una jerarquía de clases de este tipo, especialmente si el proyecto es grande. Entonces, ¿por qué no generarlo a partir de un archivo xml? Eso es lo que haremos.





Empecemos

Primero, creemos un proyecto para nuestro generador y agreguemos los paquetes necesarios.





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



¡Importante! El marco objetivo del proyecto debe ser netstandard2.0





A continuación, agreguemos la clase de nuestro generador.





Debe implementar la interfaz ISourceGenerator y estar marcado con el atributo Generator





A continuación, agreguemos la interfaz ILocalizationGenerator y la clase XmlLocalizationGenerator que la 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());
  }
}
      
      



Queda poco por hacer. Es necesario implementar la clase del generador en sí.





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)
  {
  	//       ,
    //   
  }
}
      
      



¡Eso es todo! Ahora solo tienes que comprobar nuestro generador. Para hacer esto, cree un proyecto de aplicación de consola





dotnet new console -o Test
      
      



Agregue la plantilla y el archivo de localización





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>
      
      



Editemos el archivo del proyecto





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>
      
      



Y el código del 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) - repositorio del generador








All Articles