"me-web?" 0.7.0: the name in quotes, copy buttons, and every translation rewritten in plain words
Build / Desktop, server and tools (push) Successful in 1m19s
Release / Every platform, the server, the release (push) Successful in 9m34s

- The name is now always written in quotation marks, each language with its own: “me-web?”, «me-web?», „me-web?“, « me-web? », 「me-web?」. Without them the question mark read as the end of a sentence. In Arabic, Hebrew and the other right-to-left languages the question mark no longer jumps to the wrong side of the name.
- Your addresses, your router, your network, the reverse name, the route and the code of a submission you sent now sit in a small box with a copy button, ready to paste into a message to your provider or anywhere else.
- Every translation except English and Italian was rewritten for people who are not technical: everyday words first, the technical term in brackets, and one word for one thing across the whole app. They are still machine drafts; "Suggest a correction" is welcome in any language.
- A few lines are plainer in English and Italian too: the test servers for calls, and the server in the middle that carries a call when two homes can't connect directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-15 22:49:40 +02:00
1 parent d18caed157
commit 320936c688
260 files changed
+23662 -18072

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ pass=$(printf '%s\n' "$cred" | sed -n 's/^password=//p')
out=$(mktemp)
trap 'rm -f "$out"' EXIT
body=$(printf '{"name":"%s","private":false,"default_branch":"master","description":"me-web? Can you run your own server from here? Checks your connection and device and explains what stands in the way."}' "$NAME")
body=$(printf '{"name":"%s","private":false,"default_branch":"master","description":"“me-web?”: can you run your own server from here? Checks your connection and device and explains what stands in the way."}' "$NAME")
code=$(curl -s -o "$out" -w '%{http_code}' -u "$user:$pass" -H 'Content-Type: application/json' \
-X POST "$GITEA/api/v1/orgs/$ORG/repos" -d "$body")
case "$code" in
+44
View File
@@ -32,6 +32,32 @@ var supplied = new Dictionary<string, (string Name, string English, bool Rtl)>
["wni"] = ("Shindzuani", "Ndzwani Comorian", false),
["zdj"] = ("Shingazidja", "Ngazidja Comorian", false),
};
var suppliedQuotes = new Dictionary<string, string[]>
{
["ay"] = ["«", "»"],
["bi"] = ["“", "”"],
["dv"] = ["”", "“"],
["fj"] = ["“", "”"],
["gil"] = ["“", "”"],
["gn"] = ["«", "»"],
["hif"] = ["“", "”"],
["ho"] = ["“", "”"],
["ht"] = ["«\u00A0", "\u00A0»"],
["mh"] = ["“", "”"],
["ms-Arab"] = ["“", "”"],
["na"] = ["“", "”"],
["ny"] = ["“", "”"],
["pau"] = ["“", "”"],
["sm"] = ["“", "”"],
["ss"] = ["“", "”"],
["tet"] = ["“", "”"],
["tpi"] = ["“", "”"],
["tvl"] = ["“", "”"],
["wni"] = ["«\u00A0", "\u00A0»"],
["zdj"] = ["«\u00A0", "\u00A0»"],
};
var spacedQuotes = new HashSet<string> { "fr" };
var fixedQuotes = new Dictionary<string, string[]> { ["st"] = ["“", "”"] };
var root = FindRoot();
var target = Path.Combine(root, "data", "languages.json");
@@ -41,6 +67,7 @@ var baseUrl = $"https://raw.githubusercontent.com/unicode-org/cldr-json/{Cldr}/c
var territories = JsonNode.Parse(await http.GetStringAsync($"{baseUrl}/territoryInfo.json"))["supplemental"]["territoryInfo"].AsObject();
var containment = JsonNode.Parse(await http.GetStringAsync($"{baseUrl}/territoryContainment.json"))["supplemental"]["territoryContainment"];
var members = containment["UN"]["_contains"].AsArray().Select(n => (string)n).ToHashSet();
var miscUrl = $"https://raw.githubusercontent.com/unicode-org/cldr-json/{Cldr}/cldr-json/cldr-misc-full/main";
var known = CultureInfo.GetCultures(CultureTypes.NeutralCultures | CultureTypes.SpecificCultures).Select(c => c.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
var codes = new SortedSet<string>(unLanguages, StringComparer.Ordinal);
@@ -68,6 +95,7 @@ foreach (var code in codes)
{
var (name, english, rtl) = supplied.TryGetValue(code, out var given) ? given : FromCulture(code);
var status = reviewed.Contains(code) ? "reviewed" : previous.GetValueOrDefault(code) ?? "draft";
var quotes = await Quotes(code);
list.Add((JsonNode)new JsonObject
{
["code"] = code,
@@ -75,6 +103,7 @@ foreach (var code in codes)
["english"] = english,
["rtl"] = rtl,
["status"] = status,
["quotes"] = new JsonArray(quotes[0], quotes[1]),
});
}
File.WriteAllText(target, list.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) + "\n");
@@ -82,6 +111,21 @@ Console.WriteLine($"{list.Count} languages written to data/languages.json (CLDR
if (missing.Count > 0) Console.WriteLine($"official but neither in ICU nor supplied, left out: {string.Join(", ", missing)}");
return 0;
async Task<string[]> Quotes(string code)
{
if (suppliedQuotes.TryGetValue(code, out var given) || fixedQuotes.TryGetValue(code, out given)) return given;
foreach (var locale in new[] { code, code.Split('-')[0] }.Distinct())
{
using var response = await http.GetAsync($"{miscUrl}/{locale}/delimiters.json");
if (!response.IsSuccessStatusCode) continue;
var delimiters = JsonNode.Parse(await response.Content.ReadAsStringAsync())["main"].AsObject().First().Value["delimiters"];
var open = (string)delimiters["quotationStart"];
var close = (string)delimiters["quotationEnd"];
return spacedQuotes.Contains(code) ? [open + "\u00A0", "\u00A0" + close] : [open, close];
}
throw new InvalidOperationException($"{code}: no quotation marks in CLDR and none supplied");
}
(string Name, string English, bool Rtl) FromCulture(string code)
{
var culture = CultureInfo.GetCultureInfo(code);
+8 -8
View File
@@ -36,7 +36,7 @@ var android = Path.Combine(root, "src", "MeWeb.Android", "MeWeb.Android.csproj")
var server = Path.Combine(root, "src", "MeWeb.Server", "MeWeb.Server.csproj");
var branding = Path.Combine(root, "branding");
Directory.CreateDirectory(output);
Console.WriteLine($"me-web? {version} ({commit}): {string.Join(", ", targets)} -> {output}");
Console.WriteLine($"“me-web?” {version} ({commit}): {string.Join(", ", targets)} -> {output}");
if (targets.Count == 0)
{
WriteNotes();
@@ -81,7 +81,7 @@ void WriteNotes()
var lines = Changes();
lines.AddRange(
[
$"me-web? {version}, commit {commit}, built {builtAt}.",
$"“me-web?” {version}, commit {commit}, built {builtAt}.",
"",
"Built with these open network datasets:",
"",
@@ -100,7 +100,7 @@ List<string> Changes()
foreach (var entry in log.Split('\x1e', StringSplitOptions.RemoveEmptyEntries))
{
var parts = entry.Split('\x1f');
var subject = Regex.Replace(parts[0].Trim(), @"^me-web\? [\d.]+:\s*", "");
var subject = Regex.Replace(parts[0].Trim(), @"^[""“]?me-web\?[""”]? [\d.]+:\s*", "");
if (subject.Length == 0) continue;
var body = (parts.Length > 1 ? parts[1] : "").Split('\n').Select(l => l.TrimEnd())
.Where(l => !Regex.IsMatch(l, @"^[A-Za-z-]+-(By|Session|Off-By):", RegexOptions.IgnoreCase))
@@ -127,7 +127,7 @@ string Linux()
File.WriteAllText(Path.Combine(stage, "software.arasaka.meweb.desktop"), """
[Desktop Entry]
Type=Application
Name=me-web?
Name=“me-web?”
Comment=Can you run your own server from here?
Comment[it]=Si può ospitare un server da questa connessione?
Exec=meweb
@@ -162,7 +162,7 @@ string Linux()
homepage: https://git.thepra.dev/Arasaka/MeWeb
license: AGPL-3.0-only
description: |
Can you run your own server from here? me-web? checks your connection and device and
Can you run your own server from here? “me-web?” checks your connection and device and
explains, in plain words, what you could host at home and what stands in the way.
contents:
- src: {installed}
@@ -269,8 +269,8 @@ string MacOs()
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>me-web?</string>
<key>CFBundleDisplayName</key><string>me-web?</string>
<key>CFBundleName</key><string>“me-web?”</string>
<key>CFBundleDisplayName</key><string>“me-web?”</string>
<key>CFBundleIdentifier</key><string>software.arasaka.meweb</string>
<key>CFBundleVersion</key><string>{version}</string>
<key>CFBundleShortVersionString</key><string>{version}</string>
@@ -279,7 +279,7 @@ string MacOs()
<key>CFBundlePackageType</key><string>APPL</string>
<key>LSMinimumSystemVersion</key><string>13.0</string>
<key>NSHighResolutionCapable</key><true/>
<key>NSLocalNetworkUsageDescription</key><string>me-web? looks for your router to see whether it can open a port for a test.</string>
<key>NSLocalNetworkUsageDescription</key><string>“me-web?” looks for your router to see whether it can open a port for a test.</string>
</dict>
</plist>
+38 -2
View File
@@ -7,13 +7,18 @@ 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]);
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> | merge <keys.json> | import <code> <strings.json> | drop <Key>");
if (args.Length == 1 && args[0] == "quote") return Quote();
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> | merge <keys.json> | import <code> <strings.json> | drop <Key> | 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;
@@ -24,8 +29,15 @@ int Check()
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 (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)
@@ -105,6 +117,30 @@ int Drop(string key)
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<string, string>(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<string, string> Read(string code)
{
var path = Path.Combine(folder, $"{code}.json");