summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets5
-rw-r--r--build/prepare-install-package.targets4
-rw-r--r--docs/README.md20
-rw-r--r--docs/release-notes.md5
-rw-r--r--src/SMAPI/Framework/ModHelpers/TranslationHelper.cs78
-rw-r--r--src/SMAPI/Framework/SCore.cs127
-rw-r--r--src/SMAPI/Framework/SGame.cs7
-rw-r--r--src/SMAPI/Framework/Translator.cs128
-rw-r--r--src/SMAPI/SMAPI.csproj3
-rw-r--r--src/SMAPI/i18n/default.json3
10 files changed, 265 insertions, 115 deletions
diff --git a/build/common.targets b/build/common.targets
index eb92cddd..10cdbe2c 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -21,6 +21,10 @@
<CallTarget Targets="CopySMAPI;CopyDefaultMods" />
</Target>
<Target Name="CopySMAPI" Condition="'$(MSBuildProjectName)' == 'SMAPI'">
+ <ItemGroup>
+ <TranslationFiles Include="$(TargetDir)\i18n\*.json" />
+ </ItemGroup>
+
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
@@ -29,6 +33,7 @@
<Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)\smapi-internal" />
+ <Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
</Target>
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets
index 18a69a24..e5286bf5 100644
--- a/build/prepare-install-package.targets
+++ b/build/prepare-install-package.targets
@@ -17,6 +17,9 @@
<PlatformName>windows</PlatformName>
<PlatformName Condition="$(OS) != 'Windows_NT'">unix</PlatformName>
</PropertyGroup>
+ <ItemGroup>
+ <TranslationFiles Include="$(CompiledSmapiPath)\i18n\*.json" />
+ </ItemGroup>
<!-- reset package directory -->
<RemoveDir Directories="$(PackagePath)" />
@@ -46,6 +49,7 @@
<Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
+ <Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(PackagePath)\bundle\smapi-internal\i18n" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(TargetDir)\unix-launcher.sh" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
diff --git a/docs/README.md b/docs/README.md
index 90204387..fdb60693 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -54,3 +54,23 @@ developers and other modders!
### For SMAPI developers
* [Technical docs](technical/smapi.md)
+
+## Translating SMAPI
+SMAPI rarely shows text in-game, so it only has a few translations. Contributions are welcome! See
+[Modding:Translations](https://stardewvalleywiki.com/Modding:Translations) on the wiki for help
+contributing translations.
+
+locale | status
+---------- | :----------------
+default | ✓ [fully translated](../src/SMAPI/i18n/default.json)
+Chinese | ❑ not translated
+French | ❑ not translated
+German | ❑ not translated
+Hungarian | ❑ not translated
+Italian | ❑ not translated
+Japanese | ❑ not translated
+Korean | ❑ not translated
+Portuguese | ❑ not translated
+Russian | ❑ not translated
+Spanish | ❑ not translated
+Turkish | ❑ not translated
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 26645210..5d8253b4 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -113,6 +113,11 @@ For modders:
* Fixed changes to `Data\NPCDispositions` not always propagated correctly to existing NPCs.
* Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save.
+### For SMAPI maintainers
+* Added support for core translation files.
+* Migrated to new `.csproj` format.
+* Internal refactoring.
+
## 2.11.3
Released 13 September 2019 for Stardew Valley 1.3.36.
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> &lt; <c>pt.json</c> &lt; <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 @@
+{
+
+}