using System; using System.Collections.Generic; using System.Linq; using StardewValley; namespace StardewModdingAPI.Framework { /// <summary>Encapsulates access to arbitrary translations. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary> internal class Translator { /********* ** Fields *********/ /// <summary>The translations for each locale.</summary> private readonly IDictionary<string, IDictionary<string, string>> All = new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase); /// <summary>The translations for the current locale, with locale fallback taken into account.</summary> private IDictionary<string, Translation> ForLocale; /********* ** Accessors *********/ /// <summary>The current locale.</summary> public string Locale { get; private set; } /// <summary>The game's current language code.</summary> public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> public Translator() { this.SetLocale(string.Empty, LocalizedContentManager.LanguageCode.en); } /// <summary>Set the current locale and precache translations.</summary> /// <param name="locale">The current locale.</param> /// <param name="localeEnum">The game's current language code.</param> public void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) { this.Locale = locale.ToLower().Trim(); this.LocaleEnum = localeEnum; this.ForLocale = new Dictionary<string, Translation>(StringComparer.OrdinalIgnoreCase); foreach (string key in this.GetAllKeysRaw()) { string text = this.GetRaw(key, locale, withFallback: true); this.ForLocale.Add(key, new Translation(this.Locale, key, text)); } } /// <summary>Get all translations for the current locale.</summary> public IEnumerable<Translation> GetTranslations() { return this.ForLocale.Values.ToArray(); } /// <summary>Get a translation for the current locale.</summary> /// <param name="key">The translation key.</param> public Translation Get(string key) { this.ForLocale.TryGetValue(key, out Translation translation); return translation ?? new Translation(this.Locale, key, null); } /// <summary>Get a translation for the current locale.</summary> /// <param name="key">The translation key.</param> /// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param> public Translation Get(string key, object tokens) { return this.Get(key).Tokens(tokens); } /// <summary>Get a translation in every locale for which it's defined.</summary> /// <param name="key">The translation key.</param> /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param> public IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback) { IDictionary<string, Translation> translations = new Dictionary<string, Translation>(); foreach (var localeSet in this.All) { string locale = localeSet.Key; string text = this.GetRaw(key, locale, withFallback); if (text != null) translations[locale] = new Translation(locale, key, text); } return translations; } /// <summary>Set the translations to use.</summary> /// <param name="translations">The translations to use.</param> internal Translator SetTranslations(IDictionary<string, IDictionary<string, string>> translations) { // reset translations this.All.Clear(); foreach (var pair in translations) this.All[pair.Key] = new Dictionary<string, string>(pair.Value, StringComparer.OrdinalIgnoreCase); // rebuild cache this.SetLocale(this.Locale, this.LocaleEnum); return this; } /********* ** Private methods *********/ /// <summary>Get all translation keys in the underlying translation data, ignoring the <see cref="ForLocale"/> cache.</summary> private IEnumerable<string> GetAllKeysRaw() { return new HashSet<string>( this.All.SelectMany(p => p.Value.Keys), StringComparer.OrdinalIgnoreCase ); } /// <summary>Get a translation from the underlying translation data, ignoring the <see cref="ForLocale"/> cache.</summary> /// <param name="key">The translation key.</param> /// <param name="locale">The locale to get.</param> /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param> private string GetRaw(string key, string locale, bool withFallback) { foreach (string next in this.GetRelevantLocales(locale)) { string translation = null; bool hasTranslation = this.All.TryGetValue(next, out IDictionary<string, string> translations) && translations.TryGetValue(key, out translation); if (hasTranslation) return translation; if (!withFallback) break; } return null; } /// <summary>Get the locales which can provide translations for the given locale, in precedence order.</summary> /// <param name="locale">The locale for which to find valid locales.</param> private IEnumerable<string> GetRelevantLocales(string locale) { // given locale yield return locale; // broader locales (like pt-BR => pt) while (true) { int dashIndex = locale.LastIndexOf('-'); if (dashIndex <= 0) break; locale = locale.Substring(0, dashIndex); yield return locale; } // default if (locale != "default") yield return "default"; } } }