diff --git a/I18N Commander/.idea/.idea.I18N Commander/.idea/riderMarkupCache.xml b/I18N Commander/.idea/.idea.I18N Commander/.idea/riderMarkupCache.xml new file mode 100644 index 0000000..4d68344 --- /dev/null +++ b/I18N Commander/.idea/.idea.I18N Commander/.idea/riderMarkupCache.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/I18N Commander/.idea/.idea.I18N Commander/.idea/vcs.xml b/I18N Commander/.idea/.idea.I18N Commander/.idea/vcs.xml index 2e3f692..fa8c83e 100644 --- a/I18N Commander/.idea/.idea.I18N Commander/.idea/vcs.xml +++ b/I18N Commander/.idea/.idea.I18N Commander/.idea/vcs.xml @@ -1,5 +1,12 @@ + + + + + + + diff --git a/I18N Commander/DataModel/Database/SettingGeneratorMode.cs b/I18N Commander/DataModel/Database/SettingGeneratorMode.cs new file mode 100644 index 0000000..bcfab42 --- /dev/null +++ b/I18N Commander/DataModel/Database/SettingGeneratorMode.cs @@ -0,0 +1,7 @@ +namespace DataModel.Database; + +public enum SettingGeneratorMode +{ + AUTOMATIC, + MANUAL, +} \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/SettingNames.cs b/I18N Commander/DataModel/Database/SettingNames.cs index d4ee3fb..7d419cf 100644 --- a/I18N Commander/DataModel/Database/SettingNames.cs +++ b/I18N Commander/DataModel/Database/SettingNames.cs @@ -2,9 +2,16 @@ public static class SettingNames { - public static readonly string DEEPL_SOURCE_CULTURE = "DeepL Source Culture"; public static readonly string CULTURE = "Culture"; public static readonly string DEEPL_ACTION = "DeepL Action"; public static readonly string DEEPL_API_KEY = "DeepL API Key"; + public static readonly string DEEPL_SOURCE_CULTURE = "DeepL Source Culture"; public static readonly string DEEPL_MODE = "DeepL 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_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 86d2a2e..fc3e43f 100644 --- a/I18N Commander/Processor/AppSettings.cs +++ b/I18N Commander/Processor/AppSettings.cs @@ -7,284 +7,200 @@ namespace Processor; public static class AppSettings { + #region Common DB Code + + private static readonly Dictionary CACHES = new(); + private static readonly Dictionary CACHE_LOADED = new(); + + private static async Task GetSetting(string settingName, T defaultValue) + { + // When possible, use the cache: + if (CACHE_LOADED.ContainsKey(settingName) && CACHE_LOADED[settingName]) + return (T)CACHES[settingName]; + + var settingValue = defaultValue; + 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 == settingName) is { } existingSetting) + { + settingValue = settingValue switch + { + bool => (T)(object)existingSetting.BoolValue, + string => (T)(object)existingSetting.TextValue, + int => (T)(object)existingSetting.IntegerValue, + Guid => (T)(object)existingSetting.GuidValue, + + SettingDeepLMode or + SettingDeepLAction or + SettingGeneratorMode => (T)(object)existingSetting.IntegerValue, + + _ => defaultValue, + }; + return settingValue; + } + + // Does not exist, so create it: + var setting = new Setting + { + Code = settingName, + }; + + switch (settingValue) + { + case bool: + setting.BoolValue = (bool)(object)settingValue; + break; + + case string: + setting.TextValue = (string)(object)settingValue; + break; + + case int: + setting.IntegerValue = (int)(object)settingValue; + break; + + case Guid: + setting.GuidValue = (Guid)(object)settingValue; + break; + + case SettingDeepLMode: + case SettingDeepLAction: + case SettingGeneratorMode: + setting.IntegerValue = (int)(object)settingValue; + break; + } + + await db.Settings.AddAsync(setting); + await db.SaveChangesAsync(); + return settingValue; + } + finally + { + CACHE_LOADED[settingName] = true; + CACHES[settingName] = settingValue!; + } + } + + public static async Task SetSetting(string settingName, T settingValue) + { + // Update the cache: + CACHES[settingName] = settingValue!; + CACHE_LOADED[settingName] = 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 == settingName) is { } existingSetting) + { + switch (settingValue) + { + case bool: + existingSetting.BoolValue = (bool)(object)settingValue; + break; + + case string: + existingSetting.TextValue = (string)(object)settingValue; + break; + + case int: + existingSetting.IntegerValue = (int)(object)settingValue; + break; + + case Guid: + existingSetting.GuidValue = (Guid)(object)settingValue; + break; + + case SettingDeepLMode: + case SettingDeepLAction: + case SettingGeneratorMode: + existingSetting.IntegerValue = (int)(object)settingValue; + break; + } + await db.SaveChangesAsync(); + } + + // Does not exist, so create it: + else + { + var setting = new Setting + { + Code = SettingNames.GENERATOR_DOTNET_ENABLED, + }; + + switch (settingValue) + { + case bool: + setting.BoolValue = (bool)(object)settingValue; + break; + + case string: + setting.TextValue = (string)(object)settingValue; + break; + + case int: + setting.IntegerValue = (int)(object)settingValue; + break; + + case Guid: + setting.GuidValue = (Guid)(object)settingValue; + break; + + case SettingDeepLMode: + case SettingDeepLAction: + case SettingGeneratorMode: + setting.IntegerValue = (int)(object)settingValue; + break; + } + + await db.Settings.AddAsync(setting); + await db.SaveChangesAsync(); + } + } + + #endregion + #region DeepL Settings #region DeepL Mode - private static SettingDeepLMode CACHE_DEEPL_MODE = SettingDeepLMode.DISABLED; - private static bool CACHE_DEEPL_MODE_IS_LOADED = false; - - public static async Task SetDeepLMode(SettingDeepLMode mode) - { - // Convert the enum to its int value: - var intValue = (int)mode; - - // Update the cache: - CACHE_DEEPL_MODE = mode; - CACHE_DEEPL_MODE_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.DEEPL_MODE) is {} existingSetting) - { - existingSetting.IntegerValue = intValue; - await db.SaveChangesAsync(); - } - - // Does not exist, so create it: - else - { - var setting = new Setting - { - Code = SettingNames.DEEPL_MODE, - IntegerValue = intValue, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - } - } + public static async Task SetDeepLMode(SettingDeepLMode mode) => await AppSettings.SetSetting(SettingNames.DEEPL_MODE, mode); - public static async Task GetDeepLMode() - { - if (CACHE_DEEPL_MODE_IS_LOADED) - return CACHE_DEEPL_MODE; - - // 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.DEEPL_MODE) is { } existingSetting) - return (SettingDeepLMode) existingSetting.IntegerValue; - - // Does not exist, so create it: - var setting = new Setting - { - Code = SettingNames.DEEPL_MODE, - IntegerValue = (int)SettingDeepLMode.DISABLED, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - - var mode = (SettingDeepLMode) setting.IntegerValue; - CACHE_DEEPL_MODE = mode; - CACHE_DEEPL_MODE_IS_LOADED = true; - - return mode; - } + public static async Task GetDeepLMode() => await AppSettings.GetSetting(SettingNames.DEEPL_MODE, SettingDeepLMode.DISABLED); #endregion #region DeepL API Key - - private static string CACHE_DEEPL_API_KEY = string.Empty; - private static bool CACHE_DEEPL_API_KEY_IS_LOADED = false; - - public static async Task SetDeepLAPIKey(string apiKey) - { - // Update the cache: - CACHE_DEEPL_API_KEY = apiKey; - CACHE_DEEPL_API_KEY_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.DEEPL_API_KEY) is {} existingSetting) - { - existingSetting.TextValue = apiKey; - await db.SaveChangesAsync(); - } - - // Does not exist, so create it: - else - { - var setting = new Setting - { - Code = SettingNames.DEEPL_API_KEY, - TextValue = apiKey, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - } - } - - public static async Task GetDeepLAPIKey() - { - // Check the cache: - if (CACHE_DEEPL_API_KEY_IS_LOADED) - return CACHE_DEEPL_API_KEY; - - // 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.DEEPL_API_KEY) is { } existingSetting) - return existingSetting.TextValue; - - // Does not exist, so create it: - var setting = new Setting - { - Code = SettingNames.DEEPL_API_KEY, - TextValue = string.Empty, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - - var key = setting.TextValue; - CACHE_DEEPL_API_KEY = key; - CACHE_DEEPL_API_KEY_IS_LOADED = true; - - return key; - } - + + public static async Task SetDeepLAPIKey(string apiKey) => await AppSettings.SetSetting(SettingNames.DEEPL_API_KEY, apiKey); + + public static async Task GetDeepLAPIKey() => await AppSettings.GetSetting(SettingNames.DEEPL_API_KEY, string.Empty); + #endregion #region DeepL Action - - private static SettingDeepLAction CACHE_DEEPL_ACTION = SettingDeepLAction.MANUAL; - private static bool CACHE_DEEPL_ACTION_IS_LOADED = false; - public static async Task SetDeepLAction(SettingDeepLAction action) - { - // Convert the enum to its int value: - var intValue = (int)action; - - // Update the cache: - CACHE_DEEPL_ACTION = action; - CACHE_DEEPL_ACTION_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.DEEPL_ACTION) is {} existingSetting) - { - existingSetting.IntegerValue = intValue; - await db.SaveChangesAsync(); - } - - // Does not exist, so create it: - else - { - var setting = new Setting - { - Code = SettingNames.DEEPL_ACTION, - IntegerValue = intValue, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - } - } - - public static async Task GetDeepLAction() - { - // Check the cache: - if (CACHE_DEEPL_ACTION_IS_LOADED) - return CACHE_DEEPL_ACTION; - - // 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.DEEPL_ACTION) is { } existingSetting) - return (SettingDeepLAction) existingSetting.IntegerValue; - - // Does not exist, so create it: - var setting = new Setting - { - Code = SettingNames.DEEPL_ACTION, - IntegerValue = (int)SettingDeepLAction.MANUAL, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - - var action = (SettingDeepLAction) setting.IntegerValue; - CACHE_DEEPL_ACTION = action; - CACHE_DEEPL_ACTION_IS_LOADED = true; - - return action; - } - + public static async Task SetDeepLAction(SettingDeepLAction action) => await AppSettings.SetSetting(SettingNames.DEEPL_ACTION, action); + + public static async Task GetDeepLAction() => await AppSettings.GetSetting(SettingNames.DEEPL_ACTION, SettingDeepLAction.MANUAL); + #endregion #region DeepL Source Culture - private static int CACHE_DEEPL_SOURCE_CULTURE = -1; - private static bool CACHE_DEEPL_SOURCE_CULTURE_IS_LOADED = false; - - public static async Task SetDeepLSourceCultureIndex(int cultureIndex) - { - // Update the cache: - CACHE_DEEPL_SOURCE_CULTURE = cultureIndex; - CACHE_DEEPL_SOURCE_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.DEEPL_SOURCE_CULTURE) is {} existingSetting) - { - existingSetting.IntegerValue = cultureIndex; - await db.SaveChangesAsync(); - } - - // Does not exist, so create it: - else - { - var setting = new Setting - { - Code = SettingNames.DEEPL_SOURCE_CULTURE, - IntegerValue = cultureIndex, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - } - } - - public static async Task GetDeepLSourceCultureIndex() - { - // Check the cache: - if (CACHE_DEEPL_SOURCE_CULTURE_IS_LOADED) - return CACHE_DEEPL_SOURCE_CULTURE; - - // 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.DEEPL_SOURCE_CULTURE) is { } existingSetting) - return existingSetting.IntegerValue; - - // Does not exist, so create it: - var setting = new Setting - { - Code = SettingNames.DEEPL_SOURCE_CULTURE, - IntegerValue = -1, - }; - - await db.Settings.AddAsync(setting); - await db.SaveChangesAsync(); - - var cultureIndex = setting.IntegerValue; - CACHE_DEEPL_SOURCE_CULTURE = cultureIndex; - CACHE_DEEPL_SOURCE_CULTURE_IS_LOADED = true; - - return cultureIndex; - } - + public static async Task SetDeepLSourceCultureIndex(int cultureIndex) => await AppSettings.SetSetting(SettingNames.DEEPL_SOURCE_CULTURE, cultureIndex); + + public static async Task GetDeepLSourceCultureIndex() => await AppSettings.GetSetting(SettingNames.DEEPL_SOURCE_CULTURE, -1); + #endregion #endregion - #region Translation Settings + #region Culture Settings #region List of culture indices @@ -432,4 +348,64 @@ public static class AppSettings #endregion #endregion + + #region Generator Settings + + #region Generator Mode + + public static async Task GetGeneratorMode() => await AppSettings.GetSetting(SettingNames.GENERATOR_MODE, SettingGeneratorMode.MANUAL); + + public static async Task SetGeneratorMode(SettingGeneratorMode mode) => await AppSettings.SetSetting(SettingNames.GENERATOR_MODE, mode); + + #endregion + + #region .NET Generator Enabled/Disabled + + public static async Task GetGeneratorDotnetEnabled() => await AppSettings.GetSetting(SettingNames.GENERATOR_DOTNET_ENABLED, false); + + public static async Task SetGeneratorDotnetEnabled(bool enabled) => await AppSettings.SetSetting(SettingNames.GENERATOR_DOTNET_ENABLED, enabled); + + #endregion + + #region .NET Generator Destination Path + + public static async Task GetGeneratorDotnetDestinationPath() => await AppSettings.GetSetting(SettingNames.GENERATOR_DOTNET_DESTINATION_PATH, string.Empty); + + public static async Task SetGeneratorDotnetDestinationPath(string path) => await AppSettings.SetSetting(SettingNames.GENERATOR_DOTNET_DESTINATION_PATH, path); + + #endregion + + #region .NET Generator Namespace + + public static async Task GetGeneratorDotnetNamespace() => await AppSettings.GetSetting(SettingNames.GENERATOR_DOTNET_NAMESPACE, "I18N"); + + public static async Task SetGeneratorDotnetNamespace(string updatedNamespace) => await AppSettings.SetSetting(SettingNames.GENERATOR_DOTNET_NAMESPACE, updatedNamespace); + + #endregion + + #region .NET Generator Default Culture + + public static async Task GetGeneratorDotnetDefaultCultureIndex() => await AppSettings.GetSetting(SettingNames.GENERATOR_DOTNET_DEFAULT_CULTURE, 0); + + public static async Task SetGeneratorDotnetDefaultCultureIndex(int updatedCulture) => await AppSettings.SetSetting(SettingNames.GENERATOR_DOTNET_DEFAULT_CULTURE, updatedCulture); + + #endregion + + #region Godot Generator Enabled/Disabled + + public static async Task GetGeneratorGodotEnabled() => await AppSettings.GetSetting(SettingNames.GENERATOR_GODOT_ENABLED, false); + + public static async Task SetGeneratorGodotEnabled(bool enabled) => await AppSettings.SetSetting(SettingNames.GENERATOR_GODOT_ENABLED, enabled); + + #endregion + + #region Godot Generator Destination Path + + public static async Task GetGeneratorGodotDestinationPath() => await AppSettings.GetSetting(SettingNames.GENERATOR_GODOT_DESTINATION_PATH, string.Empty); + + public static async Task SetGeneratorGodotDestinationPath(string path) => await AppSettings.SetSetting(SettingNames.GENERATOR_GODOT_DESTINATION_PATH, path); + + #endregion + + #endregion } \ No newline at end of file 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..7b20abd --- /dev/null +++ b/I18N Commander/Processor/Generators/DotnetBigFile.cs @@ -0,0 +1,175 @@ +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); + long destBytesWritten = 0; + + var pathFinal = Path.Join(destPath, filename); + var pathTemp = Path.Join(destPath, filename + ".gen"); + var issueFinal = string.Empty; + + try + { + if(File.Exists(pathTemp)) + File.Delete(pathTemp); + } + catch (IOException e) + { + return new ProcessorResult(0, false, $"Cannot delete the temporary file: '{e.Message}'. Hint: Is the ransomware protection enabled in your Windows system? If so, please make sure that the I18N Commander has write permission."); + } + + 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); + }); + } + catch (IOException e) + { + // Happens, e.g. when the ransomware protection on Windows is active and + // the I18N commander is not on the exclusion list. + return new ProcessorResult(0, false, $"Cannot write the generator's result file: '{e.Message}'. Hint: Is the ransomware protection enabled in your Windows system? If so, please make sure that the I18N Commander has write permission."); + } + finally + { + if (File.Exists(pathTemp)) + { + destBytesWritten = new FileInfo(pathTemp).Length; + if (destBytesWritten > 0) + { + try + { + if (File.Exists(pathFinal)) + File.Delete(pathFinal); + + File.Move(pathTemp, pathFinal); + } + catch (IOException e) + { + // Happens when the file is still in use by the compiler, the IDE, etc. + // Depends on the timing, this happens sometimes. We ignore it, though. + issueFinal = e.Message; + } + } + } + } + + if(string.IsNullOrWhiteSpace(issueFinal)) + return new ProcessorResult(destBytesWritten, true, string.Empty); + else + return new ProcessorResult(0, false, $"Cannot move the generator's result file to the destination: '{issueFinal}'. Hint: Is the ransomware protection enabled in your Windows system? If so, please make sure that the I18N Commander has write permission."); + } + + 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..29ed588 --- /dev/null +++ b/I18N Commander/Processor/Generators/Generator.cs @@ -0,0 +1,41 @@ +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(); + long bytesWritten = 0; + + if (dotnetEnabled) + { + var result = await Generator.Get(Type.DOTNET).GenerateAsync(); + if(!result.Successful) + return result; + + bytesWritten += result.Result; + } + + if(godotEnabled) + { + var result = await Generator.Get(Type.GODOT).GenerateAsync(); + if(!result.Successful) + return result; + + bytesWritten += result.Result; + } + + return new ProcessorResult(bytesWritten); + } +} \ 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..39ded9e --- /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/AppEvents.cs b/I18N Commander/UI WinForms/AppEvents.cs index 5ba9c0d..1145e8e 100644 --- a/I18N Commander/UI WinForms/AppEvents.cs +++ b/I18N Commander/UI WinForms/AppEvents.cs @@ -4,10 +4,26 @@ namespace UI_WinForms; internal static class AppEvents { + internal static void ResetAllSubscriptions() + { + WhenSettingsChanged = null; + WhenSectionChanged = null; + WhenTextElementChanged = null; + WhenTranslationChanged = null; + } + + #region Event: Settings were + + internal static event EventHandler? WhenSettingsChanged; + + internal static void SettingsChanged() => WhenSettingsChanged?.Invoke(null, EventArgs.Empty); + + #endregion + #region Event: Section was changed // Section changed event which can be subscribed: - internal static event EventHandler
WhenSectionChanged; + internal static event EventHandler
? WhenSectionChanged; // Method to raise the section changed event: internal static void SectionChanged(Section section) => WhenSectionChanged?.Invoke(null, section); @@ -17,7 +33,7 @@ internal static class AppEvents #region Event: Text element was changed // Text element changed event which can be subscribed: - internal static event EventHandler WhenTextElementChanged; + internal static event EventHandler? WhenTextElementChanged; // Method to raise the text element changed event: internal static void TextElementChanged(TextElement? textElement) => WhenTextElementChanged?.Invoke(null, textElement); @@ -27,7 +43,7 @@ internal static class AppEvents #region Translation was changed // Translation changed event which can be subscribed: - internal static event EventHandler WhenTranslationChanged; + internal static event EventHandler? WhenTranslationChanged; // Method to raise the translation changed event: internal static void TranslationChanged(Translation? translation) => WhenTranslationChanged?.Invoke(null, translation); diff --git a/I18N Commander/UI WinForms/Components/Main.cs b/I18N Commander/UI WinForms/Components/Main.cs index af58cec..b4e1950 100644 --- a/I18N Commander/UI WinForms/Components/Main.cs +++ b/I18N Commander/UI WinForms/Components/Main.cs @@ -16,6 +16,7 @@ public partial class Main : UserControl if(result == DialogResult.Yes) { Program.RestartMainApp = true; + AppEvents.ResetAllSubscriptions(); this.ParentForm!.Close(); } } diff --git a/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs b/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs index ebd2639..219fbad 100644 --- a/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs +++ b/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs @@ -34,6 +34,7 @@ this.buttonAdd = new System.Windows.Forms.Button(); this.buttonRemove = new System.Windows.Forms.Button(); this.buttonRename = new System.Windows.Forms.Button(); + this.buttonGenerate = new System.Windows.Forms.Button(); this.treeView = new System.Windows.Forms.TreeView(); this.toolTip = new System.Windows.Forms.ToolTip(this.components); this.tableLayout.SuspendLayout(); @@ -61,6 +62,7 @@ this.flowLayoutBottom.Controls.Add(this.buttonAdd); this.flowLayoutBottom.Controls.Add(this.buttonRemove); this.flowLayoutBottom.Controls.Add(this.buttonRename); + this.flowLayoutBottom.Controls.Add(this.buttonGenerate); this.flowLayoutBottom.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutBottom.Location = new System.Drawing.Point(0, 445); this.flowLayoutBottom.Margin = new System.Windows.Forms.Padding(0); @@ -115,6 +117,22 @@ this.buttonRename.UseVisualStyleBackColor = true; this.buttonRename.Click += new System.EventHandler(this.buttonRename_Click); // + // buttonGenerate + // + this.buttonGenerate.AutoSize = true; + this.buttonGenerate.Enabled = false; + this.buttonGenerate.FlatAppearance.BorderSize = 0; + this.buttonGenerate.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.buttonGenerate.Image = global::UI_WinForms.Resources.Icons.icons8_code_512__2_; + this.buttonGenerate.Location = new System.Drawing.Point(201, 3); + this.buttonGenerate.Name = "buttonGenerate"; + this.buttonGenerate.Size = new System.Drawing.Size(60, 60); + this.buttonGenerate.TabIndex = 3; + this.buttonGenerate.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + this.toolTip.SetToolTip(this.buttonGenerate, "Triggers all enabled generators"); + this.buttonGenerate.UseVisualStyleBackColor = true; + this.buttonGenerate.Click += new System.EventHandler(this.buttonGenerate_Click); + // // treeView // this.treeView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; @@ -159,5 +177,6 @@ private TreeView treeView; private ToolTip toolTip; private Button buttonRename; + private Button buttonGenerate; } } diff --git a/I18N Commander/UI WinForms/Components/SectionTree.cs b/I18N Commander/UI WinForms/Components/SectionTree.cs index d402363..878fe74 100644 --- a/I18N Commander/UI WinForms/Components/SectionTree.cs +++ b/I18N Commander/UI WinForms/Components/SectionTree.cs @@ -1,13 +1,16 @@ using DataModel.Database; using Processor; +using Processor.Generators; using UI_WinForms.Dialogs; using UI_WinForms.Resources; +using Timer = System.Timers.Timer; namespace UI_WinForms.Components; public partial class SectionTree : UserControl { - private static readonly Dictionary> NODE_PROGRESS_HANDLERS = new(); + private static readonly Dictionary?> NODE_PROGRESS_HANDLERS = new(); + private readonly Timer generatorTimer; public SectionTree() { @@ -26,10 +29,39 @@ public partial class SectionTree : UserControl // Set the image list to the tree view: this.treeView.ImageList = imgList; + // The generator timer: + this.generatorTimer = new Timer + { + Enabled = false, // disable timer for now, + Interval = 6_000, // 6 second interval, + AutoReset = false, // runs only once + }; + + this.generatorTimer.Elapsed += async (sender, args) => await this.PerformGenerators(); + // Subscribe to the load event: this.Load += this.LoadNodes; + this.Load += (sender, args) => this.SetupGeneratorButton(); + + // Subscribe to all event for triggering the generators: + AppEvents.WhenTranslationChanged += (sender, translation) => this.GeneratorEvent(); + AppEvents.WhenSettingsChanged += (sender, args) => this.GeneratorEvent(); } + private async void SetupGeneratorButton() + { + this.buttonGenerate.Enabled = false; + + // Depend the generator button's visibility on the generator settings: + this.buttonGenerate.Enabled = await AppSettings.GetGeneratorDotnetEnabled() || await AppSettings.GetGeneratorGodotEnabled(); + + // Subscribe to the changed settings event: + AppEvents.WhenSettingsChanged += async (sender, args) => + { + this.buttonGenerate.Enabled = await AppSettings.GetGeneratorDotnetEnabled() || await AppSettings.GetGeneratorGodotEnabled(); + }; + } + private async void LoadNodes(object? sender, EventArgs e) { if(this.DesignMode) @@ -277,4 +309,31 @@ public partial class SectionTree : UserControl selectedNode.Text = alteredSection.Result!.Name; selectedNode.Name = alteredSection.Result.DataKey; // [sic] name is the key } + + private async void buttonGenerate_Click(object sender, EventArgs e) => await this.PerformGenerators(); + + private async void GeneratorEvent() + { + if(await AppSettings.GetGeneratorMode() == SettingGeneratorMode.MANUAL) + return; + + if (this.generatorTimer.Enabled) + this.generatorTimer.Stop(); + this.generatorTimer.Start(); + } + + private async Task PerformGenerators() + { + if (this.buttonGenerate.InvokeRequired) + { + await this.buttonGenerate.Invoke(this.PerformGenerators); + return; + } + + this.buttonGenerate.Enabled = false; + var result = await Generator.TriggerAllAsync(); + result.ProcessError(); + + 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 2bee739..0d248b0 100644 --- a/I18N Commander/UI WinForms/Components/Setting.cs +++ b/I18N Commander/UI WinForms/Components/Setting.cs @@ -77,6 +77,8 @@ public sealed partial class Setting : UserControl { if(this.data.ChangeNeedsRestart) this.needRestart = true; + + AppEvents.SettingsChanged(); } private void UpdateExplanation() => this.labelExplanation.Text = this.data.SettingExplanation(); @@ -210,7 +212,7 @@ public sealed partial class Setting : UserControl }; // Update the explanation text. Therefore, we need to get the setting object through the chain of parents: - var setting = (Setting) ((Control) sender).Parent.Parent.Parent; + var setting = (Setting) (((Control) sender!)!).Parent.Parent.Parent; setting.UpdateExplanation(); }; @@ -257,7 +259,6 @@ public sealed partial class Setting : UserControl }; // Setup the change event handler: - dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); dropdown.SelectedValueChanged += async (sender, args) => await AppSettings.SetDeepLAction(dropdown.SelectedIndex switch { 0 => SettingDeepLAction.MANUAL, @@ -265,6 +266,7 @@ public sealed partial class Setting : UserControl _ => SettingDeepLAction.MANUAL, }); + dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); // Apply the desired layout: dropdown.Dock = DockStyle.Fill; @@ -314,12 +316,12 @@ public sealed partial class Setting : UserControl dropdown.SelectedIndex = currentCultureDropdownIndex; // Setup the change event handler: - dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); dropdown.SelectedValueChanged += async (sender, args) => { if(dropdown.SelectedItem is ComboBoxItem selectedItem) await AppSettings.SetDeepLSourceCultureIndex(selectedItem.CultureIndex); }; + dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); // Apply the desired layout: dropdown.Dock = DockStyle.Fill; @@ -388,14 +390,314 @@ public sealed partial class Setting : UserControl cultureIndices.Remove(innerLoopIndex); } } + + private static async Task ShowGeneratorModeSettingAsync() + { + var currentSetting = await AppSettings.GetGeneratorMode(); + + var settingData = new SettingUIData( + Icon: Icons.icons8_code_512, + SettingName: () => "Generator Mode", + ChangeNeedsRestart: false, + SettingExplanation: () => "The generator mode determines how the translation files are generated.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // We set up a combo box with the available actions: + var dropdown = new ComboBox(); + dropdown.Items.Add("Automatic generation"); + dropdown.Items.Add("Manual generation"); + dropdown.SelectedIndex = currentSetting switch + { + SettingGeneratorMode.AUTOMATIC => 0, + SettingGeneratorMode.MANUAL => 1, + + _ => 0, + }; + + // Setup the change event handler: + dropdown.SelectedValueChanged += async (sender, args) => await AppSettings.SetGeneratorMode(dropdown.SelectedIndex switch + { + 0 => SettingGeneratorMode.AUTOMATIC, + 1 => SettingGeneratorMode.MANUAL, + + _ => SettingGeneratorMode.AUTOMATIC, + }); + 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 ShowGeneratorDotnetEnabledSettingAsync() + { + var currentSetting = await AppSettings.GetGeneratorDotnetEnabled(); + + var settingData = new SettingUIData( + Icon: Icons.icons8_code_512, + SettingName: () => "Generator: .NET", + ChangeNeedsRestart: false, + SettingExplanation: () => "When enabled, .NET translation files are generated. Requires a .NET 6 or newer project.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up an checkbox: + var checkbox = new CheckBox(); + checkbox.Checked = currentSetting; + checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetGeneratorDotnetEnabled(checkbox.Checked); + checkbox.CheckedChanged += (sender, args) => changeTrigger(); + checkbox.Text = "Enable .NET Generator"; + + // Apply the desired layout: + checkbox.Dock = DockStyle.Fill; + return checkbox; + } + ); + + return new Setting(settingData); + } + + private static async Task ShowGeneratorDotnetDestinationPathSettingAsync() + { + var currentSetting = await AppSettings.GetGeneratorDotnetDestinationPath(); + + var settingData = new SettingUIData( + Icon: Icons.icons8_code_512, + SettingName: () => "Generator: .NET Destination Path", + ChangeNeedsRestart: false, + SettingExplanation: () => "The destination path for the .NET translation files. You might use environment variables like %USERPROFILE%.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up a horizontal layout: + var layout = new TableLayoutPanel(); + layout.ColumnCount = 2; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 66F)); + layout.RowCount = 1; + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + layout.Dock = DockStyle.Fill; + + // Set up a textbox: + var textbox = new TextBox(); + textbox.Text = currentSetting; + textbox.TextChanged += async (sender, args) => await AppSettings.SetGeneratorDotnetDestinationPath(textbox.Text); + textbox.TextChanged += (sender, args) => changeTrigger(); + textbox.Dock = DockStyle.Fill; + textbox.Margin = new Padding(0, 13, 0, 13); + layout.Controls.Add(textbox, 0, 0); + + // Set up a button: + var button = new Button(); + button.Text = string.Empty; + button.Image = Icons.icons8_folder_tree_512; + button.FlatStyle = FlatStyle.Flat; + button.FlatAppearance.BorderSize = 0; + button.BackColor = Color.Empty; + button.UseVisualStyleBackColor = true; + button.Size = new Size(60, 60); + button.Click += (sender, args) => + { + var dialog = new FolderBrowserDialog(); + dialog.SelectedPath = textbox.Text; + dialog.InitialDirectory = textbox.Text; + dialog.Description = "Select the destination path for the .NET translation files."; + dialog.ShowNewFolderButton = true; + if (dialog.ShowDialog() == DialogResult.OK) + textbox.Text = dialog.SelectedPath; + }; + button.Dock = DockStyle.Fill; + layout.Controls.Add(button, 1, 0); + + return layout; + } + ); + + 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(); + + var settingData = new SettingUIData( + Icon: Icons.icons8_code_512, + SettingName: () => "Generator: Godot", + ChangeNeedsRestart: false, + SettingExplanation: () => "When enabled, Godot translation files are generated. Requires a Godot 3.5 or newer project.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up an checkbox: + var checkbox = new CheckBox(); + checkbox.Checked = currentSetting; + checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetGeneratorGodotEnabled(checkbox.Checked); + checkbox.CheckedChanged += (sender, args) => changeTrigger(); + checkbox.Text = "Enable Godot Generator"; + + // Apply the desired layout: + checkbox.Dock = DockStyle.Fill; + return checkbox; + } + ); + + return new Setting(settingData); + } + + private static async Task ShowGeneratorGodotDestinationPathSettingAsync() + { + var currentSetting = await AppSettings.GetGeneratorGodotDestinationPath(); + + var settingData = new SettingUIData( + Icon: Icons.icons8_code_512, + SettingName: () => "Generator: Godot Destination Path", + ChangeNeedsRestart: false, + SettingExplanation: () => "The destination path for the Godot translation files. You might use environment variables like %USERPROFILE%.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up a horizontal layout: + var layout = new TableLayoutPanel(); + layout.ColumnCount = 2; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 66F)); + layout.RowCount = 1; + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + layout.Dock = DockStyle.Fill; + + // Set up a textbox: + var textbox = new TextBox(); + textbox.Text = currentSetting; + textbox.TextChanged += async (sender, args) => await AppSettings.SetGeneratorGodotDestinationPath(textbox.Text); + textbox.TextChanged += (sender, args) => changeTrigger(); + textbox.Dock = DockStyle.Fill; + textbox.Margin = new Padding(0, 13, 0, 13); + layout.Controls.Add(textbox, 0, 0); + + // Set up a button: + var button = new Button(); + button.Text = string.Empty; + button.Image = Icons.icons8_folder_tree_512; + button.FlatStyle = FlatStyle.Flat; + button.FlatAppearance.BorderSize = 0; + button.BackColor = Color.Empty; + button.UseVisualStyleBackColor = true; + button.Size = new Size(60, 60); + button.Click += (sender, args) => + { + var dialog = new FolderBrowserDialog(); + dialog.SelectedPath = textbox.Text; + dialog.InitialDirectory = textbox.Text; + dialog.Description = "Select the destination path for the Godot translation files."; + dialog.ShowNewFolderButton = true; + if (dialog.ShowDialog() == DialogResult.OK) + textbox.Text = dialog.SelectedPath; + }; + button.Dock = DockStyle.Fill; + layout.Controls.Add(button, 1, 0); + + return layout; + } + ); + + return new Setting(settingData); + } public static IEnumerable> GetAllSettings() { + yield return ShowGeneratorGodotDestinationPathSettingAsync(); + yield return ShowGeneratorGodotEnabledSettingAsync(); + yield return ShowGeneratorDotnetDefaultCultureSettingAsync(); + yield return ShowGeneratorDotnetNamespaceSettingAsync(); + yield return ShowGeneratorDotnetDestinationPathSettingAsync(); + yield return ShowGeneratorDotnetEnabledSettingAsync(); + yield return ShowGeneratorModeSettingAsync(); yield return ShowDeepLSourceCultureSettingAsync(); foreach (var setting in ShowCultureSettingsAsync()) - { yield return setting; - } yield return ShowDeepLUsageSettingAsync(); yield return ShowDeepLActionSettingAsync(); diff --git a/I18N Commander/UI WinForms/Components/Translation.cs b/I18N Commander/UI WinForms/Components/Translation.cs index 80cee88..7f664cc 100644 --- a/I18N Commander/UI WinForms/Components/Translation.cs +++ b/I18N Commander/UI WinForms/Components/Translation.cs @@ -22,6 +22,9 @@ public sealed partial class Translation : UserControl { this.InitializeComponent(); this.Dock = DockStyle.Top; + + this.saveTimer = new Timer(); + this.translationTimer = new Timer(); } public Translation(AppSettings.CultureInfo cultureInfo) @@ -121,7 +124,7 @@ public sealed partial class Translation : UserControl } } - private async void SaveChanges(object? sender, ElapsedEventArgs e) + private async void SaveChanges(object? sender, ElapsedEventArgs? e) { if (this.currentTranslationId > -1) { @@ -135,7 +138,7 @@ public sealed partial class Translation : UserControl this.isManualOnlyMode = this.checkBoxManual.Checked; } - private async void TriggerTranslateNow(object? sender, ElapsedEventArgs e) + private async void TriggerTranslateNow(object? sender, ElapsedEventArgs? e) { if (this.currentTranslationId < 0 || this.isDeepLSourceCulture == false) return; diff --git a/I18N Commander/UI WinForms/ExtensionsError.cs b/I18N Commander/UI WinForms/ExtensionsError.cs index e21b8ee..f265e66 100644 --- a/I18N Commander/UI WinForms/ExtensionsError.cs +++ b/I18N Commander/UI WinForms/ExtensionsError.cs @@ -6,7 +6,8 @@ public static class ExtensionsError { public static void ProcessError(this ProcessorResult result) { - if (result.Successful) return; + if (result.Successful) + return; MessageBox.Show(result.ErrorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } diff --git a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs index cedf6c1..cf5126b 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs +++ b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs @@ -140,6 +140,26 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_code_512 { + get { + object obj = ResourceManager.GetObject("icons8_code_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_code_512__2_ { + get { + object obj = ResourceManager.GetObject("icons8_code_512__2_", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -180,6 +200,16 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_folder_tree_512 { + get { + object obj = ResourceManager.GetObject("icons8_folder_tree_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/I18N Commander/UI WinForms/Resources/Icons.resx b/I18N Commander/UI WinForms/Resources/Icons.resx index 9cfa843..8e6f5c8 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.resx +++ b/I18N Commander/UI WinForms/Resources/Icons.resx @@ -142,6 +142,12 @@ icons8-chat-bubble-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-code-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + icons8-code-512 (2).png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-collectibles-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -154,6 +160,9 @@ icons8-document-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-folder-tree-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-increase-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/I18N Commander/UI WinForms/Resources/icons8-code-512 (2).png b/I18N Commander/UI WinForms/Resources/icons8-code-512 (2).png new file mode 100644 index 0000000..298896d Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-code-512 (2).png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-code-512.png b/I18N Commander/UI WinForms/Resources/icons8-code-512.png new file mode 100644 index 0000000..00d10eb Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-code-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-folder-tree-512.png b/I18N Commander/UI WinForms/Resources/icons8-folder-tree-512.png new file mode 100644 index 0000000..60db4b8 Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-folder-tree-512.png differ