[.NET, C#] Реализация локализации при помощи Source code generators

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
09-Мар-2021 19:31

Недавно я столкнулся с проблемой локализации своего приложения и задумался над её решением.Первым на ум приходить самый очевидный и простой способ - словарь, но он был тут же отвергнут, так как никак не нельзя проверить существует ли строка в словаре на момент компиляции.Куда более изящное решение - создать иерархию классов типа этой:
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) - репозиторий генератора
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_.net, #_c#, #_sourcegenerator, #_source, #_csharp, #_.net, #_c#
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 19:21
Часовой пояс: UTC + 5