Skip to content

Commit bb17cfa

Browse files
RoosterDragonPunkPun
authored andcommitted
Expose mod.yaml content to localisation.
Mod metadata, load screens and mod content is all now sourced from ftl files, allowing these items to be translated. Translations are now initialized as part of ModData creation, as currently they are made available too late for the usage we need here. The "modcontent" mod learns a new parameter for "Content.TranslationFile" - this allows a mod to provide the path of a translation file to the mod which it can load. This allows mods such as ra, cnc, d2k, ts to own the translations for their ModContent, yet still make them accessible to the modcontent mod. CheckFluentReference learns to validate all these new fields to ensure translations have been set.
1 parent d1583e8 commit bb17cfa

File tree

36 files changed

+291
-143
lines changed

36 files changed

+291
-143
lines changed

OpenRA.Game/ExternalMods.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public class ExternalMod
2727
{
2828
public readonly string Id;
2929
public readonly string Version;
30-
public readonly string Title;
3130
public readonly string LaunchPath;
3231
public readonly string[] LaunchArgs;
3332
public Sprite Icon { get; internal set; }
@@ -127,7 +126,6 @@ internal void Register(Manifest mod, string launchPath, IEnumerable<string> laun
127126
{
128127
new MiniYamlNode("Id", mod.Id),
129128
new MiniYamlNode("Version", mod.Metadata.Version),
130-
new MiniYamlNode("Title", mod.Metadata.Title),
131129
new MiniYamlNode("LaunchPath", launchPath),
132130
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
133131
}));

OpenRA.Game/Game.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ static void Initialize(Arguments args)
395395
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
396396
Console.WriteLine("Internal mods:");
397397
foreach (var mod in Mods)
398-
Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({mod.Value.Metadata.Version})");
398+
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
399399

400400
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
401401

@@ -420,7 +420,7 @@ static void Initialize(Arguments args)
420420

421421
Console.WriteLine("External mods:");
422422
foreach (var mod in ExternalMods)
423-
Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
423+
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
424424

425425
InitializeMod(modID, args);
426426
}
@@ -499,8 +499,8 @@ public static void InitializeMod(string mod, Arguments args)
499499
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
500500

501501
var metadata = ModData.Manifest.Metadata;
502-
if (!string.IsNullOrEmpty(metadata.WindowTitle))
503-
Renderer.Window.SetWindowTitle(metadata.WindowTitle);
502+
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
503+
Renderer.Window.SetWindowTitle(metadata.WindowTitleTranslated);
504504

505505
PerfHistory.Items["render"].HasNormalTick = false;
506506
PerfHistory.Items["batches"].HasNormalTick = false;

OpenRA.Game/Manifest.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,20 @@ public SpriteSequenceFormat(MiniYaml yaml)
4545

4646
public class ModMetadata
4747
{
48-
public string Title;
49-
public string Version;
50-
public string Website;
51-
public string WebIcon32;
52-
public string WindowTitle;
53-
public bool Hidden;
48+
// FieldLoader used here, must matching naming in YAML.
49+
#pragma warning disable IDE1006 // Naming Styles
50+
[FluentReference]
51+
readonly string Title;
52+
public readonly string Version;
53+
public readonly string Website;
54+
public readonly string WebIcon32;
55+
[FluentReference]
56+
readonly string WindowTitle;
57+
public readonly bool Hidden;
58+
#pragma warning restore IDE1006 // Naming Styles
59+
60+
public string TitleTranslated => FluentProvider.GetString(Title);
61+
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetString(WindowTitle) : null;
5462
}
5563

5664
/// <summary>Describes what is to be loaded in order to run a mod.</summary>

OpenRA.Game/ModData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
6565

6666
Manifest.LoadCustomData(ObjectCreator);
6767

68+
FluentProvider.Initialize(this, DefaultFileSystem);
69+
6870
if (useLoadScreen)
6971
{
7072
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);

