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 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 SupportedLanguages => _cultures; /// /// Get a display attribute for the provided member in the requested language. /// The type or property a translated display attribute for. /// The desired language of the output attribute. Defaults to the currently selected language /// if not specified. /// 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. /// 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 _cultures; private readonly ImmutableDictionary _lookup; public Translator() { var translationDir = TranslationManager.TranslationDirectory; var translationFiles = Directory.Exists(translationDir) ? Directory.GetFiles(translationDir, "*.resx", SearchOption.AllDirectories) : []; var lut = new Dictionary>(); 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(); } } }