using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.RegularExpressions; namespace StardewModdingAPI { /// A translation string with a fluent API to customise it. public class Translation { /********* ** Fields *********/ /// The placeholder text when the translation is null or empty, where {0} is the translation key. internal const string PlaceholderText = "(no translation:{0})"; /// The locale for which the translation was fetched like fr-FR, or an empty string for English. private readonly string Locale; /// The underlying translation text. private readonly string? Text; /// The value to return if the translations is undefined. private readonly string? Placeholder; /********* ** Accessors *********/ /// The original translation key. public string Key { get; } /********* ** Public methods *********/ /// Construct an instance. /// The locale for which the translation was fetched. /// The translation key. /// The underlying translation text. internal Translation(string locale, string key, string? text) : this(locale, key, text, string.Format(Translation.PlaceholderText, key)) { } /// 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.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. Due to limitations with nullable reference types, setting this to false will still mark the text non-nullable. public Translation UsePlaceholder(bool use) { return new Translation(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 object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. /// The argument is null. public Translation Tokens(object? tokens) { if (string.IsNullOrWhiteSpace(this.Text) || tokens == null) return this; // get dictionary of tokens Dictionary tokenLookup = new(StringComparer.OrdinalIgnoreCase); { // from dictionary if (tokens is IDictionary inputLookup) { foreach (DictionaryEntry entry in inputLookup) { string? key = entry.Key.ToString()?.Trim(); if (key != null) tokenLookup[key] = entry.Value?.ToString(); } } // from object properties else { Type type = tokens.GetType(); foreach (PropertyInfo prop in type.GetProperties()) tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString(); foreach (FieldInfo field in type.GetFields()) tokenLookup[field.Name] = field.GetValue(tokens)?.ToString(); } } // format translation string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => { string key = match.Groups[1].Value.Trim(); return tokenLookup.TryGetValue(key, out string? value) ? (value ?? "") : match.Value; }); return new Translation(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. /// Limitation with nullable reference types: if there's no text and you disabled the fallback via , this will return null but the return value will still be marked non-nullable. 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. /// Limitation with nullable reference types: if there's no text and you disabled the fallback via , this will return null but the return value will still be marked non-nullable. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The null check is required due to limitations in nullable type annotations (see remarks).")] public static implicit operator string(Translation translation) { return translation?.ToString()!; } /********* ** Private methods *********/ /// Construct an instance. /// The locale for which the translation was fetched. /// The translation key. /// The underlying translation text. /// The value to return if the translations is undefined. private Translation(string locale, string key, string? text, string? placeholder) { this.Locale = locale; this.Key = key; this.Text = text; this.Placeholder = placeholder; } } }