OpenRA.Game/Network/GameServer.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,13 @@ public GameServer(MiniYaml yaml)
182182
if (external != null && external.Version == Version)
183183
{
184184
// Use external mod registration to populate the section header
185-
ModTitle = external.Title;
185+
ModTitle = external.Id;
186186
}
187187
else if (Game.Mods.TryGetValue(Mod, out var mod))
188188
{
189189
// Use internal mod data to populate the section header, but
190190
// on-connect switching must use the external mod plumbing.
191-
ModTitle = mod.Metadata.Title;
191+
ModTitle = mod.Metadata.TitleTranslated;
192192
}
193193
else
194194
{
@@ -199,7 +199,7 @@ public GameServer(MiniYaml yaml)
199199
.FirstOrDefault(m => m.Id == Mod);
200200

201201
if (guessMod != null)
202-
ModTitle = guessMod.Title;
202+
ModTitle = guessMod.Id;
203203
else
204204
ModTitle = $"Unknown mod: {Mod}";
205205
}
@@ -222,7 +222,7 @@ public GameServer(Server.Server server)
222222
Map = server.Map.Uid;
223223
Mod = manifest.Id;
224224
Version = manifest.Metadata.Version;
225-
ModTitle = manifest.Metadata.Title;
225+
ModTitle = manifest.Metadata.TitleTranslated;
226226
ModWebsite = manifest.Metadata.Website;
227227
ModIcon32 = manifest.Metadata.WebIcon32;
228228
Protected = !string.IsNullOrEmpty(server.Settings.Password);

OpenRA.Game/Network/SyncReport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ internal void DumpSyncReport(int frame)
122122
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
123123
if (Game.IsHost)
124124
Log.Write("sync", "Player is host.");
125-
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
125+
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.TitleTranslated} at Version {mod.Version})");
126126
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
127127
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
128128
Log.Write("sync", "Synced Traits:");

OpenRA.Game/Support/ExceptionHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public static void HandleFatalError(Exception ex)
3131

3232
if (Game.ModData != null)
3333
{
34-
var mod = Game.ModData.Manifest.Metadata;
35-
Log.Write("exception", $"{mod.Title} mod version {mod.Version}");
34+
var manifest = Game.ModData.Manifest;
35+
Log.Write("exception", $"{manifest.Id} mod version {manifest.Metadata.Version}");
3636
}
3737

3838
if (Game.OrderManager != null && Game.OrderManager.World != null && Game.OrderManager.World.Map != null)

