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();
}
}
}