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