XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text.Json;

namespace XFEEliteDangerous.Client;

/// <summary>UI and operator messages only. Never use translated text as a protocol identifier.</summary>
public static class AppText
{
    private static readonly string SystemLanguage = Resolve(CultureInfo.CurrentUICulture.Name);
    private static string processLanguage = SystemLanguage;
    private static readonly AsyncLocal<string?> RequestLanguage = new();
    private static readonly IReadOnlyDictionary<string, string> English = Load("en-US");
    private static readonly IReadOnlyDictionary<string, string> Chinese = Load("zh-CN");
    public static string Language => RequestLanguage.Value ?? Volatile.Read(ref processLanguage);
    public static CultureInfo DisplayCulture => CultureInfo.GetCultureInfo(Language);
    public static IReadOnlyDictionary<string, string> Catalog(string language) => Resolve(language) == "zh-CN" ? Chinese : English;

    public static string NormalizePreference(string? language) => language?.Trim().ToLowerInvariant() switch
    {
        null or "" or "system" or "auto" => "system",
        "en" or "en-us" or "en-gb" => "en-US",
        "zh" or "zh-cn" or "zh-hans" or "zh-hans-cn" => "zh-CN",
        _ => throw new ArgumentException("Supported languages: system, en-US, zh-CN.", nameof(language))
    };

    public static void Apply(string? preference)
    {
        var normalized = NormalizePreference(preference);
        var culture = CultureInfo.GetCultureInfo(normalized == "system" ? SystemLanguage : normalized);
        Volatile.Write(ref processLanguage, culture.Name);
        // Wire serializers and native parsers keep their existing numeric culture.
        CultureInfo.DefaultThreadCurrentUICulture = culture;
        CultureInfo.CurrentUICulture = culture;
    }

    public static string[] ConfigureArguments(string[] arguments, string? defaultLanguage = "system")
    {
        var remaining = new List<string>();
        var language = Environment.GetEnvironmentVariable("XFE_LANGUAGE") ?? defaultLanguage;
        var found = false;
        for (var i = 0; i < arguments.Length; i++)
        {
            var argument = arguments[i];
            if (argument != "--language" && !argument.StartsWith("--language=", StringComparison.Ordinal)) { remaining.Add(argument); continue; }
            if (found) throw new ArgumentException("Duplicate --language option.");
            found = true;
            language = argument == "--language"
                ? ++i < arguments.Length ? arguments[i] : throw new ArgumentException("Missing --language value.")
                : argument["--language=".Length..];
            _ = NormalizePreference(language);
        }
        Apply(language);
        return remaining.ToArray();
    }

    public static string Get(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
        var catalog = Language == "zh-CN" ? Chinese : English;
        return catalog.TryGetValue(key, out var text) || English.TryGetValue(key, out text)
            ? text : throw new KeyNotFoundException("Missing localization key: " + key);
    }
    public static string Format(string key, params object?[] values) => string.Format(DisplayCulture, Get(key), values);

    /// <summary>Request-local culture; disposing restores the caller without changing process defaults.</summary>
    public static IDisposable UseLanguage(string? acceptLanguage)
    {
        var prior = CultureInfo.CurrentUICulture;
        var priorRequest = RequestLanguage.Value;
        var language = SelectRequestLanguage(acceptLanguage);
        RequestLanguage.Value = language;
        CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(language);
        return new CultureScope(prior, priorRequest);
    }

    private static string SelectRequestLanguage(string? header)
    {
        if (string.IsNullOrWhiteSpace(header)) return Language;
        // Bound untrusted HTTP headers and honor quality values, including exclusions (q=0).
        if (header.Length > 1024) return "en-US";
        var candidates = new List<(string language, double weight, int order)>();
        foreach (var part in header.Split(',').Take(20))
        {
            var fields = part.Trim().Split(';'); var tag = fields[0].Trim(); double weight = 1;
            if (fields.Length > 1 && (!fields[1].Trim().StartsWith("q=", StringComparison.OrdinalIgnoreCase) ||
                !double.TryParse(fields[1].Trim()[2..], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out weight))) continue;
            if (weight <= 0 || weight > 1 || !double.IsFinite(weight)) continue;
            var language = tag.StartsWith("zh", StringComparison.OrdinalIgnoreCase) ? "zh-CN" :
                tag == "*" || tag.Equals("en", StringComparison.OrdinalIgnoreCase) || tag.StartsWith("en-", StringComparison.OrdinalIgnoreCase) ? "en-US" : null;
            if (language is not null) candidates.Add((language, weight, candidates.Count));
        }
        return candidates.OrderByDescending(x => x.weight).ThenBy(x => x.order).Select(x => x.language).FirstOrDefault() ?? "en-US";
    }

    private static string Resolve(string name) => name.StartsWith("zh", StringComparison.OrdinalIgnoreCase) ? "zh-CN" : "en-US";
    private static IReadOnlyDictionary<string, string> Load(string language)
    {
        using var stream = typeof(AppText).Assembly.GetManifestResourceStream("Localization." + language + ".json")
            ?? throw new InvalidOperationException("Missing embedded language: " + language);
        using var document = JsonDocument.Parse(stream);
        var entries = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var property in document.RootElement.EnumerateObject()) entries.Add(property.Name, property.Value.GetString()!);
        return new ReadOnlyDictionary<string, string>(entries);
    }
    private sealed class CultureScope(CultureInfo prior, string? priorRequest) : IDisposable
    {
        public void Dispose() { RequestLanguage.Value = priorRequest; CultureInfo.CurrentUICulture = prior; }
    }
}