using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Xml.Serialization; namespace OpenTap.Translation; /// /// Functions for working with translations /// public static class TranslationManager { private static readonly ITranslator Translator = new Translator(); internal static string CultureAsString(CultureInfo culture) { if (CultureInfo.InvariantCulture.Equals(culture)) return "Neutral"; return $"{culture.NativeName} ({culture.EnglishName})"; } /// /// The default language. /// [Browsable(false)] [XmlIgnore] internal static CultureInfo NeutralLanguage => CultureInfo.InvariantCulture; /// /// The list of languages supported by installed resource files. /// public static IEnumerable SupportedLanguages => Translator.SupportedLanguages; /// /// Get an appropriate DisplayAttribute for the specified reflection data in the requested language. /// /// /// /// public static DisplayAttribute TranslateMember(IReflectionData mem, CultureInfo language = null) { return Translator.TranslateMember(mem, language ?? EngineSettings.Current.Language); } /// /// Get an appropriate DisplayAttribute for the specified enum in the requested language. /// /// /// /// public static DisplayAttribute TranslateEnum(Enum e, CultureInfo language = null) { return Translator.TranslateEnum(e, language ?? EngineSettings.Current.Language); } /// /// The directory containing translation resource files. /// public static string TranslationDirectory => Path.Combine(ExecutorClient.ExeDir, "Languages"); /// /// Get an appropriate DisplayAttribute for the specified enum in the requested language. /// /// /// /// public static string TranslateKey(string key, CultureInfo language = null) { return Translator.TranslateString(key, language ?? EngineSettings.Current.Language); } /// /// Look for a translated version of the input member and validate the format parameters. /// If no translation is found, or the parameter count does not match, return the neutral string. /// /// The string localizer which owns the translated string. /// The neutral (non-translated) string. /// Lookup key for finding the translation. /// The desired translation language /// The translated string, if a translation exists, and the string format parameters in the translated string matches the source string. Otherwise, returns the neutral string. public static FormatString TranslateFormat(this IStringLocalizer stringLocalizer, string neutral, [CallerMemberName] string key = null, CultureInfo language = null) { var translated = Translate(stringLocalizer, neutral, key, language); if (!ReferenceEquals(neutral, translated)) { var c1 = CountTemplates(neutral); var c2 = CountTemplates(translated); if (c1 != c2) { log.ErrorOnce(neutral, $"Template parameter count mismatch detected.\n" + $"'{neutral}' has {c1} template parameters.\n" + $"'{translated}' has {c2} template parameters.\n" + $"The first string will be preferred over the second string."); // Use neutral string instead return new FormatString(neutral); } } return new FormatString(translated); } /// /// Look for a translated version of the input member. If no translation is found, return the neutral string. /// /// The string localizer which owns the translated string. /// The neutral (non-translated) string. /// Lookup key for finding the translation. /// The desired translation language /// The translated string, if a translation exists. Otherwise, returns the neutral string. public static string Translate(this IStringLocalizer stringLocalizer, string neutral, [CallerMemberName] string key = null, CultureInfo language = null) { return TranslateFunction(stringLocalizer, neutral, key, language); } // We add this small level of indirection in order to inject a hook to automatically // populate a list of lookup keys and strings for creating translations. // Do not rename this field or change the delegate!! private static Func TranslateFunction = _translate; private static string _translate(this IStringLocalizer stringLocalizer, string neutral, [CallerMemberName] string key = null, CultureInfo language = null) { language ??= EngineSettings.Current.Language; if (language.Equals(NeutralLanguage) || string.IsNullOrWhiteSpace(key)) return neutral; var type = stringLocalizer.GetType(); if (type.IsGenericType) type = type.GetGenericTypeDefinition(); var fullKey = type.FullName + $".{key}"; return TranslateKey(fullKey, language) ?? neutral; } static int CountTemplates(string s) { var chars = s.ToArray(); int n = 0; bool bracketOpen = false; for (int i = 0; i < chars.Length; i++) { if (!bracketOpen && chars[i] == '{') { bracketOpen = true; } else if (bracketOpen && chars[i] == '{') { /* double bracket escapes */ bracketOpen = false; } else if (bracketOpen && chars[i] == '}') { bracketOpen = false; n++; } } return n; } private static readonly TraceSource log = Log.CreateSource("Translation Manager"); } /// /// An IStringLocalizer can define localizable strings by calling the Translate() or TranslateFormat() method from a public computed property. /// Example: public string MyString => Translate("Neutral string") // returns "Translated string" if available /// A localizable string defaults to the neutral string, unless, OpenTAP is configured to display in a different /// language in which a translation is available. /// public interface IStringLocalizer : ITapPlugin { } /// /// A FormatString is a string which requires format parameters. /// public class FormatString(string format) { /// /// Creates a formatted string based on this instance and the provided format arguments. /// /// The format arguments /// The formatted string public string Format(params object[] args) => string.Format(format, args); }