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
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();
}