diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index bbdf7d93bc..8a4a5b274b 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -408,6 +408,7 @@ namespace OpenRA Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Title, mod.Value.Version); InitializeMod(modID, args); + Ui.InitializeTranslation(); } public static void InitializeMod(string mod, Arguments args) diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index c247281500..7eb89b720f 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -72,7 +72,7 @@ namespace OpenRA public readonly string[] Rules, ServerTraits, Sequences, ModelSequences, Cursors, Chrome, Assemblies, ChromeLayout, - Weapons, Voices, Notifications, Music, TileSets, + Weapons, Voices, Notifications, Music, Translations, TileSets, ChromeMetrics, MapCompatibility, Missions, Hotkeys; public readonly IReadOnlyDictionary Packages; @@ -141,6 +141,7 @@ namespace OpenRA Voices = YamlList(yaml, "Voices"); Notifications = YamlList(yaml, "Notifications"); Music = YamlList(yaml, "Music"); + Translations = YamlList(yaml, "Translations"); TileSets = YamlList(yaml, "TileSets"); ChromeMetrics = YamlList(yaml, "ChromeMetrics"); Missions = YamlList(yaml, "Missions"); diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index 0f75bcc6c8..baad403c54 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -21,6 +21,7 @@ using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Support; using OpenRA.Traits; +using OpenRA.Widgets; namespace OpenRA { @@ -164,6 +165,7 @@ namespace OpenRA new MapField("Bounds"), new MapField("Visibility"), new MapField("Categories"), + new MapField("Translations", required: false), new MapField("LockPreview", required: false, ignoreIfValue: "False"), new MapField("Players", "PlayerDefinitions"), new MapField("Actors", "ActorDefinitions"), @@ -189,6 +191,7 @@ namespace OpenRA public Rectangle Bounds; public MapVisibility Visibility = MapVisibility.Lobby; public string[] Categories = { "Conquest" }; + public string[] Translations; public int2 MapSize { get; private set; } @@ -246,6 +249,8 @@ namespace OpenRA CellLayer> inverseCellProjection; CellLayer projectedHeight; + internal Translation Translation; + public static string ComputeUID(IReadOnlyPackage package) { // UID is calculated by taking an SHA1 of the yaml and binary data @@ -418,6 +423,8 @@ namespace OpenRA Rules.Sequences.Preload(); + Translation = new Translation(Game.Settings.Player.Language, Translations, this); + var tl = new MPos(0, 0).ToCPos(this); var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this); AllCells = new CellRegion(Grid.Type, tl, br); @@ -1307,5 +1314,13 @@ namespace OpenRA return false; } + + public string Translate(string key, IDictionary args = null, string attribute = null) + { + if (Translation.GetFormattedMessage(key, args, attribute) == key) + return Ui.Translate(key, args, attribute); + + return Translation.GetFormattedMessage(key, args, attribute); + } } } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index b8cbe7fb8d..d6ab0975fd 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -11,6 +11,7 @@ + diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index 854723c8e4..5fd32b3e53 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -221,6 +221,7 @@ namespace OpenRA public Color Color = Color.FromArgb(200, 32, 32); public string LastServer = "localhost:1234"; public Color[] CustomColors = { }; + public string Language = "en"; } public class GameSettings diff --git a/OpenRA.Game/Translation.cs b/OpenRA.Game/Translation.cs new file mode 100644 index 0000000000..7dffa411ea --- /dev/null +++ b/OpenRA.Game/Translation.cs @@ -0,0 +1,121 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Fluent.Net; +using Fluent.Net.RuntimeAst; +using OpenRA.FileSystem; + +namespace OpenRA +{ + public class Translation + { + readonly IEnumerable messageContexts; + + public Translation(string language, string[] translations, IReadOnlyFileSystem fileSystem) + { + if (translations == null || !translations.Any()) + return; + + messageContexts = GetMessageContext(language, translations, fileSystem).ToList(); + } + + IEnumerable GetMessageContext(string language, string[] translations, IReadOnlyFileSystem fileSystem) + { + var backfall = translations.Where(t => t.EndsWith("en.ftl")); + var paths = translations.Where(t => t.EndsWith(language + ".ftl")); + foreach (var path in paths.Concat(backfall)) + { + var stream = fileSystem.Open(path); + using (var reader = new StreamReader(stream)) + { + var options = new MessageContextOptions { UseIsolating = false }; + var messageContext = new MessageContext(language, options); + var errors = messageContext.AddMessages(reader); + foreach (var error in errors) + Log.Write("debug", error.ToString()); + + yield return messageContext; + } + } + } + + public string GetFormattedMessage(string key, IDictionary args = null, string attribute = null) + { + if (key == null) + return ""; + + foreach (var messageContext in messageContexts) + { + var message = messageContext.GetMessage(key); + if (message != null) + { + if (string.IsNullOrEmpty(attribute)) + return messageContext.Format(message, args); + else + return messageContext.Format(message.Attributes[attribute], args); + } + } + + return key; + } + + public string GetAttribute(string key, string attribute) + { + if (key == null) + return ""; + + foreach (var messageContext in messageContexts) + { + var message = messageContext.GetMessage(key); + if (message != null && message.Attributes != null && message.Attributes.ContainsKey(attribute)) + { + var node = message.Attributes[attribute]; + var stringLiteral = (StringLiteral)node; + return stringLiteral.Value; + } + } + + return ""; + } + + // Adapted from Fluent.Net.SimpleExample.TranslationService by Mark Weaver + public static Dictionary Arguments(string name, object value, params object[] args) + { + if (args.Length % 2 != 0) + throw new ArgumentException("Expected a comma separated list of name, value arguments" + + "but the number of arguments is not a multiple of two", nameof(args)); + + var argumentDictionary = new Dictionary { { name, value } }; + + for (var i = 0; i < args.Length; i += 2) + { + name = args[i] as string; + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Expected the argument at index {0} to be a non-empty string".F(i), + nameof(args)); + } + + value = args[i + 1]; + if (value == null) + throw new ArgumentNullException("args", "Expected the argument at index {0} to be a non-null value".F(i + 1)); + + argumentDictionary.Add(name, value); + } + + return argumentDictionary; + } + } +} diff --git a/OpenRA.Game/Widgets/Widget.cs b/OpenRA.Game/Widgets/Widget.cs index bbbc4f4706..e1e301e734 100644 --- a/OpenRA.Game/Widgets/Widget.cs +++ b/OpenRA.Game/Widgets/Widget.cs @@ -32,6 +32,8 @@ namespace OpenRA.Widgets public static Widget KeyboardFocusWidget; public static Widget MouseOverWidget; + internal static Translation Translation; + public static void CloseWindow() { if (WindowList.Count > 0) @@ -155,6 +157,27 @@ namespace OpenRA.Widgets HandleInput(new MouseInput(MouseInputEvent.Move, MouseButton.None, Viewport.LastMousePos, int2.Zero, Modifiers.None, 0)); } + + public static void InitializeTranslation() + { + Translation = new Translation(Game.Settings.Player.Language, Game.ModData.Manifest.Translations, Game.ModData.DefaultFileSystem); + } + + public static string Translate(string key, IDictionary args = null, string attribute = null) + { + if (Translation == null) + return null; + + return Translation.GetFormattedMessage(key, args, attribute); + } + + public static string TranslationAttribute(string key, string attribute = null) + { + if (Translation == null) + return null; + + return Translation.GetAttribute(key, attribute); + } } public class ChromeLogic : IDisposable diff --git a/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs index cd4277da80..09b526bf6e 100644 --- a/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/UserInterfaceGlobal.cs @@ -31,5 +31,10 @@ namespace OpenRA.Mods.Common.Scripting.Global var c = color.HasValue ? color.Value : Color.White; luaLabel.GetColor = () => c; } + + public string Translate(string text) + { + return Context.World.Map.Translate(text); + } } }