From 4918807c60bfdb26a7b6b521fc9c6e9c35b3d638 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 13 Feb 2023 20:47:22 +0100 Subject: [PATCH 1/2] Added Godot generator --- I18N Commander/I18N Commander.sln.DotSettings | 1 + I18N Commander/Processor/ExtensionsData.cs | 10 ++ .../Processor/Generators/Generator.cs | 10 +- .../Processor/Generators/GodotCSV.cs | 151 ++++++++++++++++++ 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 I18N Commander/Processor/ExtensionsData.cs create mode 100644 I18N Commander/Processor/Generators/GodotCSV.cs diff --git a/I18N Commander/I18N Commander.sln.DotSettings b/I18N Commander/I18N Commander.sln.DotSettings index 886d922..83c7f5a 100644 --- a/I18N Commander/I18N Commander.sln.DotSettings +++ b/I18N Commander/I18N Commander.sln.DotSettings @@ -1,4 +1,5 @@  + CSV EF JSON True \ No newline at end of file diff --git a/I18N Commander/Processor/ExtensionsData.cs b/I18N Commander/Processor/ExtensionsData.cs new file mode 100644 index 0000000..d9f2cb8 --- /dev/null +++ b/I18N Commander/Processor/ExtensionsData.cs @@ -0,0 +1,10 @@ +namespace Processor; + +public static class ExtensionsData +{ + public static IEnumerable ReverseIt(this IList items) + { + for (var i = items.Count - 1; 0 <= i; i--) + yield return items[i]; + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/Generators/Generator.cs b/I18N Commander/Processor/Generators/Generator.cs index 29ed588..56c2adb 100644 --- a/I18N Commander/Processor/Generators/Generator.cs +++ b/I18N Commander/Processor/Generators/Generator.cs @@ -7,18 +7,16 @@ public static class Generator public static IGenerator Get(Type genType) => genType switch { - Type.DOTNET => GENERATORS.ContainsKey(genType) ? GENERATORS[genType] : GENERATORS[genType] = new DotnetBigFile(), + Type.DOTNET => GENERATORS.TryGetValue(genType, out var value) ? value : GENERATORS[genType] = new DotnetBigFile(), + Type.GODOT => GENERATORS.TryGetValue(genType, out var value) ? value : GENERATORS[genType] = new GodotCSV(), _ => VOID_GENERATOR, }; public static async Task> TriggerAllAsync() { - var dotnetEnabled = await AppSettings.GetGeneratorDotnetEnabled(); - var godotEnabled = await AppSettings.GetGeneratorGodotEnabled(); long bytesWritten = 0; - - if (dotnetEnabled) + if (await AppSettings.GetGeneratorDotnetEnabled()) { var result = await Generator.Get(Type.DOTNET).GenerateAsync(); if(!result.Successful) @@ -27,7 +25,7 @@ public static class Generator bytesWritten += result.Result; } - if(godotEnabled) + if(await AppSettings.GetGeneratorGodotEnabled()) { var result = await Generator.Get(Type.GODOT).GenerateAsync(); if(!result.Successful) diff --git a/I18N Commander/Processor/Generators/GodotCSV.cs b/I18N Commander/Processor/Generators/GodotCSV.cs new file mode 100644 index 0000000..8ed4129 --- /dev/null +++ b/I18N Commander/Processor/Generators/GodotCSV.cs @@ -0,0 +1,151 @@ +using System.Text; + +namespace Processor.Generators; + +public class GodotCSV : IGenerator +{ + private static readonly List CULTURE_CODES = new(); + + #region Implementation of IGenerator + + /// + public async Task> GenerateAsync() + { + const string filename = "I18N.csv"; + const char delimiter = ','; + + var destPath = await AppSettings.GetGeneratorGodotDestinationPath(); + 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.OrderBy(n => n.Code)) + CULTURE_CODES.Add(code); + + if (CULTURE_CODES.Count == 0) + return new ProcessorResult(0, true, string.Empty); + + try + { + await using var fileStream = new FileStream(pathTemp, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using var writer = new StreamWriter(fileStream, Encoding.UTF8); + + // Write the header: + await writer.WriteLineAsync($"keys{delimiter}{string.Join(delimiter, CULTURE_CODES)}"); + + // + // Iterate through all sections and text elements: + // + var sectionDepth = await SectionProcessor.GetDepth(); + foreach (var sectionLayer in Enumerable.Range(0, sectionDepth + 1)) + { + var sections = await SectionProcessor.LoadLayer(sectionLayer); + foreach (var section in sections.OrderBy(n => n.DataKey)) + foreach (var textElement in section.TextElements.OrderBy(n => n.Code)) + { + // + // Build the path to the text element: + // + var textElementPath = new List(section.Depth + 2) + { + textElement.Code + }; + + var parent = section; + while (parent is not null) + { + textElementPath.Add(parent.DataKey); + parent = parent.Parent; + } + + // Write the path to the text element as key: + await writer.WriteAsync($"{string.Join(".", textElementPath.ReverseIt())}{delimiter}"); + + // Load all translations: + var translations = textElement.Translations.DistinctBy(n => n.Culture).ToDictionary(translation => translation.Culture, translation => translation.Text); + + // Iterate through all text elements and write the translations out: + for (var cultureIndex = 0; cultureIndex < CULTURE_CODES.Count; cultureIndex++) + { + var cultureCode = CULTURE_CODES[cultureIndex]; + var cultureText = translations.TryGetValue(cultureCode, out var text) ? text : string.Empty; + + // Write the text with delimiter or only the text in case of the last column: + if (cultureIndex < CULTURE_CODES.Count - 1) + await writer.WriteAsync($@"""{Escape4CSV(cultureText)}""{delimiter}"); + else + await writer.WriteLineAsync($@"""{Escape4CSV(cultureText)}"""); + } + } + } + } + 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 static string Escape4CSV(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return string.Empty; + + var sb = new StringBuilder(text.Length); + foreach (var c in text) + { + if (c == '"') + sb.Append(@""""""); // quoted double quote, i.e. prints "" + else + sb.Append(c); + } + + return sb.ToString(); + } + + #endregion +} \ No newline at end of file From 8904712c2e556680d0a275d558e54dd7eaf14d3a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 13 Feb 2023 20:47:52 +0100 Subject: [PATCH 2/2] Improved handling of missing translations --- I18N Commander/Processor/TranslationProcessor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/I18N Commander/Processor/TranslationProcessor.cs b/I18N Commander/Processor/TranslationProcessor.cs index 9de7cb8..41593f3 100644 --- a/I18N Commander/Processor/TranslationProcessor.cs +++ b/I18N Commander/Processor/TranslationProcessor.cs @@ -20,12 +20,14 @@ public static class TranslationProcessor TranslateManual = false, Culture = cultureInfo.Code, Text = string.Empty, + TextElement = textElement, }); } if(missedTranslations.Count > 0) { textElement.Translations.AddRange(missedTranslations); + db.Translations.AddRange(missedTranslations); db.TextElements.Update(textElement); await db.SaveChangesAsync(); }