Implemented the .NET generator

- Added .NET generator setting for the namespace to use
- Added .NET generator setting to choose the default culture
- Added get child section method to section processor
- Added util to convert any string to a verbatim string literal
- Removed redundant variables on exceptions
This commit is contained in:
Thorsten Sommer 2022-10-30 15:49:22 +01:00
parent 547a22bfd2
commit 630f014c1a
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
13 changed files with 454 additions and 5 deletions

View File

@ -10,6 +10,8 @@ public static class SettingNames
public static readonly string GENERATOR_MODE = "Generator Mode"; public static readonly string GENERATOR_MODE = "Generator Mode";
public static readonly string GENERATOR_DOTNET_ENABLED = "Generator .NET Enabled"; public static readonly string GENERATOR_DOTNET_ENABLED = "Generator .NET Enabled";
public static readonly string GENERATOR_DOTNET_DESTINATION_PATH = "Generator .NET Destination Path"; public static readonly string GENERATOR_DOTNET_DESTINATION_PATH = "Generator .NET Destination Path";
public static readonly string GENERATOR_DOTNET_NAMESPACE = "Generator .NET Namespace";
public static readonly string GENERATOR_DOTNET_DEFAULT_CULTURE = "Generator .NET Default Culture";
public static readonly string GENERATOR_GODOT_ENABLED = "Generator Godot Enabled"; public static readonly string GENERATOR_GODOT_ENABLED = "Generator Godot Enabled";
public static readonly string GENERATOR_GODOT_DESTINATION_PATH = "Generator Godot Destination Path"; public static readonly string GENERATOR_GODOT_DESTINATION_PATH = "Generator Godot Destination Path";
} }

View File

@ -685,6 +685,154 @@ public static class AppSettings
#endregion #endregion
#region .NET Generator Namespace
private static string CACHE_GENERATOR_DOTNET_NAMESPACE = string.Empty;
private static bool CACHE_GENERATOR_DOTNET_NAMESPACE_IS_LOADED = false;
public static async Task<string> GetGeneratorDotnetNamespace()
{
// When possible, use the cache:
if (CACHE_GENERATOR_DOTNET_NAMESPACE_IS_LOADED)
return CACHE_GENERATOR_DOTNET_NAMESPACE;
var generatorDotnetNamespace = "I18N";
try
{
// Get the database:
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
// Check, if the setting is already set:
if (await db.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.GENERATOR_DOTNET_NAMESPACE) is { } existingSetting)
{
generatorDotnetNamespace = existingSetting.TextValue;
return generatorDotnetNamespace;
}
// Does not exist, so create it:
var setting = new Setting
{
Code = SettingNames.GENERATOR_DOTNET_NAMESPACE,
TextValue = generatorDotnetNamespace,
};
await db.Settings.AddAsync(setting);
await db.SaveChangesAsync();
return generatorDotnetNamespace;
}
finally
{
CACHE_GENERATOR_DOTNET_NAMESPACE_IS_LOADED = true;
CACHE_GENERATOR_DOTNET_NAMESPACE = generatorDotnetNamespace;
}
}
public static async Task SetGeneratorDotnetNamespace(string updatedNamespace)
{
// Update the cache:
CACHE_GENERATOR_DOTNET_NAMESPACE = updatedNamespace;
CACHE_GENERATOR_DOTNET_NAMESPACE_IS_LOADED = true;
// Get the database:
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
// Check, if the setting is already set:
if (await db.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.GENERATOR_DOTNET_NAMESPACE) is { } existingSetting)
{
existingSetting.TextValue = updatedNamespace;
await db.SaveChangesAsync();
}
// Does not exist, so create it:
else
{
var setting = new Setting
{
Code = SettingNames.GENERATOR_DOTNET_NAMESPACE,
TextValue = updatedNamespace,
};
await db.Settings.AddAsync(setting);
await db.SaveChangesAsync();
}
}
#endregion
#region .NET Generator Default Culture
private static int CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE = -1;
private static bool CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE_IS_LOADED = false;
public static async Task<int> GetGeneratorDotnetDefaultCultureIndex()
{
// When possible, use the cache:
if (CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE_IS_LOADED)
return CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE;
var generatorDotnetDefaultCulture = 0;
try
{
// Get the database:
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
// Check, if the setting is already set:
if (await db.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.GENERATOR_DOTNET_DEFAULT_CULTURE) is { } existingSetting)
{
generatorDotnetDefaultCulture = existingSetting.IntegerValue;
return generatorDotnetDefaultCulture;
}
// Does not exist, so create it:
var setting = new Setting
{
Code = SettingNames.GENERATOR_DOTNET_DEFAULT_CULTURE,
IntegerValue = generatorDotnetDefaultCulture,
};
await db.Settings.AddAsync(setting);
await db.SaveChangesAsync();
return generatorDotnetDefaultCulture;
}
finally
{
CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE_IS_LOADED = true;
CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE = generatorDotnetDefaultCulture;
}
}
public static async Task SetGeneratorDotnetDefaultCultureIndex(int updatedCulture)
{
// Update the cache:
CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE = updatedCulture;
CACHE_GENERATOR_DOTNET_DEFAULT_CULTURE_IS_LOADED = true;
// Get the database:
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
// Check, if the setting is already set:
if (await db.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.GENERATOR_DOTNET_DEFAULT_CULTURE) is { } existingSetting)
{
existingSetting.IntegerValue = updatedCulture;
await db.SaveChangesAsync();
}
// Does not exist, so create it:
else
{
var setting = new Setting
{
Code = SettingNames.GENERATOR_DOTNET_DEFAULT_CULTURE,
IntegerValue = updatedCulture,
};
await db.Settings.AddAsync(setting);
await db.SaveChangesAsync();
}
}
#endregion
#region Godot Generator Enabled/Disabled #region Godot Generator Enabled/Disabled
private static bool CACHE_GENERATOR_GODOT_ENABLED = true; private static bool CACHE_GENERATOR_GODOT_ENABLED = true;