OpenRA.Mods.Cnc/CncLoadScreen.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ namespace OpenRA.Mods.Cnc
1919
{
2020
public sealed class CncLoadScreen : SheetLoadScreen
2121
{
22+
[FluentReference]
23+
const string Loading = "loadscreen-loading";
24+
2225
int loadTick;
2326

2427
Sprite nodLogo, gdiLogo, evaLogo, brightBlock, dimBlock;
@@ -31,11 +34,15 @@ public sealed class CncLoadScreen : SheetLoadScreen
3134
int lastDensity;
3235
Size lastResolution;
3336

37+
string message = "";
38+
3439
public override void Init(ModData modData, Dictionary<string, string> info)
3540
{
3641
base.Init(modData, info);
3742

3843
versionText = modData.Manifest.Metadata.Version;
44+
45+
message = FluentProvider.GetString(Loading);
3946
}
4047

4148
public override void DisplayInner(Renderer r, Sheet s, int density)
@@ -89,7 +96,7 @@ public override void DisplayInner(Renderer r, Sheet s, int density)
8996
if (r.Fonts != null)
9097
{
9198
var loadingFont = r.Fonts["BigBold"];
92-
var loadingText = Info["Text"];
99+
var loadingText = message;
93100
var loadingPos = new float2((bounds.Width - loadingFont.Measure(loadingText).X) / 2, barY);
94101
loadingFont.DrawText(loadingText, loadingPos, Color.Gray);
95102

OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ bool IFileSystemExternalContent.InstallContentIfRequired(ModData modData)
4848
if (contentInstalled)
4949
return false;
5050

51-
Game.InitializeMod(content.ContentInstallerMod, new Arguments("Content.Mod=" + modData.Manifest.Id));
51+
string translationPath;
52+
using (var fs = (FileStream)modData.DefaultFileSystem.Open(content.Translation))
53+
translationPath = fs.Name;
54+
Game.InitializeMod(
55+
content.ContentInstallerMod,
56+
new Arguments(new[] { "Content.Mod=" + modData.Manifest.Id, "Content.TranslationFile=" + translationPath }));
5257
return true;
5358
}
5459
}

OpenRA.Mods.Common/Lint/CheckFluentReferences.cs

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,17 @@ void ILintMapPass.Run(Action<string> emitError, Action<string> emitWarning, ModD
4343

4444
var mapTranslations = FieldLoader.GetValue<string[]>("value", map.TranslationDefinitions.Value);
4545

46-
foreach (var language in GetModLanguages(modData))
46+
var allModTranslations = modData.Manifest.Translations.Append(modData.Manifest.Get<ModContent>().Translation).ToArray();
47+
foreach (var language in GetModLanguages(allModTranslations))
4748
{
4849
// Check keys and variables are not missing across all language files.
4950
// But for maps we don't warn on unused keys. They might be unused on *this* map,
5051
// but the mod or another map may use them and we don't have sight of that.
5152
CheckKeys(
52-
modData.Manifest.Translations.Concat(mapTranslations), map.Open, usedKeys,
53+
allModTranslations.Concat(mapTranslations), map.Open, usedKeys,
5354
language, _ => false, emitError, emitWarning);
5455

55-
var modFluentBundle = new FluentBundle(language, modData.Manifest.Translations, modData.DefaultFileSystem, _ => { });
56+
var modFluentBundle = new FluentBundle(language, allModTranslations, modData.DefaultFileSystem, _ => { });
5657
var mapFluentBundle = new FluentBundle(language, mapTranslations, map, error => emitError(error.Message));
5758

5859
foreach (var group in usedKeys.KeysWithContext)
@@ -78,14 +79,15 @@ void ILintPass.Run(Action<string> emitError, Action<string> emitWarning, ModData
7879
foreach (var context in usedKeys.EmptyKeyContexts)
7980
emitWarning($"Empty key in mod translation files required by {context}");
8081

81-
foreach (var language in GetModLanguages(modData))
82+
var allModTranslations = modData.Manifest.Translations.Append(modData.Manifest.Get<ModContent>().Translation).ToArray();
83+
foreach (var language in GetModLanguages(allModTranslations))
8284
{
8385
Console.WriteLine($"Testing language: {language}");
8486
CheckModWidgets(modData, usedKeys, testedFields);
8587

8688
// With the fully populated keys, check keys and variables are not missing and not unused across all language files.
8789
var keyWithAttrs = CheckKeys(
88-
modData.Manifest.Translations, modData.DefaultFileSystem.Open, usedKeys,
90+
allModTranslations, modData.DefaultFileSystem.Open, usedKeys,
8991
language,
9092
file =>
9193
!modData.Manifest.AllowUnusedTranslationsInExternalPackages ||
@@ -113,9 +115,9 @@ void ILintPass.Run(Action<string> emitError, Action<string> emitWarning, ModData
113115
$"`{field.ReflectedType.Name}.{field.Name}` - previous warnings may be incorrect");
114116
}
115117

116-
static IEnumerable<string> GetModLanguages(ModData modData)
118+
static IEnumerable<string> GetModLanguages(IEnumerable<string> translations)
117119
{
118-
return modData.Manifest.Translations
120+
return translations
119121
.Select(filename => FilenameRegex.Match(filename).Groups["language"].Value)
120122
.Distinct()
121123
.OrderBy(l => l);
@@ -249,51 +251,55 @@ static Keys GetUsedFluentKeysInMap(Map map, Action<string> emitWarning)
249251
.Where(t => t.IsSubclassOf(typeof(TraitInfo)) || t.IsSubclassOf(typeof(Warhead)))
250252
.SelectMany(t => t.GetFields().Where(f => f.HasAttribute<FluentReferenceAttribute>())));
251253

252-
// HACK: Need to hardcode the custom loader for GameSpeeds.
253-
var gameSpeeds = modData.Manifest.Get<GameSpeeds>();
254-
var gameSpeedNameField = typeof(GameSpeed).GetField(nameof(GameSpeed.Name));
255-
var gameSpeedFluentReference = Utility.GetCustomAttributes<FluentReferenceAttribute>(gameSpeedNameField, true)[0];
256-
testedFields.Add(gameSpeedNameField);
257-
foreach (var speed in gameSpeeds.Speeds.Values)
258-
usedKeys.Add(speed.Name, gameSpeedFluentReference, $"`{nameof(GameSpeed)}.{nameof(GameSpeed.Name)}`");
254+
// TODO: linter does not work with LoadUsing
255+
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
256+
usedKeys, testedFields, Utility.GetFields(typeof(GameSpeed)), modData.Manifest.Get<GameSpeeds>().Speeds.Values);
259257

260258
// TODO: linter does not work with LoadUsing
261-
foreach (var actorInfo in modData.DefaultRules.Actors)
262-
{
263-
foreach (var info in actorInfo.Value.TraitInfos<ResourceRendererInfo>())
264-
{
265-
var resourceTypeNameField = typeof(ResourceRendererInfo.ResourceTypeInfo).GetField(nameof(ResourceRendererInfo.ResourceTypeInfo.Name));
266-
var resourceTypeFluentReference = Utility.GetCustomAttributes<FluentReferenceAttribute>(resourceTypeNameField, true)[0];
267-
testedFields.Add(resourceTypeNameField);
268-
foreach (var resourceTypes in info.ResourceTypes)
269-
usedKeys.Add(
270-
resourceTypes.Value.Name,
271-
resourceTypeFluentReference,
272-
$"`{nameof(ResourceRendererInfo.ResourceTypeInfo)}.{nameof(ResourceRendererInfo.ResourceTypeInfo.Name)}`");
273-
}
274-
}
259+
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
260+
usedKeys, testedFields,
261+
Utility.GetFields(typeof(ResourceRendererInfo.ResourceTypeInfo)),
262+
modData.DefaultRules.Actors
263+
.SelectMany(actorInfo => actorInfo.Value.TraitInfos<ResourceRendererInfo>())
264+
.SelectMany(info => info.ResourceTypes.Values));
265+
266+
const BindingFlags Binding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
267+
var constFields = modData.ObjectCreator.GetTypes().SelectMany(modType => modType.GetFields(Binding)).Where(f => f.IsLiteral);
268+
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
269+
usedKeys, testedFields, constFields, new[] { (object)null });
270+
271+
var modMetadataFields = typeof(ModMetadata).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
272+
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
273+
usedKeys, testedFields, modMetadataFields, new[] { modData.Manifest.Metadata });
274+
275+
var modContent = modData.Manifest.Get<ModContent>();
276+
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
277+
usedKeys, testedFields, Utility.GetFields(typeof(ModContent)), new[] { modContent });
278+
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
279+
usedKeys, testedFields, Utility.GetFields(typeof(ModContent.ModPackage)), modContent.Packages.Values);
280+
281+
return (usedKeys, testedFields);
282+
}
275283

