From 052b833dd63fdb5db6f076629d2d151d66374325 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Sun, 22 Feb 2026 23:43:57 +0000 Subject: [PATCH] Replaced IFailedLoopback with IOUtcome Added a translation service that will update the base language conversions for each supported language. --- .../Enums/ApprovalVerdict.cs | 11 + e-suite.Workflow.Core/Interfaces/IOutcome.cs | 12 ++ eSuite.Translator/Program.cs | 197 ++++++++++++++++++ eSuite.Translator/TranslationService.cs | 41 ++++ eSuite.Translator/eSuite.Translator.csproj | 10 + 5 files changed, 271 insertions(+) create mode 100644 e-suite.Workflow.Core/Enums/ApprovalVerdict.cs create mode 100644 e-suite.Workflow.Core/Interfaces/IOutcome.cs create mode 100644 eSuite.Translator/Program.cs create mode 100644 eSuite.Translator/TranslationService.cs create mode 100644 eSuite.Translator/eSuite.Translator.csproj diff --git a/e-suite.Workflow.Core/Enums/ApprovalVerdict.cs b/e-suite.Workflow.Core/Enums/ApprovalVerdict.cs new file mode 100644 index 0000000..8af3511 --- /dev/null +++ b/e-suite.Workflow.Core/Enums/ApprovalVerdict.cs @@ -0,0 +1,11 @@ +namespace e_suite.Workflow.Core.Enums; + +public enum ApprovalVerdict +{ + None, + Pending, + Approved, + ApprovedWithComments, + Rejected, + Reviewed +} \ No newline at end of file diff --git a/e-suite.Workflow.Core/Interfaces/IOutcome.cs b/e-suite.Workflow.Core/Interfaces/IOutcome.cs new file mode 100644 index 0000000..eb6c9d5 --- /dev/null +++ b/e-suite.Workflow.Core/Interfaces/IOutcome.cs @@ -0,0 +1,12 @@ +using e_suite.Workflow.Core.Attributes; + +namespace e_suite.Workflow.Core.Interfaces; + +[TaskCapability] +public interface IOutcome +{ + //Todo runtime only property. + //public T? TaskOutcome { get; set; } + + Dictionary OutcomeActions { get; set; } +} \ No newline at end of file diff --git a/eSuite.Translator/Program.cs b/eSuite.Translator/Program.cs new file mode 100644 index 0000000..e7a0242 --- /dev/null +++ b/eSuite.Translator/Program.cs @@ -0,0 +1,197 @@ +// See https://aka.ms/new-console-template for more information +using eSuite.Translator; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; + +var http = new HttpClient +{ + Timeout = TimeSpan.FromSeconds(600), + BaseAddress = new Uri("http://aipi.cluster.local:11434") +}; +var translator = new OllamaTranslationService(http); + +LanguagePack LoadDictionary(string rootpath, string foldername) +{ + var path = Path.Combine(rootpath, foldername); + //var files = Directory.GetFiles(path, "*.json", SearchOption.AllDirectories); + var files = Directory + .EnumerateFiles(path) + .Where(f => f.EndsWith(".json", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".lang", StringComparison.OrdinalIgnoreCase)); + + + var languagePack = new LanguagePack(); + + //load the master files into memory, + foreach (var file in files) + { + if (Path.GetExtension(file).ToLower() == ".lang") + { + languagePack.Locale = Path.GetFileNameWithoutExtension(file); + continue; + } + + var relativePath = Path.GetRelativePath(path, file); + var json = File.ReadAllText(file); + var jsonDocument = JsonNode.Parse(json); + + languagePack.Files[Path.GetFileName(file)] = jsonDocument; + } + + return languagePack; +} + +void SaveDictionary(string rootpath, string foldername, Dictionary dictionary) +{ + var folderPath = Path.Combine(rootpath, foldername); + foreach (var item in dictionary) + { + var filename = item.Key; + var path = Path.Combine(folderPath, filename); + var sorted = SortJson(item.Value); + File.WriteAllText(path, sorted.ToJsonString(new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + })); + } + Console.WriteLine($"{folderPath} saved"); +} + +async Task ReconcileNode(JsonNode masterNode, JsonNode languageNode, string targetLanguage) +{ + // If the master node is a value, nothing to recurse into + if (masterNode is JsonValue) + return; + + // If the master node is an object, ensure the language node is also an object + if (masterNode is JsonObject masterObj) + { + var langObj = languageNode as JsonObject + ?? throw new InvalidOperationException("Language node must be an object here"); + + foreach (var kvp in masterObj) + { + var key = kvp.Key; + var masterChild = kvp.Value!; + + // If the language is missing this key, create a placeholder + if (!langObj.TryGetPropertyValue(key, out var langChild)) + { + // If master is a value → create empty string + if (masterChild is JsonValue) + { + var sourceText = masterChild.ToString(); + var translated = await translator.TranslateAsync(sourceText, targetLanguage); + if (!translated.Equals(sourceText, StringComparison.InvariantCultureIgnoreCase)) + langChild = JsonValue.Create(translated); + else + langChild = null; + } + else + { + // If master is an object → create empty object + langChild = new JsonObject(); + } + + if (langChild != null) + langObj[key] = langChild; + } + + // Recurse into children + if (masterChild is JsonObject) + await ReconcileNode(masterChild, langChild!, targetLanguage); + } + } +} + +async Task ReconcileDictionary(Dictionary languageDictionary, Dictionary masterDictionary, string targetLanguage) +{ + foreach (var kvp in masterDictionary) + { + var filename = kvp.Key; + var masterJson = kvp.Value; + + // Does the language folder have this file? + if (!languageDictionary.TryGetValue(filename, out var langJson)) + { + langJson = new JsonObject(); + languageDictionary[filename] = langJson; + + } + + // Reconcile the tree structure + await ReconcileNode(masterJson, langJson, targetLanguage); + } +} + +static JsonNode SortJson(JsonNode node) +{ + switch (node) + { + case JsonObject obj: + { + var sorted = new JsonObject(); + + foreach (var kvp in obj.OrderBy(k => k.Key, StringComparer.Ordinal)) + { + // Clone before sorting to avoid parent conflicts + var cloned = kvp.Value?.DeepClone(); + sorted[kvp.Key] = SortJson(cloned!); + } + + return sorted; + } + + case JsonArray arr: + { + var newArr = new JsonArray(); + foreach (var item in arr) + { + var cloned = item?.DeepClone(); + newArr.Add(SortJson(cloned!)); + } + return newArr; + } + + default: + return node.DeepClone(); // primitives + } +} + + +Console.WriteLine("Starting translation reconciliation"); + +const string rootpath = @"C:\Code\Gitea\e-suite\e-suite.webui\public\locales"; + +var masterDictionary = LoadDictionary(rootpath, "en"); +SaveDictionary(rootpath, "en", masterDictionary.Files); + +Console.WriteLine("Master files loaded"); + +var rootDir = new DirectoryInfo(rootpath); +var folders = rootDir.GetDirectories().Select(x => x.Name ).ToList(); +folders.Remove("en"); //Ignore the master folder + +var languageFolders = folders.Where(x => !x.Contains("-")).ToList(); +var localeFolders = folders.Where(x => x.Contains("-")).ToList(); + +foreach ( var language in languageFolders) +{ + var languageDictionary = LoadDictionary(rootpath, language); + + await ReconcileDictionary(languageDictionary.Files, masterDictionary.Files, languageDictionary.Locale); + SaveDictionary(rootpath, language, languageDictionary.Files); +} + + +Console.WriteLine("Main Language files completed."); + + + +public class LanguagePack +{ + public string Locale { get; set; } + public Dictionary Files { get; set; } = new(); +} \ No newline at end of file diff --git a/eSuite.Translator/TranslationService.cs b/eSuite.Translator/TranslationService.cs new file mode 100644 index 0000000..a1c75fe --- /dev/null +++ b/eSuite.Translator/TranslationService.cs @@ -0,0 +1,41 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace eSuite.Translator; + +public interface ITranslationService +{ + Task TranslateAsync(string sourceText, string targetLanguage); +} + +public class OllamaTranslationService : ITranslationService +{ + private readonly HttpClient _http; + + public OllamaTranslationService(HttpClient http) + { + _http = http; + } + + public async Task TranslateAsync(string sourceText, string targetLanguage) + { + Console.Write( $"Translating \"{sourceText}\" into \"{targetLanguage}\": "); + var request = new + { + model = "ZimaBlueAI/HY-MT1.5-1.8:7b", + prompt = $"Translate this text into {targetLanguage}: {sourceText}", + stream = false + }; + + var response = await _http.PostAsJsonAsync("/api/generate", request); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadFromJsonAsync(); + + // Ollama returns streaming chunks; for simplicity assume "response" contains the text + var result = (json?["response"]?.ToString() ?? "").Trim(); + Console.WriteLine(result); + + return result; + } +} \ No newline at end of file diff --git a/eSuite.Translator/eSuite.Translator.csproj b/eSuite.Translator/eSuite.Translator.csproj new file mode 100644 index 0000000..d20809e --- /dev/null +++ b/eSuite.Translator/eSuite.Translator.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + +