Реализация локализации при помощи Source code generators

от автора

Недавно я столкнулся с проблемой локализации своего приложения и задумался над её решением.

Первым на ум приходить самый очевидный и простой способ — словарь, но он был тут же отвергнут, так как никак не нельзя проверить существует ли строка в словаре на момент компиляции.

Куда более изящное решение — создать иерархию классов типа этой:

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;} }

Дальше можно просто сериализировать/десериализировать xml’ку.

Только есть одно «но». На создание такой иерархии классов может уйти достаточно времени, особенно если проект большой. Так почему бы не генерировать ее из xml файла? Этим мы и займемся.

Приступим

Для начала создадим проект нашего генератора и добавим в него необходимые пакеты

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

Важно! Target framework проекта обязательно должен быть netstandard2.0

Далее добавим класс нашего генератора

Он должен реализовать интерфейс ISourceGenerator и быть помеченным атрибутом Generator

Далее добавим интерфейс ILocalizationGenerator и класс XmlLocalizationGenerator, который реализует его:

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());   } }

Осталось дело за малым. Необходимо реализовать класс самого генератора

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)   {   	//В данном случае нам не нужно ничего инициализировать,     //поэтому оставим реализацию пустой   } }

Вот и все! Теперь нужно лишь проверить наш генератор. Для этого создадим проект консольного приложения

dotnet new console -o Test

Добавим файл шаблона и локализации

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>

Отредактируем файл проекта

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>

И код программы

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) — репозиторий генератора

ссылка на оригинал статьи https://habr.com/ru/post/546192/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *