using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// Encapsulates access to arbitrary translations. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json).
internal class Translator
{
/*********
** Fields
*********/
/// The translations for each locale.
private readonly IDictionary> All = new Dictionary>(StringComparer.OrdinalIgnoreCase);
/// The translations for the current locale, with locale fallback taken into account.
private IDictionary ForLocale;
/*********
** Accessors
*********/
/// The current locale code like fr-FR, or an empty string for English.
public string Locale { get; private set; }
/// The game's current language code.
public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; }
/*********
** Public methods
*********/
/// Construct an instance.
public Translator()
{
this.SetLocale(string.Empty, LocalizedContentManager.LanguageCode.en);
}
/// Set the current locale and pre-cache translations.
/// The current locale.
/// The game's current language code.
[MemberNotNull(nameof(Translator.ForLocale), nameof(Translator.Locale))]
public void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum)
{
this.Locale = locale.ToLower().Trim();
this.LocaleEnum = localeEnum;
this.ForLocale = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (string key in this.GetAllKeysRaw())
{
string? text = this.GetRaw(key, locale, withFallback: true);
if (text != null)
this.ForLocale.Add(key, new Translation(this.Locale, key, text));
}
}
/// Get all translations for the current locale.
public IEnumerable GetTranslations()
{
return this.ForLocale.Values.ToArray();
}
/// Get a translation for the current locale.
/// The translation key.
public Translation Get(string key)
{
this.ForLocale.TryGetValue(key, out Translation? translation);
return translation ?? new Translation(this.Locale, key, null);
}
/// Get a translation for the current locale.
/// The translation key.
/// 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.
public Translation Get(string key, object? tokens)
{
return this.Get(key).Tokens(tokens);
}
/// Get a translation in every locale for which it's defined.
/// The translation key.
/// Whether to add duplicate translations for locale fallback. For example, if a translation is defined in default.json but not fr.json, setting this to true will add a fr entry which duplicates the default text.
public IDictionary GetInAllLocales(string key, bool withFallback)
{
IDictionary translations = new Dictionary();
foreach (var localeSet in this.All)
{
string locale = localeSet.Key;
string? text = this.GetRaw(key, locale, withFallback);
if (text != null)
translations[locale] = new Translation(locale, key, text);
}
return translations;
}
/// Set the translations to use.
/// The translations to use.
internal Translator SetTranslations(IDictionary> translations)
{
// reset translations
this.All.Clear();
foreach (var pair in translations)
this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.OrdinalIgnoreCase);
// rebuild cache
this.SetLocale(this.Locale, this.LocaleEnum);
return this;
}
/*********
** Private methods
*********/
/// Get all translation keys in the underlying translation data, ignoring the cache.
private IEnumerable GetAllKeysRaw()
{
return new HashSet(
this.All.SelectMany(p => p.Value.Keys),
StringComparer.OrdinalIgnoreCase
);
}
/// Get a translation from the underlying translation data, ignoring the cache.
/// The translation key.
/// The locale to get.
/// Whether to add duplicate translations for locale fallback. For example, if a translation is defined in default.json but not fr.json, setting this to true will add a fr entry which duplicates the default text.
private string? GetRaw(string key, string locale, bool withFallback)
{
foreach (string next in this.GetRelevantLocales(locale))
{
string? translation = null;
bool hasTranslation =
this.All.TryGetValue(next, out IDictionary? translations)
&& translations.TryGetValue(key, out translation);
if (hasTranslation)
return translation;
if (!withFallback)
break;
}
return null;
}
/// Get the locales which can provide translations for the given locale, in precedence order.
/// The locale for which to find valid locales.
private IEnumerable GetRelevantLocales(string locale)
{
// given locale
yield return locale;
// broader locales (like pt-BR => pt)
while (true)
{
int dashIndex = locale.LastIndexOf('-');
if (dashIndex <= 0)
break;
locale = locale.Substring(0, dashIndex);
yield return locale;
}
// default
if (locale != "default")
yield return "default";
}
}
}