"me-web?" 0.7.0: the name in quotes, copy buttons, and every translation rewritten in plain words
- 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:
1 parent
d18caed157
commit
320936c688
260 files changed
+23662
-18072
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user