diff --git a/OpenRA.Game/ExternalMods.cs b/OpenRA.Game/ExternalMods.cs
index d79ce87b52..2cb1c42cf9 100644
--- a/OpenRA.Game/ExternalMods.cs
+++ b/OpenRA.Game/ExternalMods.cs
@@ -27,7 +27,6 @@ namespace OpenRA
{
public readonly string Id;
public readonly string Version;
- public readonly string Title;
public readonly string LaunchPath;
public readonly string[] LaunchArgs;
public Sprite Icon { get; internal set; }
@@ -127,7 +126,6 @@ namespace OpenRA
{
new MiniYamlNode("Id", mod.Id),
new MiniYamlNode("Version", mod.Metadata.Version),
- new MiniYamlNode("Title", mod.Metadata.Title),
new MiniYamlNode("LaunchPath", launchPath),
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
}));
diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs
index d61023407f..e71d7c7a63 100644
--- a/OpenRA.Game/Game.cs
+++ b/OpenRA.Game/Game.cs
@@ -395,7 +395,7 @@ namespace OpenRA
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
- Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({mod.Value.Metadata.Version})");
+ Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
@@ -420,7 +420,7 @@ namespace OpenRA
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
- Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
+ Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
InitializeMod(modID, args);
}
@@ -499,8 +499,8 @@ namespace OpenRA
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
var metadata = ModData.Manifest.Metadata;
- if (!string.IsNullOrEmpty(metadata.WindowTitle))
- Renderer.Window.SetWindowTitle(metadata.WindowTitle);
+ if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
+ Renderer.Window.SetWindowTitle(metadata.WindowTitleTranslated);
PerfHistory.Items["render"].HasNormalTick = false;
PerfHistory.Items["batches"].HasNormalTick = false;
diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs
index 3b086f00f5..c3e4dbd8a8 100644
--- a/OpenRA.Game/Manifest.cs
+++ b/OpenRA.Game/Manifest.cs
@@ -45,12 +45,20 @@ namespace OpenRA
public class ModMetadata
{
- public string Title;
- public string Version;
- public string Website;
- public string WebIcon32;
- public string WindowTitle;
- public bool Hidden;
+ // FieldLoader used here, must matching naming in YAML.
+#pragma warning disable IDE1006 // Naming Styles
+ [FluentReference]
+ readonly string Title;
+ public readonly string Version;
+ public readonly string Website;
+ public readonly string WebIcon32;
+ [FluentReference]
+ readonly string WindowTitle;
+ public readonly bool Hidden;
+#pragma warning restore IDE1006 // Naming Styles
+
+ public string TitleTranslated => FluentProvider.GetString(Title);
+ public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetString(WindowTitle) : null;
}
/// Describes what is to be loaded in order to run a mod.
diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs
index 35155b26aa..200bc578bd 100644
--- a/OpenRA.Game/ModData.cs
+++ b/OpenRA.Game/ModData.cs
@@ -65,6 +65,8 @@ namespace OpenRA
Manifest.LoadCustomData(ObjectCreator);
+ FluentProvider.Initialize(this, DefaultFileSystem);
+
if (useLoadScreen)
{
LoadScreen = ObjectCreator.CreateObject(Manifest.LoadScreen.Value);
diff --git a/OpenRA.Game/Network/GameServer.cs b/OpenRA.Game/Network/GameServer.cs
index b9645cd1da..ce4fe4f3e3 100644
--- a/OpenRA.Game/Network/GameServer.cs
+++ b/OpenRA.Game/Network/GameServer.cs
@@ -182,13 +182,13 @@ namespace OpenRA.Network
if (external != null && external.Version == Version)
{
// Use external mod registration to populate the section header
- ModTitle = external.Title;
+ ModTitle = external.Id;
}
else if (Game.Mods.TryGetValue(Mod, out var mod))
{
// Use internal mod data to populate the section header, but
// on-connect switching must use the external mod plumbing.
- ModTitle = mod.Metadata.Title;
+ ModTitle = mod.Metadata.TitleTranslated;
}
else
{
@@ -199,7 +199,7 @@ namespace OpenRA.Network
.FirstOrDefault(m => m.Id == Mod);
if (guessMod != null)
- ModTitle = guessMod.Title;
+ ModTitle = guessMod.Id;
else
ModTitle = $"Unknown mod: {Mod}";
}
@@ -222,7 +222,7 @@ namespace OpenRA.Network
Map = server.Map.Uid;
Mod = manifest.Id;
Version = manifest.Metadata.Version;
- ModTitle = manifest.Metadata.Title;
+ ModTitle = manifest.Metadata.TitleTranslated;
ModWebsite = manifest.Metadata.Website;
ModIcon32 = manifest.Metadata.WebIcon32;
Protected = !string.IsNullOrEmpty(server.Settings.Password);
diff --git a/OpenRA.Game/Network/SyncReport.cs b/OpenRA.Game/Network/SyncReport.cs
index 19b18cfcf5..4c563a17b6 100644
--- a/OpenRA.Game/Network/SyncReport.cs
+++ b/OpenRA.Game/Network/SyncReport.cs
@@ -122,7 +122,7 @@ namespace OpenRA.Network
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
if (Game.IsHost)
Log.Write("sync", "Player is host.");
- Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
+ Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.TitleTranslated} at Version {mod.Version})");
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Synced Traits:");
diff --git a/OpenRA.Game/Support/ExceptionHandler.cs b/OpenRA.Game/Support/ExceptionHandler.cs
index 87d1034fc5..cdffe39ff3 100644
--- a/OpenRA.Game/Support/ExceptionHandler.cs
+++ b/OpenRA.Game/Support/ExceptionHandler.cs
@@ -31,8 +31,8 @@ namespace OpenRA
if (Game.ModData != null)
{
- var mod = Game.ModData.Manifest.Metadata;
- Log.Write("exception", $"{mod.Title} mod version {mod.Version}");
+ var manifest = Game.ModData.Manifest;
+ Log.Write("exception", $"{manifest.Id} mod version {manifest.Metadata.Version}");
}
if (Game.OrderManager != null && Game.OrderManager.World != null && Game.OrderManager.World.Map != null)
diff --git a/OpenRA.Mods.Cnc/CncLoadScreen.cs b/OpenRA.Mods.Cnc/CncLoadScreen.cs
index 2dbb3f5745..5b811d7d26 100644
--- a/OpenRA.Mods.Cnc/CncLoadScreen.cs
+++ b/OpenRA.Mods.Cnc/CncLoadScreen.cs
@@ -19,6 +19,9 @@ namespace OpenRA.Mods.Cnc
{
public sealed class CncLoadScreen : SheetLoadScreen
{
+ [FluentReference]
+ const string Loading = "loadscreen-loading";
+
int loadTick;
Sprite nodLogo, gdiLogo, evaLogo, brightBlock, dimBlock;
@@ -31,11 +34,15 @@ namespace OpenRA.Mods.Cnc
int lastDensity;
Size lastResolution;
+ string message = "";
+
public override void Init(ModData modData, Dictionary info)
{
base.Init(modData, info);
versionText = modData.Manifest.Metadata.Version;
+
+ message = FluentProvider.GetString(Loading);
}
public override void DisplayInner(Renderer r, Sheet s, int density)
@@ -89,7 +96,7 @@ namespace OpenRA.Mods.Cnc
if (r.Fonts != null)
{
var loadingFont = r.Fonts["BigBold"];
- var loadingText = Info["Text"];
+ var loadingText = message;
var loadingPos = new float2((bounds.Width - loadingFont.Measure(loadingText).X) / 2, barY);
loadingFont.DrawText(loadingText, loadingPos, Color.Gray);
diff --git a/OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs b/OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs
index 8cd69d1cd6..f20753b930 100644
--- a/OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs
+++ b/OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs
@@ -48,7 +48,12 @@ namespace OpenRA.Mods.Common.FileSystem
if (contentInstalled)
return false;
- Game.InitializeMod(content.ContentInstallerMod, new Arguments("Content.Mod=" + modData.Manifest.Id));
+ string translationPath;
+ using (var fs = (FileStream)modData.DefaultFileSystem.Open(content.Translation))
+ translationPath = fs.Name;
+ Game.InitializeMod(
+ content.ContentInstallerMod,
+ new Arguments(new[] { "Content.Mod=" + modData.Manifest.Id, "Content.TranslationFile=" + translationPath }));
return true;
}
}
diff --git a/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs b/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs
index 825132d503..599dce9a70 100644
--- a/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs
+++ b/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs
@@ -43,16 +43,17 @@ namespace OpenRA.Mods.Common.Lint
var mapTranslations = FieldLoader.GetValue("value", map.TranslationDefinitions.Value);
- foreach (var language in GetModLanguages(modData))
+ var allModTranslations = modData.Manifest.Translations.Append(modData.Manifest.Get().Translation).ToArray();
+ foreach (var language in GetModLanguages(allModTranslations))
{
// Check keys and variables are not missing across all language files.
// But for maps we don't warn on unused keys. They might be unused on *this* map,
// but the mod or another map may use them and we don't have sight of that.
CheckKeys(
- modData.Manifest.Translations.Concat(mapTranslations), map.Open, usedKeys,
+ allModTranslations.Concat(mapTranslations), map.Open, usedKeys,
language, _ => false, emitError, emitWarning);
- var modFluentBundle = new FluentBundle(language, modData.Manifest.Translations, modData.DefaultFileSystem, _ => { });
+ var modFluentBundle = new FluentBundle(language, allModTranslations, modData.DefaultFileSystem, _ => { });
var mapFluentBundle = new FluentBundle(language, mapTranslations, map, error => emitError(error.Message));
foreach (var group in usedKeys.KeysWithContext)
@@ -78,14 +79,15 @@ namespace OpenRA.Mods.Common.Lint
foreach (var context in usedKeys.EmptyKeyContexts)
emitWarning($"Empty key in mod translation files required by {context}");
- foreach (var language in GetModLanguages(modData))
+ var allModTranslations = modData.Manifest.Translations.Append(modData.Manifest.Get().Translation).ToArray();
+ foreach (var language in GetModLanguages(allModTranslations))
{
Console.WriteLine($"Testing language: {language}");
CheckModWidgets(modData, usedKeys, testedFields);
// With the fully populated keys, check keys and variables are not missing and not unused across all language files.
var keyWithAttrs = CheckKeys(
- modData.Manifest.Translations, modData.DefaultFileSystem.Open, usedKeys,
+ allModTranslations, modData.DefaultFileSystem.Open, usedKeys,
language,
file =>
!modData.Manifest.AllowUnusedTranslationsInExternalPackages ||
@@ -113,9 +115,9 @@ namespace OpenRA.Mods.Common.Lint
$"`{field.ReflectedType.Name}.{field.Name}` - previous warnings may be incorrect");
}
- static IEnumerable GetModLanguages(ModData modData)
+ static IEnumerable GetModLanguages(IEnumerable translations)
{
- return modData.Manifest.Translations
+ return translations
.Select(filename => FilenameRegex.Match(filename).Groups["language"].Value)
.Distinct()
.OrderBy(l => l);
@@ -249,51 +251,55 @@ namespace OpenRA.Mods.Common.Lint
.Where(t => t.IsSubclassOf(typeof(TraitInfo)) || t.IsSubclassOf(typeof(Warhead)))
.SelectMany(t => t.GetFields().Where(f => f.HasAttribute())));
- // HACK: Need to hardcode the custom loader for GameSpeeds.
- var gameSpeeds = modData.Manifest.Get();
- var gameSpeedNameField = typeof(GameSpeed).GetField(nameof(GameSpeed.Name));
- var gameSpeedFluentReference = Utility.GetCustomAttributes(gameSpeedNameField, true)[0];
- testedFields.Add(gameSpeedNameField);
- foreach (var speed in gameSpeeds.Speeds.Values)
- usedKeys.Add(speed.Name, gameSpeedFluentReference, $"`{nameof(GameSpeed)}.{nameof(GameSpeed.Name)}`");
+ // TODO: linter does not work with LoadUsing
+ GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ usedKeys, testedFields, Utility.GetFields(typeof(GameSpeed)), modData.Manifest.Get().Speeds.Values);
// TODO: linter does not work with LoadUsing
- foreach (var actorInfo in modData.DefaultRules.Actors)
+ GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ usedKeys, testedFields,
+ Utility.GetFields(typeof(ResourceRendererInfo.ResourceTypeInfo)),
+ modData.DefaultRules.Actors
+ .SelectMany(actorInfo => actorInfo.Value.TraitInfos())
+ .SelectMany(info => info.ResourceTypes.Values));
+
+ const BindingFlags Binding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
+ var constFields = modData.ObjectCreator.GetTypes().SelectMany(modType => modType.GetFields(Binding)).Where(f => f.IsLiteral);
+ GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ usedKeys, testedFields, constFields, new[] { (object)null });
+
+ var modMetadataFields = typeof(ModMetadata).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ usedKeys, testedFields, modMetadataFields, new[] { modData.Manifest.Metadata });
+
+ var modContent = modData.Manifest.Get();
+ GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ usedKeys, testedFields, Utility.GetFields(typeof(ModContent)), new[] { modContent });
+ GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ usedKeys, testedFields, Utility.GetFields(typeof(ModContent.ModPackage)), modContent.Packages.Values);
+
+ return (usedKeys, testedFields);
+ }
+
+ static void GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
+ Keys usedKeys, List testedFields,
+ IEnumerable newFields, IEnumerable