summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI/Translation.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/StardewModdingAPI/Translation.cs')
-rw-r--r--src/StardewModdingAPI/Translation.cs154
1 files changed, 154 insertions, 0 deletions
diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs
new file mode 100644
index 00000000..ce344f81
--- /dev/null
+++ b/src/StardewModdingAPI/Translation.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+namespace StardewModdingAPI
+{
+ /// <summary>A translation string with a fluent API to customise it.</summary>
+ public class Translation
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The placeholder text when the translation is <c>null</c> or empty, where <c>{0}</c> is the translation key.</summary>
+ internal const string PlaceholderText = "(no translation:{0})";
+
+ /// <summary>The name of the relevant mod for error messages.</summary>
+ private readonly string ModName;
+
+ /// <summary>The locale for which the translation was fetched.</summary>
+ private readonly string Locale;
+
+ /// <summary>The underlying translation text.</summary>
+ private readonly string Text;
+
+ /// <summary>The value to return if the translations is undefined.</summary>
+ private readonly string Placeholder;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The original translation key.</summary>
+ public string Key { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an isntance.</summary>
+ /// <param name="modName">The name of the relevant mod for error messages.</param>
+ /// <param name="locale">The locale for which the translation was fetched.</param>
+ /// <param name="key">The translation key.</param>
+ /// <param name="text">The underlying translation text.</param>
+ internal Translation(string modName, string locale, string key, string text)
+ : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { }
+
+ /// <summary>Construct an isntance.</summary>
+ /// <param name="modName">The name of the relevant mod for error messages.</param>
+ /// <param name="locale">The locale for which the translation was fetched.</param>
+ /// <param name="key">The translation key.</param>
+ /// <param name="text">The underlying translation text.</param>
+ /// <param name="placeholder">The value to return if the translations is undefined.</param>
+ 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;
+ }
+
+ /// <summary>Throw an exception if the translation text is <c>null</c> or empty.</summary>
+ /// <exception cref="KeyNotFoundException">There's no available translation matching the requested key and locale.</exception>
+ 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;
+ }
+
+ /// <summary>Replace the text if it's <c>null</c> or empty. If you set a <c>null</c> or empty value, the translation will show the fallback "no translation" placeholder (see <see cref="UsePlaceholder"/> if you want to disable that). Returns a new instance if changed.</summary>
+ /// <param name="default">The default value.</param>
+ public Translation Default(string @default)
+ {
+ return this.HasValue()
+ ? this
+ : new Translation(this.ModName, this.Locale, this.Key, @default);
+ }
+
+ /// <summary>Whether to return a "no translation" placeholder if the translation is <c>null</c> or empty. Returns a new instance.</summary>
+ /// <param name="use">Whether to return a placeholder.</param>
+ public Translation UsePlaceholder(bool use)
+ {
+ return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null);
+ }
+
+ /// <summary>Replace tokens in the text like <c>{{value}}</c> with the given values. Returns a new instance.</summary>
+ /// <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>
+ /// <exception cref="ArgumentNullException">The <paramref name="tokens"/> argument is <c>null</c>.</exception>
+ public Translation Tokens(object tokens)
+ {
+ if (string.IsNullOrWhiteSpace(this.Text) || tokens == null)
+ return this;
+
+ // get dictionary of tokens
+ IDictionary<string, string> tokenLookup = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
+ {
+ // 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.ModName, this.Locale, this.Key, text);
+ }
+
+ /// <summary>Get whether the translation has a defined value.</summary>
+ public bool HasValue()
+ {
+ return !string.IsNullOrEmpty(this.Text);
+ }
+
+ /// <summary>Get the translation text. Calling this method isn't strictly necessary, since you can assign a <see cref="Translation"/> value directly to a string.</summary>
+ public override string ToString()
+ {
+ return this.Placeholder != null && !this.HasValue()
+ ? this.Placeholder
+ : this.Text;
+ }
+
+ /// <summary>Get a string representation of the given translation.</summary>
+ /// <param name="translation">The translation key.</param>
+ public static implicit operator string(Translation translation)
+ {
+ return translation?.ToString();
+ }
+ }
+}