// Project Makoto // Copyright (C) 2023 Fortunevale // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY global using Xorog.UniversalExtensions; global using Xorog.UniversalExtensions.Entities; global using Newtonsoft.Json; global using Serilog; using System.Reflection; using Newtonsoft.Json.Linq; using System.Text.RegularExpressions; namespace TranslationSourceGenerator; internal class Program { public string MakotoSourceOrigin => $@" // Project Makoto // Copyright (C) 2023 Fortunevale // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY namespace {this.Namespace}; #pragma warning disable CS8981 #pragma warning disable CS8618 #pragma warning disable IDE1006 public class Translations : ITranslations {{ public Dictionary Progress = new(); public CommandTranslation[] CommandList {{ get; set; }} #region AutoGenerated // InsertPoint #endregion AutoGenerated }} "; public string StringsJson { get; set; } public string TranslationCs { get; set; } public string Namespace { get; set; } static void Main(string[] args) => new Program().Execute(args).GetAwaiter().GetResult(); public async Task Execute(string[] args) { if (!Directory.Exists($"{new FileInfo(Assembly.GetExecutingAssembly().FullName).Directory.FullName}/logs")) _ = Directory.CreateDirectory($"{new FileInfo(Assembly.GetExecutingAssembly().FullName).Directory.FullName}/logs"); Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); _ = Task.Run(async () => { if (!File.Exists(args.Length > 0 ? args[0] : "e252zru2rjb2rtb23jbrj2bthj2bthjb2jtb4jbtb2jtb4hj")) { Log.Error("Translation json expected as arg0"); return; } if (!File.Exists(args.Length > 1 ? args[1] : "e252zru2rjb2rtb23jbrj2bthj2bthjb2jtb4jbtb2jtb4hj")) { Log.Error("Translation json expected as arg1"); return; } if (args.Length < 2) { Log.Error("Namespace expected as arg2"); return; } this.StringsJson = args[0]; this.TranslationCs = args[1]; this.Namespace = args[2]; Log.Debug("Project Makoto Translation Source Generator started up."); Log.Debug("Strings.json Location: {0}", Path.GetFullPath(this.StringsJson)); Log.Debug("Translation.cs Location: {0}", Path.GetFullPath(this.TranslationCs)); DateTime lastModify = new(); while (true) { try { Dictionary> addedFields = new(); FileInfo fileInfo = new(this.StringsJson); if (lastModify != fileInfo.LastWriteTimeUtc) { try { List Warnings = new(); var Insert = ""; Log.Debug("Translation file updated. Updating Translations.cs.."); var jsonFile = (JObject)JsonConvert.DeserializeObject(File.ReadAllText(this.StringsJson)); string CreateValidValueName(string str) => Regex.Replace(str, @"[^a-zA-Z0-9]", "_"); void RecursiveHandle(JObject token, string ParentPath, int depth) { foreach (var item in token) { var className = CreateValidValueName($"{item.Key.First().ToString().ToLower()}{item.Key.Remove(0, 1)}"); switch (className) { case "object": className = $"@{className}"; break; default: break; } var fieldName = CreateValidValueName(item.Key.FirstLetterToUpper()); var entryPoint = $"{ParentPath}/{className}"; var IndexPath = $"// {ParentPath} InsertPoint"; var InsertPosition = 0; if (!ParentPath.IsNullOrWhiteSpace()) { InsertPosition = Insert.IndexOf(IndexPath) + IndexPath.Length; } if (!addedFields.ContainsKey(IndexPath)) addedFields.Add(IndexPath, new()); switch (item.Value.Type) { case JTokenType.Object: { var containsLocaleCode = false; var localeCodeIsArray = false; List> tokens = new(); foreach (var subItem in item.Value.ToObject()) { tokens.Add(subItem); if (subItem.Key == "en") { containsLocaleCode = true; localeCodeIsArray = subItem.Value.Type == JTokenType.Array; } } if (containsLocaleCode) { if (!localeCodeIsArray) { var line = $"{new string(' ', depth * 4)}public SingleTranslationKey {CreateValidValueName(item.Key)};"; if (addedFields[IndexPath].Contains(line)) continue; Log.Debug("Found SingleKey '{0}'", item.Key); Insert = Insert.Insert(InsertPosition, $"\n{line}"); addedFields[IndexPath].Add(line); if (!tokens.Any(x => x.Key == "de")) Warnings.Add($"String at path {entryPoint} has no de translation"); } else { var line = $"{new string(' ', depth * 4)}public MultiTranslationKey {CreateValidValueName(item.Key)};"; if (addedFields[IndexPath].Contains(line)) continue; Log.Debug("Found MultiKey '{0}'", item.Key); Insert = Insert.Insert(InsertPosition, $"\n{line}"); addedFields[IndexPath].Add(line); if (!tokens.Any(x => x.Key == "de")) Warnings.Add($"String at path {entryPoint} has no de translation"); } continue; } else { Log.Debug("Found Group '{0}'", item.Key); var line = $"\n{new string(' ', depth * 4)}public {className} {fieldName};\n" + $"{new string(' ', depth * 4)}public sealed class {className}\n" + $"{new string(' ', depth * 4)}{{\n" + $"{new string(' ', depth * 4)}// {entryPoint} InsertPoint\n" + $"{new string(' ', depth * 4)}}}\n"; if (addedFields[IndexPath].Contains(line)) break; Insert = Insert.Insert(InsertPosition, line); addedFields[IndexPath].Add(line); } RecursiveHandle(item.Value.ToObject(), entryPoint, depth + 1); break; } case JTokenType.Integer: { Log.Debug("Found Int '{0}'", item.Key); var line = $"\n{new string(' ', depth * 4)}public int {CreateValidValueName(item.Key)};"; if (addedFields[IndexPath].Contains(line)) continue; Insert = Insert.Insert(InsertPosition, line); addedFields[IndexPath].Add(line); break; } case JTokenType.Array: { Log.Debug("Found Array '{0}'", item.Key); var line = $"\n{new string(' ', depth * 4)}public {className}[] {fieldName};\n" + $"{new string(' ', depth * 4)}public sealed class {className}\n" + $"{new string(' ', depth * 4)}{{\n" + $"{new string(' ', depth * 4)}// {entryPoint} InsertPoint\n" + $"{new string(' ', depth * 4)}}}\n"; if (fieldName == "CommandList" && IndexPath == "// InsertPoint") { continue; } if (!addedFields[IndexPath].Contains(line)) { Insert = Insert.Insert(InsertPosition, line); addedFields[IndexPath].Add(line); } foreach (var b in item.Value.ToObject()) RecursiveHandle(b.ToObject(), entryPoint, depth + 1); break; } default: break; } } } RecursiveHandle(jsonFile, "", 1); foreach (var b in Warnings) { Log.Warning(b); } Log.Warning("Source Generation finished with {Count} warnings.", Warnings.Count); Insert = string.Join("\n", Insert.Split("\n").Where(x => !x.Contains("InsertPoint"))); File.WriteAllText(this.TranslationCs, string.Join("\n", this.MakotoSourceOrigin.Replace("// InsertPoint", Insert).ReplaceLineEndings("\n").Split("\n", StringSplitOptions.RemoveEmptyEntries).Where(x => !x.IsNullOrWhiteSpace()))); Log.Debug("Updated Translations.cs."); } catch (Exception ex) { Log.Error(ex, "Failed to update Translations.cs."); } } lastModify = fileInfo.LastWriteTimeUtc; await Task.Delay(1000); } catch (Exception ex) { Log.Error(ex, "Failed to watch file"); await Task.Delay(10000); } } }); await Task.Delay(-1); } }