diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/TranslationHelper.cs | 78 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 127 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/Translator.cs | 128 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.csproj | 3 | ||||
-rw-r--r-- | src/SMAPI/i18n/default.json | 3 |
6 files changed, 231 insertions, 115 deletions
diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs index 65850384..be7768e8 100644 --- a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers @@ -11,21 +9,18 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Fields *********/ - /// <summary>The translations for each locale.</summary> - private readonly IDictionary<string, IDictionary<string, string>> All = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase); - - /// <summary>The translations for the current locale, with locale fallback taken into account.</summary> - private IDictionary<string, Translation> ForLocale; + /// <summary>The underlying translation manager.</summary> + private readonly Translator Translator; /********* ** Accessors *********/ /// <summary>The current locale.</summary> - public string Locale { get; private set; } + public string Locale => this.Translator.Locale; /// <summary>The game's current language code.</summary> - public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + public LocalizedContentManager.LanguageCode LocaleEnum => this.Translator.LocaleEnum; /********* @@ -38,22 +33,21 @@ namespace StardewModdingAPI.Framework.ModHelpers public TranslationHelper(string modID, string locale, LocalizedContentManager.LanguageCode languageCode) : base(modID) { - // set locale - this.SetLocale(locale, languageCode); + this.Translator = new Translator(); + this.Translator.SetLocale(locale, languageCode); } /// <summary>Get all translations for the current locale.</summary> public IEnumerable<Translation> GetTranslations() { - return this.ForLocale.Values.ToArray(); + return this.Translator.GetTranslations(); } /// <summary>Get a translation for the current locale.</summary> /// <param name="key">The translation key.</param> public Translation Get(string key) { - this.ForLocale.TryGetValue(key, out Translation translation); - return translation ?? new Translation(this.Locale, key, null); + return this.Translator.Get(key); } /// <summary>Get a translation for the current locale.</summary> @@ -61,21 +55,14 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <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> public Translation Get(string key, object tokens) { - return this.Get(key).Tokens(tokens); + return this.Translator.Get(key, tokens); } /// <summary>Set the translations to use.</summary> /// <param name="translations">The translations to use.</param> internal TranslationHelper SetTranslations(IDictionary<string, IDictionary<string, string>> translations) { - // reset translations - this.All.Clear(); - foreach (var pair in translations) - this.All[pair.Key] = new Dictionary<string, string>(pair.Value, StringComparer.InvariantCultureIgnoreCase); - - // rebuild cache - this.SetLocale(this.Locale, this.LocaleEnum); - + this.Translator.SetTranslations(translations); return this; } @@ -84,50 +71,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="localeEnum">The game's current language code.</param> internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) { - this.Locale = locale.ToLower().Trim(); - this.LocaleEnum = localeEnum; - - this.ForLocale = new Dictionary<string, Translation>(StringComparer.InvariantCultureIgnoreCase); - foreach (string next in this.GetRelevantLocales(this.Locale)) - { - // skip if locale not defined - if (!this.All.TryGetValue(next, out IDictionary<string, string> translations)) - continue; - - // add missing translations - foreach (var pair in translations) - { - if (!this.ForLocale.ContainsKey(pair.Key)) - this.ForLocale.Add(pair.Key, new Translation(this.Locale, pair.Key, pair.Value)); - } - } - } - - - /********* - ** Private methods - *********/ - /// <summary>Get the locales which can provide translations for the given locale, in precedence order.</summary> - /// <param name="locale">The locale for which to find valid locales.</param> - private IEnumerable<string> 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"; + this.Translator.SetLocale(locale, localeEnum); } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a4b38a50..bd131762 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI.Framework /// <summary>Simplifies access to private game code.</summary> private readonly Reflector Reflection = new Reflector(); + /// <summary>Encapsulates access to SMAPI core translations.</summary> + private readonly Translator Translator = new Translator(); + /// <summary>The SMAPI configuration settings.</summary> private readonly SConfig Settings; @@ -223,6 +226,7 @@ namespace StardewModdingAPI.Framework monitor: this.Monitor, monitorForGame: this.MonitorForGame, reflection: this.Reflection, + translator: this.Translator, eventManager: this.EventManager, jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, @@ -232,6 +236,7 @@ namespace StardewModdingAPI.Framework cancellationToken: this.CancellationToken, logNetworkTraffic: this.Settings.LogNetworkTraffic ); + this.Translator.SetLocale(this.GameInstance.ContentCore.GetLocale(), this.GameInstance.ContentCore.Language); StardewValley.Program.gamePtr = this.GameInstance; // apply game patches @@ -466,6 +471,9 @@ namespace StardewModdingAPI.Framework string locale = this.ContentCore.GetLocale(); LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; + // update core translations + this.Translator.SetLocale(locale, languageCode); + // update mod translation helpers foreach (IModMetadata mod in this.ModRegistry.GetAll()) mod.Translations.SetLocale(locale, languageCode); @@ -1027,6 +1035,14 @@ namespace StardewModdingAPI.Framework TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); IModHelper modHelper; { + IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + ITranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); + return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); + } + IModEvents events = new ModEvents(mod, this.EventManager); ICommandHelper commandHelper = new CommandHelper(mod, this.GameInstance.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); @@ -1036,14 +1052,6 @@ namespace StardewModdingAPI.Framework IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); - IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) - { - IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - ITranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); - return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); - } - modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); } @@ -1205,60 +1213,85 @@ namespace StardewModdingAPI.Framework /// <param name="mods">The mods for which to reload translations.</param> private void ReloadTranslations(IEnumerable<IModMetadata> mods) { - JsonHelper jsonHelper = this.Toolkit.JsonHelper; + // core SMAPI translations + { + var translations = this.ReadTranslationFiles(Path.Combine(Constants.InternalFilesPath, "i18n"), out IList<string> errors); + if (errors.Any() || !translations.Any()) + { + this.Monitor.Log("SMAPI couldn't load some core translations. You may need to reinstall SMAPI.", LogLevel.Warn); + foreach (string error in errors) + this.Monitor.Log($" - {error}", LogLevel.Warn); + } + this.Translator.SetTranslations(translations); + } + + // mod translations foreach (IModMetadata metadata in mods) { - // read translation files - IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>(); - DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); - if (translationsDir.Exists) + var translations = this.ReadTranslationFiles(Path.Combine(metadata.DirectoryPath, "i18n"), out IList<string> errors); + if (errors.Any()) { - foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + metadata.LogAsMod("Mod couldn't load some translation files:", LogLevel.Warn); + foreach (string error in errors) + metadata.LogAsMod($" - {error}", LogLevel.Warn); + } + metadata.Translations.SetTranslations(translations); + } + } + + /// <summary>Read translations from a directory containing JSON translation files.</summary> + /// <param name="folderPath">The folder path to search.</param> + /// <param name="errors">The errors indicating why translation files couldn't be parsed, indexed by translation filename.</param> + private IDictionary<string, IDictionary<string, string>> ReadTranslationFiles(string folderPath, out IList<string> errors) + { + JsonHelper jsonHelper = this.Toolkit.JsonHelper; + + // read translation files + var translations = new Dictionary<string, IDictionary<string, string>>(); + errors = new List<string>(); + DirectoryInfo translationsDir = new DirectoryInfo(folderPath); + if (translationsDir.Exists) + { + foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + { + string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); + try { - string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); - try - { - if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary<string, string> data)) - translations[locale] = data; - else - metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed.", LogLevel.Warn); - } - catch (Exception ex) + if (!jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary<string, string> data)) { - metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}", LogLevel.Warn); + errors.Add($"{file.Name} file couldn't be read"); // should never happen, since we're iterating files that exist + continue; } - } - } - // validate translations - foreach (string locale in translations.Keys.ToArray()) - { - // skip empty files - if (translations[locale] == null || !translations[locale].Keys.Any()) + translations[locale] = data; + } + catch (Exception ex) { - metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn); - translations.Remove(locale); + errors.Add($"{file.Name} file couldn't be parsed: {ex.GetLogSummary()}"); continue; } + } + } - // handle duplicates - HashSet<string> keys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); - HashSet<string> duplicateKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); - foreach (string key in translations[locale].Keys.ToArray()) + // validate translations + foreach (string locale in translations.Keys.ToArray()) + { + // handle duplicates + HashSet<string> keys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + HashSet<string> duplicateKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in translations[locale].Keys.ToArray()) + { + if (!keys.Add(key)) { - if (!keys.Add(key)) - { - duplicateKeys.Add(key); - translations[locale].Remove(key); - } + duplicateKeys.Add(key); + translations[locale].Remove(key); } - if (duplicateKeys.Any()) - metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); } - - // update translation - metadata.Translations.SetTranslations(translations); + if (duplicateKeys.Any()) + errors.Add($"{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive."); } + + return translations; } /// <summary>The method called when the user submits a core SMAPI command in the console.</summary> diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index cb1b9be5..89705352 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -83,6 +83,9 @@ namespace StardewModdingAPI.Framework /// <summary>Simplifies access to private game code.</summary> private readonly Reflector Reflection; + /// <summary>Encapsulates access to SMAPI core translations.</summary> + private readonly Translator Translator; + /// <summary>Propagates notification that SMAPI should exit.</summary> private readonly CancellationTokenSource CancellationToken; @@ -135,6 +138,7 @@ namespace StardewModdingAPI.Framework /// <param name="monitor">Encapsulates monitoring and logging for SMAPI.</param> /// <param name="monitorForGame">Encapsulates monitoring and logging on the game's behalf.</param> /// <param name="reflection">Simplifies access to private game code.</param> + /// <param name="translator">Encapsulates access to arbitrary translations.</param> /// <param name="eventManager">Manages SMAPI events for mods.</param> /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> /// <param name="modRegistry">Tracks the installed mods.</param> @@ -143,7 +147,7 @@ namespace StardewModdingAPI.Framework /// <param name="onGameExiting">A callback to invoke when the game exits.</param> /// <param name="cancellationToken">Propagates notification that SMAPI should exit.</param> /// <param name="logNetworkTraffic">Whether to log network traffic.</param> - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; @@ -161,6 +165,7 @@ namespace StardewModdingAPI.Framework this.Events = eventManager; this.ModRegistry = modRegistry; this.Reflection = reflection; + this.Translator = translator; this.DeprecationManager = deprecationManager; this.OnGameInitialized = onGameInitialized; this.OnGameExiting = onGameExiting; diff --git a/src/SMAPI/Framework/Translator.cs b/src/SMAPI/Framework/Translator.cs new file mode 100644 index 00000000..f2738633 --- /dev/null +++ b/src/SMAPI/Framework/Translator.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// <summary>Encapsulates access to arbitrary translations. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary> + internal class Translator + { + /********* + ** Fields + *********/ + /// <summary>The translations for each locale.</summary> + private readonly IDictionary<string, IDictionary<string, string>> All = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase); + + /// <summary>The translations for the current locale, with locale fallback taken into account.</summary> + private IDictionary<string, Translation> ForLocale; + + + /********* + ** Accessors + *********/ + /// <summary>The current locale.</summary> + public string Locale { get; private set; } + + /// <summary>The game's current language code.</summary> + public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public Translator() + { + this.SetLocale(string.Empty, LocalizedContentManager.LanguageCode.en); + } + + /// <summary>Set the current locale and precache translations.</summary> + /// <param name="locale">The current locale.</param> + /// <param name="localeEnum">The game's current language code.</param> + public void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) + { + this.Locale = locale.ToLower().Trim(); + this.LocaleEnum = localeEnum; + + this.ForLocale = new Dictionary<string, Translation>(StringComparer.InvariantCultureIgnoreCase); + foreach (string next in this.GetRelevantLocales(this.Locale)) + { + // skip if locale not defined + if (!this.All.TryGetValue(next, out IDictionary<string, string> translations)) + continue; + + // add missing translations + foreach (var pair in translations) + { + if (!this.ForLocale.ContainsKey(pair.Key)) + this.ForLocale.Add(pair.Key, new Translation(this.Locale, pair.Key, pair.Value)); + } + } + } + + /// <summary>Get all translations for the current locale.</summary> + public IEnumerable<Translation> GetTranslations() + { + return this.ForLocale.Values.ToArray(); + } + + /// <summary>Get a translation for the current locale.</summary> + /// <param name="key">The translation key.</param> + public Translation Get(string key) + { + this.ForLocale.TryGetValue(key, out Translation translation); + return translation ?? new Translation(this.Locale, key, null); + } + + /// <summary>Get a translation for the current locale.</summary> + /// <param name="key">The translation key.</param> + /// <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> + public Translation Get(string key, object tokens) + { + return this.Get(key).Tokens(tokens); + } + + /// <summary>Set the translations to use.</summary> + /// <param name="translations">The translations to use.</param> + internal Translator SetTranslations(IDictionary<string, IDictionary<string, string>> translations) + { + // reset translations + this.All.Clear(); + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary<string, string>(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // rebuild cache + this.SetLocale(this.Locale, this.LocaleEnum); + + return this; + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the locales which can provide translations for the given locale, in precedence order.</summary> + /// <param name="locale">The locale for which to find valid locales.</param> + private IEnumerable<string> 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"; + } + } +} diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 7c7bfc71..62002a40 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -99,6 +99,9 @@ <Link>SMAPI.metadata.json</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> + <None Update="i18n\default.json"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> <None Update="steam_appid.txt"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> diff --git a/src/SMAPI/i18n/default.json b/src/SMAPI/i18n/default.json new file mode 100644 index 00000000..0db3279e --- /dev/null +++ b/src/SMAPI/i18n/default.json @@ -0,0 +1,3 @@ +{ + +} |