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
@@ -6,6 +6,7 @@ namespace MeWeb.App.Services;
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, WriteIndented = true)]
[JsonSerializable(typeof(List<Language>))]
[JsonSerializable(typeof(StoredSettings))]
[JsonSerializable(typeof(Dictionary<string, string>))]
public partial class AppJsonContext : JsonSerializerContext
{
}
+5 -2
View File
@@ -7,6 +7,8 @@ public class Language
{
public string Code { get; set; }
public string Name { get; set; }
public string English { get; set; }
public bool Rtl { get; set; }
public string Status { get; set; }
public bool IsDraft => Status == "draft";
@@ -14,7 +16,7 @@ public class Language
public static class Languages
{
public static Language Fallback { get; } = new() { Code = "en", Name = "English", Status = "reviewed" };
public static Language Fallback { get; } = new() { Code = "en", Name = "English", English = "English", Status = "reviewed" };
public static IReadOnlyList<Language> All { get; } = Load();
@@ -36,6 +38,7 @@ public static class Languages
using var stream = typeof(Languages).Assembly.GetManifestResourceStream("languages.json");
if (stream is null) return [Fallback];
var list = JsonSerializer.Deserialize(stream, AppJsonContext.Default.ListLanguage) ?? [];
return list.Count == 0 ? [Fallback] : list.OrderBy(l => l.Name, StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreCase)).ToList();
var shipped = list.Where(l => Localizer.Has(l.Code)).ToList();
return shipped.Count == 0 ? [Fallback] : shipped.OrderBy(l => l.Name, StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreCase)).ToList();
}
}
+32 -5
View File
@@ -1,6 +1,6 @@
using System.ComponentModel;
using System.Globalization;
using System.Resources;
using System.Text.Json;
using Avalonia.Data;
using Avalonia.Markup.Xaml;
@@ -10,7 +10,10 @@ namespace MeWeb.App.Services;
public sealed class Localizer : INotifyPropertyChanged
{
readonly ResourceManager resources = new("MeWeb.App.Strings.Strings", typeof(Localizer).Assembly);
readonly Dictionary<string, string> english = Load(Languages.Fallback.Code);
Dictionary<string, string> current;
public Localizer() => current = english;
public static Localizer Instance { get; } = new();
@@ -20,21 +23,45 @@ public sealed class Localizer : INotifyPropertyChanged
public CultureInfo Culture { get; private set; } = CultureInfo.GetCultureInfo(Languages.Fallback.Code);
public bool IsRightToLeft => Culture.TextInfo.IsRightToLeft;
public bool IsRightToLeft => Language.Rtl;
public string this[string key] => resources.GetString(key, Culture) ?? key;
public string this[string key] => current.GetValueOrDefault(key) ?? english.GetValueOrDefault(key) ?? key;
public string Format(string key, params object[] args) => string.Format(Culture, this[key], args);
public void SetLanguage(Language language)
{
Language = language ?? Languages.Fallback;
Culture = CultureInfo.GetCultureInfo(Language.Code);
current = Language.Code == Languages.Fallback.Code ? english : Load(Language.Code);
Culture = CultureFor(Language.Code);
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = Culture;
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = Culture;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
}
public static bool Has(string code) => typeof(Localizer).Assembly.GetManifestResourceInfo(ResourceName(code)) is not null;
static string ResourceName(string code) => $"strings.{code}.json";
static Dictionary<string, string> Load(string code)
{
using var stream = typeof(Localizer).Assembly.GetManifestResourceStream(ResourceName(code));
if (stream is null) return [];
return JsonSerializer.Deserialize(stream, AppJsonContext.Default.DictionaryStringString) ?? [];
}
static CultureInfo CultureFor(string code)
{
try
{
return CultureInfo.GetCultureInfo(code);
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
}
public sealed class TrExtension : MarkupExtension