diff --git a/src/SMAPI/Events/IContentEvents.cs b/src/SMAPI/Events/IContentEvents.cs
index abbaaf33..d537db70 100644
--- a/src/SMAPI/Events/IContentEvents.cs
+++ b/src/SMAPI/Events/IContentEvents.cs
@@ -19,5 +19,9 @@ namespace StardewModdingAPI.Events
/// Raised after an asset is loaded by the content pipeline, after all mod edits specified via have been applied.
/// This event is only raised if something requested the asset from the content pipeline. Invalidating an asset from the content cache won't necessarily reload it automatically.
event EventHandler AssetReady;
+
+ /// Raised after the game language changes.
+ /// For non-English players, this may be raised during startup when the game switches to the previously selected language.
+ event EventHandler LocaleChanged;
}
}
diff --git a/src/SMAPI/Events/LocaleChangedEventArgs.cs b/src/SMAPI/Events/LocaleChangedEventArgs.cs
new file mode 100644
index 00000000..09d3f6e5
--- /dev/null
+++ b/src/SMAPI/Events/LocaleChangedEventArgs.cs
@@ -0,0 +1,45 @@
+using System;
+using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
+
+namespace StardewModdingAPI.Events
+{
+ /// Event arguments for an event.
+ public class LocaleChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The previous language enum value.
+ /// For a custom language, this is always .
+ public LanguageCode OldLanguage { get; }
+
+ /// The previous locale code.
+ /// This is the locale code as it appears in asset names, like fr-FR in Maps/springobjects.fr-FR. The locale code for English is an empty string.
+ public string OldLocale { get; }
+
+ /// The new language enum value.
+ ///
+ public LanguageCode NewLanguage { get; }
+
+ /// The new locale code.
+ ///
+ public string NewLocale { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The previous language enum value.
+ /// The previous locale code.
+ /// The new language enum value.
+ /// The new locale code.
+ internal LocaleChangedEventArgs(LanguageCode oldLanguage, string oldLocale, LanguageCode newLanguage, string newLocale)
+ {
+ this.OldLanguage = oldLanguage;
+ this.OldLocale = oldLocale;
+ this.NewLanguage = newLanguage;
+ this.NewLocale = newLocale;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index bcfd7dd7..41540047 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Events
///
public readonly ManagedEvent AssetReady;
+ ///
+ public readonly ManagedEvent LocaleChanged;
+
/****
** Display
@@ -204,6 +207,7 @@ namespace StardewModdingAPI.Framework.Events
this.AssetRequested = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.AssetRequested));
this.AssetsInvalidated = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.AssetsInvalidated));
this.AssetReady = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.AssetReady));
+ this.LocaleChanged = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged));
this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged));
this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true);
diff --git a/src/SMAPI/Framework/Events/ModContentEvents.cs b/src/SMAPI/Framework/Events/ModContentEvents.cs
index cb242e99..beb96031 100644
--- a/src/SMAPI/Framework/Events/ModContentEvents.cs
+++ b/src/SMAPI/Framework/Events/ModContentEvents.cs
@@ -30,6 +30,13 @@ namespace StardewModdingAPI.Framework.Events
remove => this.EventManager.AssetReady.Remove(value);
}
+ ///
+ public event EventHandler LocaleChanged
+ {
+ add => this.EventManager.LocaleChanged.Add(value, this.Mod);
+ remove => this.EventManager.LocaleChanged.Remove(value);
+ }
+
/*********
** Public methods
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index eab977ac..5deb177c 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -46,6 +46,7 @@ using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Menus;
using xTile.Display;
+using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
using SObject = StardewValley.Object;
@@ -62,7 +63,7 @@ namespace StardewModdingAPI.Framework
** Low-level components
****/
/// Tracks whether the game should exit immediately and any pending initialization should be cancelled.
- private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource();
+ private readonly CancellationTokenSource CancellationToken = new();
/// Manages the SMAPI console window and log file.
private readonly LogManager LogManager;
@@ -71,16 +72,16 @@ namespace StardewModdingAPI.Framework
private Monitor Monitor => this.LogManager.Monitor;
/// Simplifies access to private game code.
- private readonly Reflector Reflection = new Reflector();
+ private readonly Reflector Reflection = new();
/// Encapsulates access to SMAPI core translations.
- private readonly Translator Translator = new Translator();
+ private readonly Translator Translator = new();
/// The SMAPI configuration settings.
private readonly SConfig Settings;
/// The mod toolkit used for generic mod interactions.
- private readonly ModToolkit Toolkit = new ModToolkit();
+ private readonly ModToolkit Toolkit = new();
/****
** Higher-level components
@@ -99,7 +100,7 @@ namespace StardewModdingAPI.Framework
/// Tracks the installed mods.
/// This is initialized after the game starts.
- private readonly ModRegistry ModRegistry = new ModRegistry();
+ private readonly ModRegistry ModRegistry = new();
/// Manages SMAPI events for mods.
private readonly EventManager EventManager;
@@ -129,18 +130,21 @@ namespace StardewModdingAPI.Framework
/// Whether the player just returned to the title screen.
public bool JustReturnedToTitle { get; set; }
+ /// The last language set by the game.
+ private (string Locale, LanguageCode Code) LastLanguage { get; set; } = ("", LanguageCode.en);
+
/// The maximum number of consecutive attempts SMAPI should make to recover from an update error.
- private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
+ private readonly Countdown UpdateCrashTimer = new(60); // 60 ticks = roughly one second
/// Asset interceptors added or removed since the last tick.
- private readonly List ReloadAssetInterceptorsQueue = new List();
+ private readonly List ReloadAssetInterceptorsQueue = new();
/// A list of queued commands to parse and execute.
/// This property must be thread-safe, since it's accessed from a separate console input thread.
- private readonly ConcurrentQueue RawCommandQueue = new ConcurrentQueue();
+ private readonly ConcurrentQueue RawCommandQueue = new();
/// A list of commands to execute on each screen.
- private readonly PerScreen>> ScreenCommandQueue = new PerScreen>>(() => new List>());
+ private readonly PerScreen>> ScreenCommandQueue = new(() => new List>());
/*********
@@ -369,13 +373,13 @@ namespace StardewModdingAPI.Framework
xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom));
// load mod data
- ModToolkit toolkit = new ModToolkit();
+ ModToolkit toolkit = new();
ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath);
// load mods
{
this.Monitor.Log("Loading mod metadata...", LogLevel.Debug);
- ModResolver resolver = new ModResolver();
+ ModResolver resolver = new();
// log loose files
{
@@ -1048,7 +1052,7 @@ namespace StardewModdingAPI.Framework
// get locale
string locale = this.ContentCore.GetLocale();
- LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language;
+ LanguageCode languageCode = this.ContentCore.Language;
// update core translations
this.Translator.SetLocale(locale, languageCode);
@@ -1061,6 +1065,20 @@ namespace StardewModdingAPI.Framework
foreach (ContentPack contentPack in mod.GetFakeContentPacks())
contentPack.TranslationImpl.SetLocale(locale, languageCode);
}
+
+ // raise event
+ if (this.EventManager.LocaleChanged.HasListeners())
+ {
+ this.EventManager.LocaleChanged.Raise(
+ new LocaleChangedEventArgs(
+ oldLanguage: this.LastLanguage.Code,
+ oldLocale: this.LastLanguage.Locale,
+ newLanguage: languageCode,
+ newLocale: locale
+ )
+ );
+ }
+ this.LastLanguage = (locale, languageCode);
}
/// Raised when the low-level stage while loading a save changes.
@@ -1385,7 +1403,7 @@ namespace StardewModdingAPI.Framework
{
// create client
string url = this.Settings.WebApiBaseUrl;
- WebApiClient client = new WebApiClient(url, Constants.ApiVersion);
+ WebApiClient client = new(url, Constants.ApiVersion);
this.Monitor.Log("Checking for updates...");
// check SMAPI version
@@ -1569,7 +1587,7 @@ namespace StardewModdingAPI.Framework
// load mods
IList skippedMods = new List();
- using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
+ using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
{
// init
HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
@@ -1758,7 +1776,7 @@ namespace StardewModdingAPI.Framework
IManifest manifest = mod.Manifest;
IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor);
- TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
+ TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, translationHelper, jsonHelper);
mod.SetMod(contentPack, monitor, translationHelper);
this.ModRegistry.Add(mod);
@@ -1831,16 +1849,16 @@ namespace StardewModdingAPI.Framework
// init mod helpers
IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
- TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
+ TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
IModHelper modHelper;
{
IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
{
IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name);
IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
- TranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
+ TranslationHelper packTranslationHelper = new(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
- ContentPack contentPack = new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper);
+ ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper);
this.ReloadTranslationsForTemporaryContentPack(mod, contentPack);
mod.FakeContentPacks.Add(new WeakReference(contentPack));
return contentPack;
@@ -1982,7 +2000,7 @@ namespace StardewModdingAPI.Framework
// read translation files
var translations = new Dictionary>();
errors = new List();
- DirectoryInfo translationsDir = new DirectoryInfo(folderPath);
+ DirectoryInfo translationsDir = new(folderPath);
if (translationsDir.Exists)
{
foreach (FileInfo file in translationsDir.EnumerateFiles("*.json"))
@@ -2038,7 +2056,7 @@ namespace StardewModdingAPI.Framework
{
// default path
{
- FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}"));
+ FileInfo defaultFile = new(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}"));
if (!defaultFile.Exists)
return defaultFile.FullName;
}
@@ -2046,7 +2064,7 @@ namespace StardewModdingAPI.Framework
// get first disambiguated path
for (int i = 2; i < int.MaxValue; i++)
{
- FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}"));
+ FileInfo file = new(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}"));
if (!file.Exists)
return file.FullName;
}
@@ -2058,7 +2076,7 @@ namespace StardewModdingAPI.Framework
/// Delete normal (non-crash) log files created by SMAPI.
private void PurgeNormalLogs()
{
- DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir);
+ DirectoryInfo logsDir = new(Constants.LogDir);
if (!logsDir.Exists)
return;