Every official language, in JSON: 113 listed, five shipped so far
Build / Desktop, server and tools (push) Successful in 33s

The language list comes from CLDR (official in a UN member state, plus the
six UN languages), with autonyms and text direction supplied for the 21
that ICU does not know. Strings move from resx to one JSON file per
language, looked up by code, so a language unknown to ICU or MSBuild still
loads; tools/strings.cs checks keys and placeholders in CI. Arabic, Hindi
and Chinese drafts prove right-to-left and system-font fallback.

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 01:18:13 +02:00
1 parent 7589121846
commit 8eaf9f36fc
17 files changed
+1072 -99

No files matched your search

+1
View File
@@ -2,6 +2,7 @@
set -e
. "$(dirname "$0")/sdk.sh"
dotnet --version
dotnet run tools/strings.cs -- check
for project in src/MeWeb.Desktop/MeWeb.Desktop.csproj src/MeWeb.Server/MeWeb.Server.csproj tools/Screenshots/Screenshots.csproj; do
echo "== $project"
dotnet build "$project" -c Release -nologo -nodeReuse:false -warnaserror
+100
View File
@@ -0,0 +1,100 @@
using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
const string Cldr = "47.0.0";
string[] unLanguages = ["ar", "zh", "en", "fr", "ru", "es"];
string[] reviewed = ["en", "it"];
string[] officialStatuses = ["official", "de_facto_official"];
string[] caselessScripts = ["ka"];
var supplied = new Dictionary<string, (string Name, string English, bool Rtl)>
{
["ay"] = ("Aymar aru", "Aymara", false),
["bi"] = ("Bislama", "Bislama", false),
["dv"] = ("ދިވެހި", "Dhivehi", true),
["fj"] = ("Na Vosa Vakaviti", "Fijian", false),
["gil"] = ("Taetae ni Kiribati", "Gilbertese", false),
["gn"] = ("Avañe'ẽ", "Guarani", false),
["hif"] = ("Fiji Baat", "Fiji Hindi", false),
["ho"] = ("Hiri Motu", "Hiri Motu", false),
["ht"] = ("Kreyòl ayisyen", "Haitian Creole", false),
["mh"] = ("Kajin M̧ajeļ", "Marshallese", false),
["ms-Arab"] = ("بهاس ملايو", "Malay (Arabic script)", true),
["na"] = ("Dorerin Naoero", "Nauruan", false),
["ny"] = ("Chichewa", "Chichewa", false),
["pau"] = ("Tekoi ra Belau", "Palauan", false),
["sm"] = ("Gagana Samoa", "Samoan", false),
["ss"] = ("SiSwati", "Swati", false),
["tet"] = ("Tetun", "Tetum", false),
["tpi"] = ("Tok Pisin", "Tok Pisin", false),
["tvl"] = ("Te Ggana Tuuvalu", "Tuvaluan", false),
["wni"] = ("Shindzuani", "Ndzwani Comorian", false),
["zdj"] = ("Shingazidja", "Ngazidja Comorian", false),
};
var root = FindRoot();
var target = Path.Combine(root, "data", "languages.json");
using var http = new HttpClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("meweb-languages/1.0");
var baseUrl = $"https://raw.githubusercontent.com/unicode-org/cldr-json/{Cldr}/cldr-json/cldr-core/supplemental";
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 known = CultureInfo.GetCultures(CultureTypes.NeutralCultures | CultureTypes.SpecificCultures).Select(c => c.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
var codes = new SortedSet<string>(unLanguages, StringComparer.Ordinal);
var missing = new SortedSet<string>(StringComparer.Ordinal);
foreach (var (territory, info) in territories)
{
if (!members.Contains(territory)) continue;
var population = info["languagePopulation"]?.AsObject();
if (population is null) continue;
foreach (var (language, details) in population)
{
var status = (string)details["_officialStatus"];
if (status is null || !officialStatuses.Contains(status)) continue;
var code = language.Replace('_', '-');
if (known.Contains(code) || supplied.ContainsKey(code)) codes.Add(code);
else missing.Add(code);
}
}
var previous = File.Exists(target)
? JsonNode.Parse(File.ReadAllText(target)).AsArray().ToDictionary(n => (string)n["code"], n => (string)n["status"])
: [];
var list = new JsonArray();
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";
list.Add((JsonNode)new JsonObject
{
["code"] = code,
["name"] = name,
["english"] = english,
["rtl"] = rtl,
["status"] = status,
});
}
File.WriteAllText(target, list.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) + "\n");
Console.WriteLine($"{list.Count} languages written to data/languages.json (CLDR {Cldr}, {members.Count} UN member states)");
if (missing.Count > 0) Console.WriteLine($"official but neither in ICU nor supplied, left out: {string.Join(", ", missing)}");
return 0;
(string Name, string English, bool Rtl) FromCulture(string code)
{
var culture = CultureInfo.GetCultureInfo(code);
var native = culture.NativeName;
var name = caselessScripts.Contains(code.Split('-')[0]) || native.Length == 0
? native
: culture.TextInfo.ToUpper(native[0]) + native[1..];
return (name, culture.EnglishName, culture.TextInfo.IsRightToLeft);
}
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)");
}
+71
View File
@@ -0,0 +1,71 @@
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]);
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> (values.json maps a language code to its 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;
}
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)");
}