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