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