View File

@ -28,7 +28,7 @@ public static class DeepL
return new DeepLUsage(true, usage.Character!.Count, usage.Character.Limit); return new DeepLUsage(true, usage.Character!.Count, usage.Character.Limit);
} }
catch (AuthorizationException e) catch (AuthorizationException)
{ {
DEEPL_NOT_AVAILABLE = true; DEEPL_NOT_AVAILABLE = true;
return new DeepLUsage(false, 0, 1, true); return new DeepLUsage(false, 0, 1, true);
@ -63,7 +63,7 @@ public static class DeepL
return translation.Text; return translation.Text;
} }
catch (AuthorizationException e) catch (AuthorizationException)
{ {
DEEPL_NOT_AVAILABLE = true; DEEPL_NOT_AVAILABLE = true;
return string.Empty; return string.Empty;

View File

@ -0,0 +1,141 @@
using System.Text;
using DataModel.Database;
namespace Processor.Generators;
public class DotnetBigFile : IGenerator
{
private static readonly List<string> CULTURE_CODES = new();
private static int DEFAULT_CULTURE_INDEX = -1;
public async Task GenerateAsync()
{
const string filename = "I18N.cs";
var destPath = await AppSettings.GetGeneratorDotnetDestinationPath();
destPath = Environment.ExpandEnvironmentVariables(destPath);
var pathFinal = Path.Join(destPath, filename);
var pathTemp = Path.Join(destPath, filename + ".gen");
if(File.Exists(pathTemp))
File.Delete(pathTemp);
CULTURE_CODES.Clear();
var cultures = await AppSettings.GetCultureInfos();
foreach (var (code, _) in cultures)
CULTURE_CODES.Add(code);
DEFAULT_CULTURE_INDEX = await AppSettings.GetGeneratorDotnetDefaultCultureIndex();
DEFAULT_CULTURE_INDEX -= 1; // 1-based to 0-based
try
{
await using var fileStream = new FileStream(pathTemp, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using var writer = new StreamWriter(fileStream, Encoding.UTF8);
await writer.WriteLineAsync($"namespace {await AppSettings.GetGeneratorDotnetNamespace()};");
await this.CreateStaticClass(writer, "I18N", 0, async (streamWriter, indention) =>
{
var indentionString = this.AddIndention(indention);
var buildTime = DateTime.UtcNow;
await writer.WriteLineAsync($"{indentionString}public static readonly string BUILD_TIME = \"{buildTime:yyyy.MM.dd HH:mm:ss}\";");
await writer.WriteLineAsync($"{indentionString}public static readonly long BUILD_TIME_TICKS = {buildTime.Ticks};");
await writer.WriteLineAsync();
await writer.WriteLineAsync($"{indentionString}private static int PREVIOUS_CULTURE = -1;");
// Go through the first layer of sections:
var sections = await SectionProcessor.LoadLayer(0);
foreach (var section in sections)
await this.TransformSection(writer, indention, section);
});
}
finally
{
if(new FileInfo(pathTemp).Length > 0)
{
if(File.Exists(pathFinal))
File.Delete(pathFinal);
File.Move(pathTemp, pathFinal);
}
}
}
private string AddIndention(int indention) => new string(' ', indention * 3);
private async Task TransformSection(TextWriter writer, int indention, Section section)
{
await this.CreateStaticClass(writer, section.DataKey, indention, async (_, innerIndention) =>
{
var textElements = section.TextElements;
foreach (var textElement in textElements)
await this.TransformTextElement(writer, innerIndention, textElement);
var childSections = await SectionProcessor.GetChildSections(section.DataKey);
foreach (var childSection in childSections)
await this.TransformSection(writer, innerIndention, childSection);
});
}
private async Task TransformTextElement(TextWriter writer, int indention, TextElement textElement)
{
var indentionString = this.AddIndention(indention);
var indentionPropString = this.AddIndention(indention + 1);
var indentionPropInner1String = this.AddIndention(indention + 2);
var indentionPropInner2String = this.AddIndention(indention + 3);
var indentionPropInner3String = this.AddIndention(indention + 4);
await writer.WriteLineAsync($"{indentionString}private static string E_{textElement.Code}_CACHE = \"\";");
await writer.WriteLineAsync($"{indentionString}public static string E_{textElement.Code}");
await writer.WriteLineAsync($"{indentionString}{{");
await writer.WriteLineAsync($"{indentionPropString}get");
await writer.WriteLineAsync($"{indentionPropString}{{");
await writer.WriteLineAsync($"{indentionPropInner1String}var currentCulture = CultureInfo.CurrentCulture.Name;");
await writer.WriteLineAsync($"{indentionPropInner1String}if(PREVIOUS_CULTURE == currentCulture.GetHashCode())");
await writer.WriteLineAsync($"{indentionPropInner2String}return E_{textElement.Code}_CACHE;");
await writer.WriteLineAsync($"{indentionPropInner1String}else");
await writer.WriteLineAsync($"{indentionPropInner1String}{{");
await writer.WriteLineAsync($"{indentionPropInner2String}PREVIOUS_CULTURE = currentCulture.GetHashCode();");
for (var cultureIndex = 0; cultureIndex < CULTURE_CODES.Count; cultureIndex++)
{
if(cultureIndex == 0)
await writer.WriteLineAsync($"{indentionPropInner2String}if (currentCulture.StartsWith(\"{CULTURE_CODES[cultureIndex]}\", StringComparison.InvariantCultureIgnoreCase))");
else
await writer.WriteLineAsync($"{indentionPropInner2String}else if (currentCulture.StartsWith(\"{CULTURE_CODES[cultureIndex]}\", StringComparison.InvariantCultureIgnoreCase))");
await writer.WriteLineAsync($"{indentionPropInner2String}{{");
var cultureTranslation = textElement.Translations.FirstOrDefault(x => x.Culture == CULTURE_CODES[cultureIndex]);
var cultureText = cultureTranslation?.Text ?? string.Empty;
await writer.WriteLineAsync($"{indentionPropInner3String}var text = @\"{Utils.MadeVerbatimStringLiteral(cultureText)}\";");
await writer.WriteLineAsync($"{indentionPropInner3String}E_{textElement.Code}_CACHE = text;");
await writer.WriteLineAsync($"{indentionPropInner3String}return text;");
await writer.WriteLineAsync($"{indentionPropInner2String}}}");
}
// Add the default case:
await writer.WriteLineAsync($"{indentionPropInner2String}else");
await writer.WriteLineAsync($"{indentionPropInner2String}{{");
var defaultCultureTranslation = textElement.Translations.FirstOrDefault(x => x.Culture == CULTURE_CODES[DEFAULT_CULTURE_INDEX]);
var defaultCultureText = defaultCultureTranslation?.Text ?? string.Empty;
await writer.WriteLineAsync($"{indentionPropInner3String}var text = @\"{Utils.MadeVerbatimStringLiteral(defaultCultureText)}\";");
await writer.WriteLineAsync($"{indentionPropInner3String}E_{textElement.Code}_CACHE = text;");
await writer.WriteLineAsync($"{indentionPropInner3String}return text;");
await writer.WriteLineAsync($"{indentionPropInner2String}}}");
await writer.WriteLineAsync($"{indentionPropInner1String}}}");
await writer.WriteLineAsync($"{indentionPropString}}}");
await writer.WriteLineAsync($"{indentionString}}}");
await writer.WriteLineAsync();
}
private async Task CreateStaticClass(TextWriter writer, string name, int indention, Func<TextWriter, int, Task> content)
{
var indentionString = this.AddIndention(indention);
await writer.WriteLineAsync(indentionString);
await writer.WriteLineAsync($"{indentionString}public static class {name}");
await writer.WriteLineAsync($"{indentionString}{{");
await content(writer, indention + 1);
await writer.WriteLineAsync($"{indentionString}}}");
}
}

View File

@ -0,0 +1,26 @@
namespace Processor.Generators;
public static class Generator
{
private static readonly Dictionary<Type, IGenerator> GENERATORS = new();
private static readonly IGenerator VOID_GENERATOR = new VoidGenerator();
public static IGenerator Get(Type genType) => genType switch
{
Type.DOTNET => GENERATORS.ContainsKey(genType) ? GENERATORS[genType] : GENERATORS[genType] = new DotnetBigFile(),
_ => VOID_GENERATOR,
};
public static async Task TriggerAllAsync()
{
var dotnetEnabled = await AppSettings.GetGeneratorDotnetEnabled();
var godotEnabled = await AppSettings.GetGeneratorGodotEnabled();
if (dotnetEnabled)
await Generator.Get(Type.DOTNET).GenerateAsync();
if(godotEnabled)
await Generator.Get(Type.GODOT).GenerateAsync();
}
}

View File

@ -0,0 +1,6 @@
namespace Processor.Generators;
public interface IGenerator
{
public Task GenerateAsync();
}

View File

@ -0,0 +1,9 @@
namespace Processor.Generators;
public enum Type
{
NONE,
DOTNET,
GODOT,
}

View File

@ -0,0 +1,6 @@
namespace Processor.Generators;
public class VoidGenerator : IGenerator
{
public Task GenerateAsync() => Task.CompletedTask;
}

View File

@ -198,4 +198,12 @@ public static class SectionProcessor
return $"Section's path: {path}"; return $"Section's path: {path}";
} }
public static async Task<List<Section>> GetChildSections(string sectionKey)
{
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
var section = await db.Sections.FirstAsync(n => n.DataKey == sectionKey);
return await db.Sections.Where(n => n.Parent == section).ToListAsync();
}
} }

