diff --git a/OpenRA.Game/FluentBundle.cs b/OpenRA.Game/FluentBundle.cs index 704068330a..543c8311e5 100644 --- a/OpenRA.Game/FluentBundle.cs +++ b/OpenRA.Game/FluentBundle.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using Linguini.Bundle; using Linguini.Bundle.Builder; using Linguini.Shared.Types.Bundle; @@ -53,37 +52,30 @@ namespace OpenRA { readonly Linguini.Bundle.FluentBundle bundle; - public FluentBundle(string language, string[] paths, IReadOnlyFileSystem fileSystem) - : this(language, paths, fileSystem, error => Log.Write("debug", error.Message)) { } + public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem) + : this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { } - public FluentBundle(string language, string[] paths, IReadOnlyFileSystem fileSystem, string text) - : this(language, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { } + public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text) + : this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { } - public FluentBundle(string language, string[] paths, IReadOnlyFileSystem fileSystem, Action onError) - : this(language, paths, fileSystem, null, onError) { } + public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, Action onError) + : this(culture, paths, fileSystem, null, onError) { } - public FluentBundle(string language, string text, Action onError) - : this(language, null, null, text, onError) { } + public FluentBundle(string culture, string text, Action onError) + : this(culture, null, null, text, onError) { } - public FluentBundle(string language, string[] paths, IReadOnlyFileSystem fileSystem, string text, Action onError) + public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text, Action onError) { bundle = LinguiniBuilder.Builder() - .CultureInfo(new CultureInfo(language)) + .CultureInfo(new CultureInfo(culture)) .SkipResources() .SetUseIsolating(false) .UseConcurrent() .UncheckedBuild(); - if (paths != null && paths.Length > 0) + if (paths != null) { - // Always load english strings to provide a fallback for missing translations. - // It is important to load the english files first so the chosen language's files can override them. - var resolvedPaths = paths.Where(t => t.EndsWith("en.ftl", StringComparison.Ordinal)).ToList(); - foreach (var t in paths) - if (t.EndsWith($"{language}.ftl", StringComparison.Ordinal)) - resolvedPaths.Add(t); - - foreach (var path in resolvedPaths.Distinct()) + foreach (var path in paths) { var stream = fileSystem.Open(path); using (var reader = new StreamReader(stream)) diff --git a/OpenRA.Game/FluentProvider.cs b/OpenRA.Game/FluentProvider.cs index 29bbeb2fe4..87c3412b1a 100644 --- a/OpenRA.Game/FluentProvider.cs +++ b/OpenRA.Game/FluentProvider.cs @@ -26,7 +26,7 @@ namespace OpenRA { lock (SyncObject) { - modFluentBundle = new FluentBundle(Game.Settings.Player.Language, modData.Manifest.Translations, fileSystem); + modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, modData.Manifest.Translations, fileSystem); if (fileSystem is Map map && map.FluentMessageDefinitions != null) { var files = Array.Empty(); @@ -44,7 +44,7 @@ namespace OpenRA text = builder.ToString(); } - mapFluentBundle = new FluentBundle(Game.Settings.Player.Language, files, fileSystem, text); + mapFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text); } } } diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index ee255a92b6..0be5045f78 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -83,10 +83,13 @@ namespace OpenRA public readonly string[] SpriteFormats = Array.Empty(); public readonly string[] PackageFormats = Array.Empty(); public readonly string[] VideoFormats = Array.Empty(); - public readonly bool AllowUnusedTranslationsInExternalPackages = true; public readonly int FontSheetSize = 512; public readonly int CursorSheetSize = 512; + // TODO: This should be controlled by a user-selected translation bundle! + public readonly string FluentCulture = "en"; + public readonly bool AllowUnusedTranslationsInExternalPackages = true; + readonly string[] reservedModuleNames = { "Include", "Metadata", "FileSystem", "MapFolders", "Rules", diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index e7408ae7ce..61e320c1e8 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -143,7 +143,7 @@ namespace OpenRA text = builder.ToString(); } - FluentBundle = new FluentBundle(Game.Settings.Player.Language, files, fileSystem, text); + FluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text); } else FluentBundle = null; diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index 1ad16cb644..a586fed119 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -249,7 +249,6 @@ namespace OpenRA public Color Color = Color.FromArgb(200, 32, 32); public string LastServer = "localhost:1234"; public Color[] CustomColors = Array.Empty(); - public string Language = "en"; } public class GameSettings diff --git a/OpenRA.Game/Support/ExceptionHandler.cs b/OpenRA.Game/Support/ExceptionHandler.cs index cdffe39ff3..4c4f181abb 100644 --- a/OpenRA.Game/Support/ExceptionHandler.cs +++ b/OpenRA.Game/Support/ExceptionHandler.cs @@ -26,9 +26,6 @@ namespace OpenRA if (Game.EngineVersion != null) Log.Write("exception", $"OpenRA engine version {Game.EngineVersion}"); - if (Game.Settings != null && Game.Settings.Player != null && Game.Settings.Player.Language != null) - Log.Write("exception", $"OpenRA Language: {Game.Settings.Player.Language}"); - if (Game.ModData != null) { var manifest = Game.ModData.Manifest; diff --git a/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs b/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs index 03d7ffb937..f42c373231 100644 --- a/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs +++ b/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs @@ -30,8 +30,6 @@ namespace OpenRA.Mods.Common.Lint { sealed class CheckFluentReferences : ILintPass, ILintMapPass { - static readonly Regex FilenameRegex = new(@"(?[^\/\\]+)\.ftl$"); - void ILintMapPass.Run(Action emitError, Action emitWarning, ModData modData, Map map) { if (map.FluentMessageDefinitions == null) @@ -43,32 +41,27 @@ namespace OpenRA.Mods.Common.Lint emitWarning($"Empty key in map ftl files required by {context}"); var mapTranslations = FieldLoader.GetValue("value", map.FluentMessageDefinitions.Value); - var allModTranslations = modData.Manifest.Translations; - foreach (var language in GetModLanguages(allModTranslations)) + + // 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(allModTranslations.Concat(mapTranslations), map.Open, usedKeys, + _ => false, emitError, emitWarning); + + var modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, allModTranslations, modData.DefaultFileSystem, _ => { }); + var mapFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, mapTranslations, map, error => emitError(error.Message)); + + foreach (var group in usedKeys.KeysWithContext) { - // 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( - allModTranslations.Concat(mapTranslations), map.Open, usedKeys, - language, _ => false, emitError, emitWarning); - - 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) + if (modFluentBundle.HasMessage(group.Key)) { - if (modFluentBundle.HasMessage(group.Key)) - { - if (mapFluentBundle.HasMessage(group.Key)) - emitWarning($"Key `{group.Key}` in `{language}` language in map ftl files already exists in mod translations and will not be used."); - } - else if (!mapFluentBundle.HasMessage(group.Key)) - { - foreach (var context in group) - emitWarning($"Missing key `{group.Key}` in `{language}` language in map ftl files required by {context}"); - } + if (mapFluentBundle.HasMessage(group.Key)) + emitWarning($"Key `{group.Key}` in map ftl files already exists in mod translations and will not be used."); + } + else if (!mapFluentBundle.HasMessage(group.Key)) + { + foreach (var context in group) + emitWarning($"Missing key `{group.Key}` in map ftl files required by {context}"); } } @@ -80,34 +73,30 @@ namespace OpenRA.Mods.Common.Lint void ILintPass.Run(Action emitError, Action emitWarning, ModData modData) { + Console.WriteLine("Testing Fluent references"); var (usedKeys, testedFields) = GetUsedFluentKeysInMod(modData); foreach (var context in usedKeys.EmptyKeyContexts) emitWarning($"Empty key in mod translation files required by {context}"); var allModTranslations = modData.Manifest.Translations.ToArray(); - foreach (var language in GetModLanguages(allModTranslations)) + 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( + allModTranslations, modData.DefaultFileSystem.Open, usedKeys, + file => + !modData.Manifest.AllowUnusedTranslationsInExternalPackages || + !modData.DefaultFileSystem.IsExternalFile(file), + emitError, emitWarning); + + foreach (var group in usedKeys.KeysWithContext) { - Console.WriteLine($"Testing language: {language}"); - CheckModWidgets(modData, usedKeys, testedFields); + if (keyWithAttrs.Contains(group.Key)) + continue; - // With the fully populated keys, check keys and variables are not missing and not unused across all language files. - var keyWithAttrs = CheckKeys( - allModTranslations, modData.DefaultFileSystem.Open, usedKeys, - language, - file => - !modData.Manifest.AllowUnusedTranslationsInExternalPackages || - !modData.DefaultFileSystem.IsExternalFile(file), - emitError, emitWarning); - - foreach (var group in usedKeys.KeysWithContext) - { - if (keyWithAttrs.Contains(group.Key)) - continue; - - foreach (var context in group) - emitWarning($"Missing key `{group.Key}` in `{language}` language in mod ftl files required by {context}"); - } + foreach (var context in group) + emitWarning($"Missing key `{group.Key}` in mod ftl files required by {context}"); } // Check if we couldn't test any fields. @@ -121,14 +110,6 @@ namespace OpenRA.Mods.Common.Lint $"`{field.ReflectedType.Name}.{field.Name}` - previous warnings may be incorrect"); } - static IEnumerable GetModLanguages(IEnumerable translations) - { - return translations - .Select(filename => FilenameRegex.Match(filename).Groups["language"].Value) - .Distinct() - .OrderBy(l => l); - } - static Keys GetUsedFluentKeysInRuleset(Ruleset rules) { var usedKeys = new Keys(); @@ -427,15 +408,11 @@ namespace OpenRA.Mods.Common.Lint static HashSet CheckKeys( IEnumerable paths, Func openFile, Keys usedKeys, - string language, Func checkUnusedKeysForFile, - Action emitError, Action emitWarning) + Func checkUnusedKeysForFile, Action emitError, Action emitWarning) { var keyWithAttrs = new HashSet(); foreach (var path in paths) { - if (!path.EndsWith($"{language}.ftl", StringComparison.Ordinal)) - continue; - var stream = openFile(path); using (var reader = new StreamReader(stream)) {