using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace StardewModdingAPI { /// A translation string with a fluent API to customise it. public class Translation { /********* ** Properties *********/ /// The placeholder text when the translation is null or empty, where {0} is the translation key. internal const string PlaceholderText = "(no translation:{0})"; /// The name of the relevant mod for error messages. private readonly string ModName; /// The locale for which the translation was fetched. private readonly string Locale; /// The translation key. private readonly string Key; /// The underlying translation text. private readonly string Text; /// The value to return if the translations is undefined. private readonly string Placeholder; /********* ** Public methods *********/ /// Construct an isntance. /// The name of the relevant mod for error messages. /// The locale for which the translation was fetched. /// The translation key. /// The underlying translation text. internal Translation(string modName, string locale, string key, string text) : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } /// Construct an isntance. /// The name of the relevant mod for error messages. /// The locale for which the translation was fetched. /// The translation key. /// The underlying translation text. /// The value to return if the translations is undefined. internal Translation(string modName, string locale, string key, string text, string placeholder) { this.ModName = modName; this.Locale = locale; this.Key = key; this.Text = text; this.Placeholder = placeholder; } /// Throw an exception if the translation text is null or empty. /// There's no available translation matching the requested key and locale. public Translation Assert() { if (!this.HasValue()) throw new KeyNotFoundException($"The '{this.ModName}' mod doesn't have a translation with key '{this.Key}' for the '{this.Locale}' locale or its fallbacks."); return this; } /// Replace the text if it's null or empty. If you set a null or empty value, the translation will show the fallback "no translation" placeholder (see if you want to disable that). Returns a new instance if changed. /// The default value. public Translation Default(string @default) { return this.HasValue() ? this : new Translation(this.ModName, this.Locale, this.Key, @default); } /// Whether to return a "no translation" placeholder if the translation is null or empty. Returns a new instance. /// Whether to return a placeholder. public Translation UsePlaceholder(bool use) { return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); } /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. /// An anonymous object containing token key/value pairs, like new { value = 42, name = "Cranberries" }. /// The argument is null. public Translation Tokens(object tokens) { if (tokens == null) throw new ArgumentNullException(nameof(tokens)); IDictionary dictionary = tokens .GetType() .GetProperties() .ToDictionary( p => p.Name, p => p.GetValue(tokens) ); return this.Tokens(dictionary); } /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. /// A dictionary containing token key/value pairs. /// The argument is null. public Translation Tokens(IDictionary tokens) { if (tokens == null) throw new ArgumentNullException(nameof(tokens)); tokens = tokens.ToDictionary(p => p.Key.Trim(), p => p.Value, StringComparer.InvariantCultureIgnoreCase); string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => { string key = match.Groups[1].Value.Trim(); return tokens.TryGetValue(key, out object value) ? value?.ToString() : match.Value; }); return new Translation(this.ModName, this.Locale, this.Key, text); } /// Get whether the translation has a defined value. public bool HasValue() { return !string.IsNullOrEmpty(this.Text); } /// Get the translation text. Calling this method isn't strictly necessary, since you can assign a value directly to a string. public override string ToString() { return this.Placeholder != null && !this.HasValue() ? this.Placeholder : this.Text; } /// Get a string representation of the given translation. /// The translation key. public static implicit operator string(Translation translation) { return translation?.ToString(); } } }