View File

@ -124,7 +124,7 @@ public static class TextElementProcessor
// Save the changes: // Save the changes:
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
catch (DbUpdateException updateException) catch (DbUpdateException)
{ {
} }
} }

View File

@ -1,4 +1,5 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Processor; namespace Processor;
@ -46,4 +47,25 @@ internal static class Utils
return code; return code;
} }
public static string MadeVerbatimStringLiteral(string text)
{
IEnumerable<char> ConvertAll(string source)
{
foreach(var c in source)
if(c == '"')
{
yield return '"';
yield return '"';
}
else
yield return c;
}
var sb = new StringBuilder();
foreach(var c in ConvertAll(text))
sb.Append(c);
return sb.ToString();
}
} }

View File

@ -1,5 +1,6 @@
using DataModel.Database; using DataModel.Database;
using Processor; using Processor;
using Processor.Generators;
using UI_WinForms.Dialogs; using UI_WinForms.Dialogs;
using UI_WinForms.Resources; using UI_WinForms.Resources;
@ -293,8 +294,10 @@ public partial class SectionTree : UserControl
selectedNode.Name = alteredSection.Result.DataKey; // [sic] name is the key selectedNode.Name = alteredSection.Result.DataKey; // [sic] name is the key
} }
private void buttonGenerate_Click(object sender, EventArgs e) private async void buttonGenerate_Click(object sender, EventArgs e)
{ {
this.buttonGenerate.Enabled = false;
await Generator.TriggerAllAsync();
this.buttonGenerate.Enabled = true;
} }
} }

