chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
 
namespace OpenTap.Translation;
 
internal interface ITranslator
{
    public IEnumerable<CultureInfo> SupportedLanguages { get; }
    public DisplayAttribute TranslateMember(IReflectionData i, CultureInfo language);
    public string TranslateString(string key, CultureInfo language);
    public DisplayAttribute TranslateEnum(Enum e, CultureInfo language);
}
 
internal class Translator : ITranslator, IDisposable
{
    public IEnumerable<CultureInfo> SupportedLanguages => _cultures;
 
    /// <summary>
    /// Get a display attribute for the provided member in the requested language.
    /// <param name="i">The type or property a translated display attribute for.</param>
    /// <param name="language">The desired language of the output attribute. Defaults to the currently selected language
    /// if not specified.</param>
    /// <returns>A display attribute in the requested language. If no translation could be provided,
    /// the default DisplayAttribute is returned instead. Check the Language property of the returned attribute.</returns>
    /// </summary>
    public DisplayAttribute TranslateMember(IReflectionData i, CultureInfo language)
    {
        if (i == null) return null;
        if (_lookup.TryGetValue(language, out var prov))
        {
            if (prov.GetDisplayAttribute(i) is { } result)
                return result;
        }
        return DefaultDisplayAttribute.GetUntranslatedDisplayAttribute(i);
    }
 
    public DisplayAttribute TranslateEnum(Enum e, CultureInfo language)
    {
        var enumType = e.GetType();
        var name = enumType.GetEnumName(e);
        // This happens when the enum value is out of range, or if a flags enum is passed with multiple flags set
        if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException("Enum value out of range.");
 
        if (_lookup.TryGetValue(language, out var prov))
            if (prov.GetDisplayAttribute(e, name) is { } disp)
                return disp;
 
        // fallback
        var memberInfo = enumType.GetMember(name).FirstOrDefault();
        return memberInfo.GetDisplayAttribute();
    }
 
    public string TranslateString(string key, CultureInfo language)
    {
        if (_lookup.TryGetValue(language, out var prov))
            return prov.GetString(key);
        return null;
    }
 
    private readonly ImmutableArray<CultureInfo> _cultures;
    private readonly ImmutableDictionary<CultureInfo, ITranslationProvider> _lookup;
    public Translator()
    {
        var translationDir = TranslationManager.TranslationDirectory;
        var translationFiles = Directory.Exists(translationDir)
            ? Directory.GetFiles(translationDir, "*.resx", SearchOption.AllDirectories)
            : [];
 
        var lut = new Dictionary<CultureInfo, List<string>>();
 
        foreach (var f in translationFiles)
        {
            // strip .resx
            var localname = Path.GetFileNameWithoutExtension(f);
            var cultureString = Path.GetExtension(localname);
            CultureInfo culture;
            try
            {
                if (cultureString.StartsWith(".")) cultureString = cultureString.Substring(1);
                culture = new CultureInfo(cultureString);
                // If the culture is a custom culture, this likely means that no culture was specified at all.
                // This is a bit hard to determine unambiguously because the pattern used by resx files will be e.g:
                // foo.resx -> neutral
                // foo.de.resx -> german
                // But the OpenTAP resx generator will name the resource file based on the package name,
                // so in the edge case where a package is named "MyPackage.de", the english resource file would be named
                // MyPackage.de.resx, and the german resource file would be named MyPackage.de.de.resx
                // But let's ignore this edge case for now.
                if (culture.CultureTypes.HasFlag(CultureTypes.UserCustomCulture) || string.IsNullOrWhiteSpace(cultureString))
                    culture = TranslationManager.NeutralLanguage;
            }
            catch
            {
                // If there was a parser error, also assume english
                // This normally happens when the culture contains illegal characters, but not if the culture 
                // is not recognized. E.g. the culture 'asdkjasheoiqwje' would be parsed successfully as a UserCustomCulture
                culture = TranslationManager.NeutralLanguage;
            }
 
            // Ignore neutral language files. Assume the source code is authoritative.
            if (culture.Equals(TranslationManager.NeutralLanguage))
                continue;
 
            if (!lut.TryGetValue(culture, out var lst)) lst = [];
            lst.Add(f);
            lut[culture] = lst;
        }
 
        _cultures = lut.Keys
            .Concat([TranslationManager.NeutralLanguage])
            .Distinct()
            .OrderBy(TranslationManager.CultureAsString)
            .ToImmutableArray();
        _lookup = lut.ToImmutableDictionary(x => x.Key,
            ITranslationProvider (x) => new ResXTranslationProvider(x.Key, x.Value));
    }
 
    public void Dispose()
    {
        foreach (var l in _lookup)
        {
            (l.Value as IDisposable)?.Dispose();
        }
    }
}