Every official language, in JSON: 113 listed, five shipped so far
Build / Desktop, server and tools (push) Successful in 33s
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:
1 parent
7589121846
commit
8eaf9f36fc
17 files changed
+1072
-99
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
Reference in new issue
Block a user