View File

@ -523,6 +523,82 @@ public sealed partial class Setting : UserControl
return new Setting(settingData); return new Setting(settingData);
} }
private static async Task<Setting> ShowGeneratorDotnetNamespaceSettingAsync()
{
var currentSetting = await AppSettings.GetGeneratorDotnetNamespace();
var settingData = new SettingUIData(
Icon: Icons.icons8_code_512,
SettingName: () => "Generator: .NET Namespace",
ChangeNeedsRestart: false,
SettingExplanation: () => "The namespace for the .NET I18N files.",
SettingExplanationLink: () => (string.Empty, string.Empty),
SetupDataControl: (changeTrigger) =>
{
// Set up a textbox:
var textbox = new TextBox();
textbox.Text = currentSetting;
textbox.TextChanged += async (sender, args) => await AppSettings.SetGeneratorDotnetNamespace(textbox.Text);
textbox.TextChanged += (sender, args) => changeTrigger();
textbox.Dock = DockStyle.Fill;
textbox.Margin = new Padding(0, 13, 0, 13);
return textbox;
}
);
return new Setting(settingData);
}
private static async Task<Setting> ShowGeneratorDotnetDefaultCultureSettingAsync()
{
var currentSourceCultureIndex = await AppSettings.GetGeneratorDotnetDefaultCultureIndex();
// We load the corresponding culture for that index. As dropdown items, we show
// all other available cultures:
var allCultures = await AppSettings.GetCultureInfos();
// Attention: We have to store the culture's index, because the index is not
// continuous and can change when the user adds or removes a culture!
var settingData = new SettingUIData(
Icon: Icons.icons8_code_512,
SettingName: () => "Generator: .NET Default Culture",
ChangeNeedsRestart: false,
SettingExplanation: () => "The default culture for the .NET, which is used when no culture is specified or available.",
SettingExplanationLink: () => (string.Empty, string.Empty),
SetupDataControl: (changeTrigger) =>
{
var dropdown = new ComboBox();
var currentCultureDropdownIndex = 0;
for (var n = 0; n < allCultures.Count; n++)
{
var cultureInfo = allCultures[n];
if(cultureInfo.Index == currentSourceCultureIndex)
currentCultureDropdownIndex = n;
dropdown.Items.Add(new ComboBoxItem($"{cultureInfo.Index}.: {cultureInfo.Code}", cultureInfo.Index));
}
dropdown.SelectedIndex = currentCultureDropdownIndex;
// Setup the change event handler:
dropdown.SelectedValueChanged += async (sender, args) =>
{
if(dropdown.SelectedItem is ComboBoxItem selectedItem)
await AppSettings.SetGeneratorDotnetDefaultCultureIndex(selectedItem.CultureIndex);
};
dropdown.SelectedValueChanged += (sender, args) => changeTrigger();
// Apply the desired layout:
dropdown.Dock = DockStyle.Fill;
dropdown.DropDownStyle = ComboBoxStyle.DropDownList;
return dropdown;
}
);
return new Setting(settingData);
}
private static async Task<Setting> ShowGeneratorGodotEnabledSettingAsync() private static async Task<Setting> ShowGeneratorGodotEnabledSettingAsync()
{ {
var currentSetting = await AppSettings.GetGeneratorGodotEnabled(); var currentSetting = await AppSettings.GetGeneratorGodotEnabled();
@ -614,6 +690,8 @@ public sealed partial class Setting : UserControl
{ {
yield return ShowGeneratorGodotDestinationPathSettingAsync(); yield return ShowGeneratorGodotDestinationPathSettingAsync();
yield return ShowGeneratorGodotEnabledSettingAsync(); yield return ShowGeneratorGodotEnabledSettingAsync();
yield return ShowGeneratorDotnetDefaultCultureSettingAsync();
yield return ShowGeneratorDotnetNamespaceSettingAsync();
yield return ShowGeneratorDotnetDestinationPathSettingAsync(); yield return ShowGeneratorDotnetDestinationPathSettingAsync();
yield return ShowGeneratorDotnetEnabledSettingAsync(); yield return ShowGeneratorDotnetEnabledSettingAsync();
yield return ShowGeneratorModeSettingAsync(); yield return ShowGeneratorModeSettingAsync();