276-
foreach (var modType in modData.ObjectCreator.GetTypes())
284+
static void GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
285+
Keys usedKeys, List<FieldInfo> testedFields,
286+
IEnumerable<FieldInfo> newFields, IEnumerable<object> objects)
287+
{
288+
var fieldsWithAttribute =
289+
newFields
290+
.Select(f => (Field: f, FluentReference: Utility.GetCustomAttributes<FluentReferenceAttribute>(f, true).SingleOrDefault()))
291+
.Where(x => x.FluentReference != null)
292+
.ToArray();
293+
testedFields.AddRange(fieldsWithAttribute.Select(x => x.Field));
294+
foreach (var obj in objects)
277295
{
278-
const BindingFlags Binding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
279-
foreach (var field in modType.GetFields(Binding))
296+
foreach (var (field, fluentReference) in fieldsWithAttribute)
280297
{
281-
// Checking for constant string fields.
282-
if (!field.IsLiteral)
283-
continue;
284-
285-
var fluentReference = Utility.GetCustomAttributes<FluentReferenceAttribute>(field, true).SingleOrDefault();
286-
if (fluentReference == null)
287-
continue;
288-
289-
testedFields.Add(field);
290-
var keys = LintExts.GetFieldValues(null, field, fluentReference.DictionaryReference);
298+
var keys = LintExts.GetFieldValues(obj, field, fluentReference.DictionaryReference);
291299
foreach (var key in keys)
292300
usedKeys.Add(key, fluentReference, $"`{field.ReflectedType.Name}.{field.Name}`");
293301
}
294302
}
295-
296-
return (usedKeys, testedFields);
297303
}
298304

299305
static void CheckModWidgets(ModData modData, Keys usedKeys, List<FieldInfo> testedFields)

0 commit comments

Comments
 (0)