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
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<EmbeddedResource Include="..\..\data\languages.json" LogicalName="languages.json" />
|
||||
<EmbeddedResource Include="Strings\*.json" LogicalName="strings.%(Filename).json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTagline" xml:space="preserve">
|
||||
<value>Scopra se da questa connessione può ospitare un server suo, e che cosa lo impedisce.</value>
|
||||
</data>
|
||||
<data name="Check" xml:space="preserve">
|
||||
<value>Verifica</value>
|
||||
</data>
|
||||
<data name="DraftNotice" xml:space="preserve">
|
||||
<value>Questa traduzione è una bozza automatica.</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Lingua</value>
|
||||
</data>
|
||||
<data name="StatusChecking" xml:space="preserve">
|
||||
<value>Verifico la connessione…</value>
|
||||
</data>
|
||||
<data name="StatusOffline" xml:space="preserve">
|
||||
<value>Non connesso a internet</value>
|
||||
</data>
|
||||
<data name="StatusOnline" xml:space="preserve">
|
||||
<value>Connesso a internet</value>
|
||||
</data>
|
||||
<data name="SuggestCorrection" xml:space="preserve">
|
||||
<value>Proponi una correzione</value>
|
||||
</data>
|
||||
<data name="VersionLine" xml:space="preserve">
|
||||
<value>me-web? {0} · software libero (AGPL-3.0)</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AppTagline" xml:space="preserve">
|
||||
<value>Find out whether you can run your own server from this connection, and what stands in the way.</value>
|
||||
</data>
|
||||
<data name="Check" xml:space="preserve">
|
||||
<value>Check</value>
|
||||
</data>
|
||||
<data name="DraftNotice" xml:space="preserve">
|
||||
<value>This translation is a machine draft.</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Language</value>
|
||||
</data>
|
||||
<data name="StatusChecking" xml:space="preserve">
|
||||
<value>Checking the connection…</value>
|
||||
</data>
|
||||
<data name="StatusOffline" xml:space="preserve">
|
||||
<value>Not connected to the internet</value>
|
||||
</data>
|
||||
<data name="StatusOnline" xml:space="preserve">
|
||||
<value>Connected to the internet</value>
|
||||
</data>
|
||||
<data name="SuggestCorrection" xml:space="preserve">
|
||||
<value>Suggest a correction</value>
|
||||
</data>
|
||||
<data name="VersionLine" xml:space="preserve">
|
||||
<value>me-web? {0} · free software (AGPL-3.0)</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"AppTagline": "اكتشف ما إذا كان بإمكانك تشغيل خادمك الخاص من هذا الاتصال، وما الذي يعيق ذلك.",
|
||||
"Check": "افحص",
|
||||
"DraftNotice": "هذه الترجمة مسودة آلية.",
|
||||
"Language": "اللغة",
|
||||
"StatusChecking": "جارٍ فحص الاتصال…",
|
||||
"StatusOffline": "غير متصل بالإنترنت",
|
||||
"StatusOnline": "متصل بالإنترنت",
|
||||
"SuggestCorrection": "اقترح تصحيحًا",
|
||||
"VersionLine": "me-web? {0} · برنامج حر (AGPL-3.0)"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"AppTagline": "Find out whether you can run your own server from this connection, and what stands in the way.",
|
||||
"Check": "Check",
|
||||
"DraftNotice": "This translation is a machine draft.",
|
||||
"Language": "Language",
|
||||
"StatusChecking": "Checking the connection…",
|
||||
"StatusOffline": "Not connected to the internet",
|
||||
"StatusOnline": "Connected to the internet",
|
||||
"SuggestCorrection": "Suggest a correction",
|
||||
"VersionLine": "me-web? {0} · free software (AGPL-3.0)"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"AppTagline": "जानें कि क्या आप इस कनेक्शन से अपना सर्वर चला सकते हैं, और रास्ते में क्या बाधा है।",
|
||||
"Check": "जाँचें",
|
||||
"DraftNotice": "यह अनुवाद एक मशीनी मसौदा है।",
|
||||
"Language": "भाषा",
|
||||
"StatusChecking": "कनेक्शन की जाँच हो रही है…",
|
||||
"StatusOffline": "इंटरनेट से कनेक्ट नहीं है",
|
||||
"StatusOnline": "इंटरनेट से कनेक्ट है",
|
||||
"SuggestCorrection": "सुधार सुझाएँ",
|
||||
"VersionLine": "me-web? {0} · मुक्त सॉफ़्टवेयर (AGPL-3.0)"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"AppTagline": "Scopra se da questa connessione può ospitare un server suo, e che cosa lo impedisce.",
|
||||
"Check": "Verifica",
|
||||
"DraftNotice": "Questa traduzione è una bozza automatica.",
|
||||
"Language": "Lingua",
|
||||
"StatusChecking": "Verifico la connessione…",
|
||||
"StatusOffline": "Non connesso a internet",
|
||||
"StatusOnline": "Connesso a internet",
|
||||
"SuggestCorrection": "Proponi una correzione",
|
||||
"VersionLine": "me-web? {0} · software libero (AGPL-3.0)"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"AppTagline": "了解您能否通过此网络连接运行自己的服务器,以及有哪些障碍。",
|
||||
"Check": "检测",
|
||||
"DraftNotice": "此翻译为机器草稿。",
|
||||
"Language": "语言",
|
||||
"StatusChecking": "正在检查连接…",
|
||||
"StatusOffline": "未连接到互联网",
|
||||
"StatusOnline": "已连接到互联网",
|
||||
"SuggestCorrection": "建议更正",
|
||||
"VersionLine": "me-web? {0} · 自由软件(AGPL-3.0)"
|
||||
}
|
||||
Reference in new issue
Block a user