Files
MeWeb/tools/strings.cs
T
thepraandClaude Opus 5.5 0dffbc27d6 The checks engine, the path and the cards: me-web? 0.1 reads the connection
MeWeb.Core runs seven checks concurrently and reports each as it settles:
online or behind a login page, this connection, the public IPv4 and IPv6
seen from v4./v6.meweb, the provider from a built-in map, the router
(UPnP IGD, NAT-PMP and PCP, read-only), address sharing (router WAN
against the echo, TTL hops only as a hint), and the history kept on the
device. The app draws the path from this device to the internet and lights
it node by node, with a card per check and a sheet listing every host it
contacts.

The provider map is iptoasn's table compressed to 2.1 MB by
tools/knowledge.cs and built before every app release; eleven Italian
provider profiles carry each fact with who said it, the source and the
month. The server gains a download page (en, it) and /download/{platform}
from the latest Gitea release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-15 04:39:03 +02:00

129 lines
5.0 KiB
C#

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+\}");
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]);
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> | merge <keys.json> | import <code> <strings.json> | drop <Key>");
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);
if (code == "en") continue;
var values = Read(code);
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<string, SortedDictionary<string, string>>();
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<string>();
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;
}
SortedDictionary<string, string> Read(string code)
{
var path = Path.Combine(folder, $"{code}.json");
if (!File.Exists(path)) return new SortedDictionary<string, string>(StringComparer.Ordinal);
var node = JsonNode.Parse(File.ReadAllText(path)).AsObject();
return new SortedDictionary<string, string>(node.ToDictionary(p => p.Key, p => (string)p.Value), StringComparer.Ordinal);
}
void Write(string code, SortedDictionary<string, string> 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)");
}