using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; var root = FindRoot(); var folder = Path.Combine(root, "src", "MeWeb.App", "Strings"); var options = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; var placeholder = new Regex(@"\{\d+\}"); const string Name = "me-web?"; var quotes = JsonNode.Parse(File.ReadAllText(Path.Combine(root, "data", "languages.json"))).AsArray() .ToDictionary(n => (string)n["code"], n => ((string)n["quotes"][0], (string)n["quotes"][1])); var named = new Regex(@"(?:«[\u00A0\u202F ]|[""'“”„‟«»‹›「」『』‘’‚])?me-web\?(?:[\u00A0\u202F ]»|[""'“”„‟«»‹›「」『』‘’‚])?"); if (args.Length >= 1 && args[0] == "check") return Check(); if (args.Length == 3 && args[0] == "set") return Set(args[1], args[2]); if (args.Length == 2 && args[0] == "merge") return Merge(args[1]); if (args.Length == 3 && args[0] == "import") return Import(args[1], args[2]); if (args.Length == 2 && args[0] == "drop") return Drop(args[1]); if (args.Length == 1 && args[0] == "quote") return Quote(); Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set | merge | import | drop | quote"); Console.Error.WriteLine(" values.json maps a language code to its text; keys.json maps keys to such maps; strings.json maps keys to one language's text"); return 2; int Check() { var english = Read("en"); var problems = 0; foreach (var file in Directory.GetFiles(folder, "*.json").OrderBy(f => f, StringComparer.Ordinal)) { var code = Path.GetFileNameWithoutExtension(file); var values = Read(code); var (open, close) = quotes[code]; var bare = values.Where(p => Bare(p.Value, open, close)).Select(p => p.Key).ToList(); if (bare.Count > 0) { problems++; Console.WriteLine($"{code}: {Name} not between {open.Trim()} and {close.Trim()} in [{string.Join(", ", bare)}]"); } if (code == "en") continue; var missing = english.Keys.Where(k => !values.ContainsKey(k)).ToList(); var extra = values.Keys.Where(k => !english.ContainsKey(k)).ToList(); var mismatched = english.Keys.Where(values.ContainsKey) .Where(k => !placeholder.Matches(english[k]).Select(m => m.Value).Order().SequenceEqual(placeholder.Matches(values[k]).Select(m => m.Value).Order())).ToList(); if (missing.Count + extra.Count + mismatched.Count == 0) continue; problems++; Console.WriteLine($"{code}: missing [{string.Join(", ", missing)}] extra [{string.Join(", ", extra)}] placeholders [{string.Join(", ", mismatched)}]"); } Console.WriteLine(problems == 0 ? $"strings: OK ({english.Count} keys)" : $"strings: {problems} language(s) to fix"); return problems == 0 ? 0 : 1; } int Set(string key, string valuesPath) { var values = JsonNode.Parse(File.ReadAllText(valuesPath)).AsObject(); if (values["en"] is null) throw new InvalidOperationException("values.json must carry at least \"en\""); foreach (var (code, value) in values) { var strings = Read(code); strings[key] = (string)value; Write(code, strings); } Console.WriteLine($"{key}: set in {values.Count} language(s)"); return 0; } int Merge(string path) { var keys = JsonNode.Parse(File.ReadAllText(path)).AsObject(); var byLanguage = new Dictionary>(); foreach (var (key, values) in keys) { if (values?["en"] is null) throw new InvalidOperationException($"{key} carries no \"en\""); foreach (var (code, value) in values.AsObject()) { if (!byLanguage.TryGetValue(code, out var strings)) byLanguage[code] = strings = Read(code); strings[key] = (string)value; } } foreach (var (code, strings) in byLanguage) Write(code, strings); Console.WriteLine($"merge: {keys.Count} key(s) into {byLanguage.Count} language(s)"); return 0; } int Import(string code, string path) { var english = Read("en"); var incoming = JsonNode.Parse(File.ReadAllText(path)).AsObject(); var strings = Read(code); var unknown = new List(); foreach (var (key, value) in incoming) { if (!english.ContainsKey(key)) { unknown.Add(key); continue; } strings[key] = (string)value; } Write(code, strings); Console.WriteLine($"import {code}: {incoming.Count - unknown.Count} key(s){(unknown.Count > 0 ? $", ignored unknown [{string.Join(", ", unknown)}]" : "")}"); return unknown.Count == 0 ? 0 : 1; } int Drop(string key) { var count = 0; foreach (var file in Directory.GetFiles(folder, "*.json")) { var code = Path.GetFileNameWithoutExtension(file); var strings = Read(code); if (!strings.Remove(key)) continue; Write(code, strings); count++; } Console.WriteLine($"{key}: dropped from {count} language(s)"); return 0; } int Quote() { var changed = 0; foreach (var file in Directory.GetFiles(folder, "*.json").OrderBy(f => f, StringComparer.Ordinal)) { var code = Path.GetFileNameWithoutExtension(file); var (open, close) = quotes[code]; var strings = Read(code); var quoted = new SortedDictionary(strings.ToDictionary(p => p.Key, p => named.Replace(p.Value, open + Name + close)), StringComparer.Ordinal); if (quoted.SequenceEqual(strings)) continue; Write(code, quoted); changed++; } Console.WriteLine($"quote: {changed} language(s) changed"); return 0; } static bool Bare(string text, string open, string close) { for (var at = text.IndexOf(Name, StringComparison.Ordinal); at >= 0; at = text.IndexOf(Name, at + Name.Length, StringComparison.Ordinal)) if (!text[..at].EndsWith(open, StringComparison.Ordinal) || !text[(at + Name.Length)..].StartsWith(close, StringComparison.Ordinal)) return true; return false; } SortedDictionary Read(string code) { var path = Path.Combine(folder, $"{code}.json"); if (!File.Exists(path)) return new SortedDictionary(StringComparer.Ordinal); var node = JsonNode.Parse(File.ReadAllText(path)).AsObject(); return new SortedDictionary(node.ToDictionary(p => p.Key, p => (string)p.Value), StringComparer.Ordinal); } void Write(string code, SortedDictionary strings) { var node = new JsonObject(); foreach (var (key, value) in strings) node[key] = value; File.WriteAllText(Path.Combine(folder, $"{code}.json"), node.ToJsonString(options) + "\n"); } static string FindRoot() { var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "MeWeb.slnx"))) dir = dir.Parent; return dir?.FullName ?? throw new InvalidOperationException("run from inside the repository (MeWeb.slnx not found)"); }