diff --git a/I18N Commander/DataModel/Database/SettingNames.cs b/I18N Commander/DataModel/Database/SettingNames.cs index f6dd829..7d419cf 100644 --- a/I18N Commander/DataModel/Database/SettingNames.cs +++ b/I18N Commander/DataModel/Database/SettingNames.cs @@ -10,6 +10,8 @@ public static class SettingNames public static readonly string GENERATOR_MODE = "Generator Mode"; 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_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_DESTINATION_PATH = "Generator Godot Destination Path"; } \ No newline at end of file diff --git a/I18N Commander/Processor/AppSettings.cs b/I18N Commander/Processor/AppSettings.cs index 894c57b..20051b7 100644 --- a/I18N Commander/Processor/AppSettings.cs +++ b/I18N Commander/Processor/AppSettings.cs @@ -685,6 +685,154 @@ public static class AppSettings #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 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(); + + // 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(); + + // 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 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(); + + // 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(); + + // 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 private static bool CACHE_GENERATOR_GODOT_ENABLED = true; diff --git a/I18N Commander/Processor/DeepL.cs b/I18N Commander/Processor/DeepL.cs index 3e7d8fd..af3ffe5 100644 --- a/I18N Commander/Processor/DeepL.cs +++ b/I18N Commander/Processor/DeepL.cs @@ -28,7 +28,7 @@ public static class DeepL return new DeepLUsage(true, usage.Character!.Count, usage.Character.Limit); } - catch (AuthorizationException e) + catch (AuthorizationException) { DEEPL_NOT_AVAILABLE = true; return new DeepLUsage(false, 0, 1, true); @@ -63,7 +63,7 @@ public static class DeepL return translation.Text; } - catch (AuthorizationException e) + catch (AuthorizationException) { DEEPL_NOT_AVAILABLE = true; return string.Empty; diff --git a/I18N Commander/Processor/Generators/DotnetBigFile.cs b/I18N Commander/Processor/Generators/DotnetBigFile.cs new file mode 100644 index 0000000..15625fc --- /dev/null +++ b/I18N Commander/Processor/Generators/DotnetBigFile.cs @@ -0,0 +1,141 @@ +using System.Text; +using DataModel.Database; + +namespace Processor.Generators; + +public class DotnetBigFile : IGenerator +{ + private static readonly List 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 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}}}"); + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/Generators/Generator.cs b/I18N Commander/Processor/Generators/Generator.cs new file mode 100644 index 0000000..045911a --- /dev/null +++ b/I18N Commander/Processor/Generators/Generator.cs @@ -0,0 +1,26 @@ +namespace Processor.Generators; + +public static class Generator +{ + private static readonly Dictionary 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(); + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/Generators/IGenerator.cs b/I18N Commander/Processor/Generators/IGenerator.cs new file mode 100644 index 0000000..14f87cc --- /dev/null +++ b/I18N Commander/Processor/Generators/IGenerator.cs @@ -0,0 +1,6 @@ +namespace Processor.Generators; + +public interface IGenerator +{ + public Task GenerateAsync(); +} \ No newline at end of file diff --git a/I18N Commander/Processor/Generators/Type.cs b/I18N Commander/Processor/Generators/Type.cs new file mode 100644 index 0000000..dc1ef79 --- /dev/null +++ b/I18N Commander/Processor/Generators/Type.cs @@ -0,0 +1,9 @@ +namespace Processor.Generators; + +public enum Type +{ + NONE, + + DOTNET, + GODOT, +} \ No newline at end of file diff --git a/I18N Commander/Processor/Generators/VoidGenerator.cs b/I18N Commander/Processor/Generators/VoidGenerator.cs new file mode 100644 index 0000000..c3334fe --- /dev/null +++ b/I18N Commander/Processor/Generators/VoidGenerator.cs @@ -0,0 +1,6 @@ +namespace Processor.Generators; + +public class VoidGenerator : IGenerator +{ + public Task GenerateAsync() => Task.CompletedTask; +} \ No newline at end of file diff --git a/I18N Commander/Processor/SectionProcessor.cs b/I18N Commander/Processor/SectionProcessor.cs index f66275b..614fa48 100644 --- a/I18N Commander/Processor/SectionProcessor.cs +++ b/I18N Commander/Processor/SectionProcessor.cs @@ -198,4 +198,12 @@ public static class SectionProcessor return $"Section's path: {path}"; } + + public static async Task> GetChildSections(string sectionKey) + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + var section = await db.Sections.FirstAsync(n => n.DataKey == sectionKey); + + return await db.Sections.Where(n => n.Parent == section).ToListAsync(); + } } \ No newline at end of file diff --git a/I18N Commander/Processor/TextElementProcessor.cs b/I18N Commander/Processor/TextElementProcessor.cs index ae71b46..1924cab 100644 --- a/I18N Commander/Processor/TextElementProcessor.cs +++ b/I18N Commander/Processor/TextElementProcessor.cs @@ -124,7 +124,7 @@ public static class TextElementProcessor // Save the changes: await db.SaveChangesAsync(); } - catch (DbUpdateException updateException) + catch (DbUpdateException) { } } diff --git a/I18N Commander/Processor/Utils.cs b/I18N Commander/Processor/Utils.cs index 9c19df2..13a625d 100644 --- a/I18N Commander/Processor/Utils.cs +++ b/I18N Commander/Processor/Utils.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using System.Text; using Microsoft.EntityFrameworkCore; namespace Processor; @@ -46,4 +47,25 @@ internal static class Utils return code; } + + public static string MadeVerbatimStringLiteral(string text) + { + IEnumerable 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(); + } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/SectionTree.cs b/I18N Commander/UI WinForms/Components/SectionTree.cs index a62c2f7..c67f531 100644 --- a/I18N Commander/UI WinForms/Components/SectionTree.cs +++ b/I18N Commander/UI WinForms/Components/SectionTree.cs @@ -1,5 +1,6 @@ using DataModel.Database; using Processor; +using Processor.Generators; using UI_WinForms.Dialogs; using UI_WinForms.Resources; @@ -293,8 +294,10 @@ public partial class SectionTree : UserControl 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; } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/Setting.cs b/I18N Commander/UI WinForms/Components/Setting.cs index a2be64f..c73f7a0 100644 --- a/I18N Commander/UI WinForms/Components/Setting.cs +++ b/I18N Commander/UI WinForms/Components/Setting.cs @@ -523,6 +523,82 @@ public sealed partial class Setting : UserControl return new Setting(settingData); } + private static async Task 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 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 ShowGeneratorGodotEnabledSettingAsync() { var currentSetting = await AppSettings.GetGeneratorGodotEnabled(); @@ -614,6 +690,8 @@ public sealed partial class Setting : UserControl { yield return ShowGeneratorGodotDestinationPathSettingAsync(); yield return ShowGeneratorGodotEnabledSettingAsync(); + yield return ShowGeneratorDotnetDefaultCultureSettingAsync(); + yield return ShowGeneratorDotnetNamespaceSettingAsync(); yield return ShowGeneratorDotnetDestinationPathSettingAsync(); yield return ShowGeneratorDotnetEnabledSettingAsync(); yield return ShowGeneratorModeSettingAsync();