diff --git a/OpenRA.FileFormats/FieldLoader.cs b/OpenRA.FileFormats/FieldLoader.cs index 6698a60d89..0c4a56b27c 100755 --- a/OpenRA.FileFormats/FieldLoader.cs +++ b/OpenRA.FileFormats/FieldLoader.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2013 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. For more information, @@ -14,6 +14,7 @@ using System.Drawing; using System.Globalization; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; namespace OpenRA.FileFormats { @@ -38,31 +39,31 @@ namespace OpenRA.FileFormats object val; if (kv.Value != null) val = kv.Value(kv.Key.Name, kv.Key.FieldType, my); - else if (!TryGetValueFromYaml(kv.Key.Name, kv.Key.FieldType, my, out val)) + else if (!TryGetValueFromYaml(kv.Key, my, out val)) continue; kv.Key.SetValue(self, val); } } - static bool TryGetValueFromYaml(string fieldName, Type fieldType, MiniYaml yaml, out object ret) + static bool TryGetValueFromYaml(FieldInfo field, MiniYaml yaml, out object ret) { ret = null; - var n = yaml.Nodes.Where(x => x.Key == fieldName).ToList(); + var n = yaml.Nodes.Where(x => x.Key == field.Name).ToList(); if (n.Count == 0) return false; if (n.Count == 1 && n[0].Value.Nodes.Count == 0) { - ret = GetValue(fieldName, fieldType, n[0].Value.Value); + ret = GetValue(field.Name, field.FieldType, n[0].Value.Value, field); return true; } else if (n.Count > 1) { throw new InvalidOperationException("The field {0} has multiple definitions:\n{1}" - .F(fieldName, n.Select(m => "\t- " + m.Location).JoinWith("\n"))); + .F(field.Name, n.Select(m => "\t- " + m.Location).JoinWith("\n"))); } - throw new InvalidOperationException("TryGetValueFromYaml: unable to load field {0} (of type {1})".F(fieldName, fieldType)); + throw new InvalidOperationException("TryGetValueFromYaml: unable to load field {0} (of type {1})".F(field.Name, field.FieldType)); } public static T Load(MiniYaml y) where T : new() @@ -80,7 +81,7 @@ namespace OpenRA.FileFormats if (field != null) { if (!field.HasAttribute()) - field.SetValue(self, GetValue(field.Name, field.FieldType, value)); + field.SetValue(self, GetValue(field.Name, field.FieldType, value, field)); return; } @@ -89,7 +90,7 @@ namespace OpenRA.FileFormats if (prop != null) { if (!prop.HasAttribute()) - prop.SetValue(self, GetValue(prop.Name, prop.PropertyType, value), NoIndexes); + prop.SetValue(self, GetValue(prop.Name, prop.PropertyType, value, prop), NoIndexes); return; } @@ -98,61 +99,70 @@ namespace OpenRA.FileFormats public static T GetValue(string field, string value) { - return (T)GetValue(field, typeof(T), value); + return (T)GetValue(field, typeof(T), value, null); } - public static object GetValue(string field, Type fieldType, string x) + public static object GetValue(string fieldName, Type fieldType, string value) { - if (x != null) x = x.Trim(); + return GetValue(fieldName, fieldType, value, null); + } + + public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field) + { + if (value != null) value = value.Trim(); if (fieldType == typeof(int)) { int res; - if (int.TryParse(x, out res)) + if (int.TryParse(value, out res)) return res; - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(ushort)) { ushort res; - if (ushort.TryParse(x, out res)) + if (ushort.TryParse(value, out res)) return res; - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(float)) { float res; - if (float.TryParse(x.Replace("%", ""), NumberStyles.Any, NumberFormatInfo.InvariantInfo, out res)) - return res * (x.Contains('%') ? 0.01f : 1f); - return InvalidValueAction(x, fieldType, field); + if (float.TryParse(value.Replace("%", ""), NumberStyles.Any, NumberFormatInfo.InvariantInfo, out res)) + return res * (value.Contains('%') ? 0.01f : 1f); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(decimal)) { decimal res; - if (decimal.TryParse(x.Replace("%", ""), NumberStyles.Any, NumberFormatInfo.InvariantInfo, out res)) - return res * (x.Contains('%') ? 0.01m : 1m); - return InvalidValueAction(x, fieldType, field); + if (decimal.TryParse(value.Replace("%", ""), NumberStyles.Any, NumberFormatInfo.InvariantInfo, out res)) + return res * (value.Contains('%') ? 0.01m : 1m); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(string)) - return x; + { + if (field != null && field.HasAttribute()) + return Regex.Replace(value, "@[^@]+@", m => Translate(m.Value.Substring(1, m.Value.Length - 2)), RegexOptions.Compiled); + return value; + } else if (fieldType == typeof(Color)) { - var parts = x.Split(','); + var parts = value.Split(','); if (parts.Length == 3) return Color.FromArgb(int.Parse(parts[0]).Clamp(0, 255), int.Parse(parts[1]).Clamp(0, 255), int.Parse(parts[2]).Clamp(0, 255)); if (parts.Length == 4) return Color.FromArgb(int.Parse(parts[0]).Clamp(0, 255), int.Parse(parts[1]).Clamp(0, 255), int.Parse(parts[2]).Clamp(0, 255), int.Parse(parts[3]).Clamp(0, 255)); - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(HSLColor)) { - var parts = x.Split(','); + var parts = value.Split(','); // Allow old ColorRamp format to be parsed as HSLColor if (parts.Length == 3 || parts.Length == 4) @@ -161,21 +171,21 @@ namespace OpenRA.FileFormats (byte)int.Parse(parts[1]).Clamp(0, 255), (byte)int.Parse(parts[2]).Clamp(0, 255)); - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WRange)) { WRange res; - if (WRange.TryParse(x, out res)) + if (WRange.TryParse(value, out res)) return res; - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WVec)) { - var parts = x.Split(','); + var parts = value.Split(','); if (parts.Length == 3) { WRange rx, ry, rz; @@ -183,12 +193,12 @@ namespace OpenRA.FileFormats return new WVec(rx, ry, rz); } - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WPos)) { - var parts = x.Split(','); + var parts = value.Split(','); if (parts.Length == 3) { WRange rx, ry, rz; @@ -196,62 +206,62 @@ namespace OpenRA.FileFormats return new WPos(rx, ry, rz); } - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WAngle)) { int res; - if (int.TryParse(x, out res)) + if (int.TryParse(value, out res)) return new WAngle(res); - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType == typeof(WRot)) { - var parts = x.Split(','); + var parts = value.Split(','); if (parts.Length == 3) { int rr, rp, ry; - if (int.TryParse(x, out rr) && int.TryParse(x, out rp) && int.TryParse(x, out ry)) + if (int.TryParse(value, out rr) && int.TryParse(value, out rp) && int.TryParse(value, out ry)) return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry)); } - return InvalidValueAction(x, fieldType, field); + return InvalidValueAction(value, fieldType, fieldName); } else if (fieldType.IsEnum) { - if (!Enum.GetNames(fieldType).Select(a => a.ToLower()).Contains(x.ToLower())) - return InvalidValueAction(x, fieldType, field); - return Enum.Parse(fieldType, x, true); + if (!Enum.GetNames(fieldType).Select(a => a.ToLower()).Contains(value.ToLower())) + return InvalidValueAction(value, fieldType, fieldName); + return Enum.Parse(fieldType, value, true); } else if (fieldType == typeof(bool)) - return ParseYesNo(x, fieldType, field); + return ParseYesNo(value, fieldType, fieldName); else if (fieldType.IsArray) { - if (x == null) + if (value == null) return Array.CreateInstance(fieldType.GetElementType(), 0); - var parts = x.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length); for (int i = 0; i < parts.Length; i++) - ret.SetValue(GetValue(field, fieldType.GetElementType(), parts[i].Trim()), i); + ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i].Trim(), field), i); return ret; } else if (fieldType == typeof(int2)) { - var parts = x.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new int2(int.Parse(parts[0]), int.Parse(parts[1])); } else if (fieldType == typeof(float2)) { - var parts = x.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); float xx = 0; float yy = 0; float res; @@ -264,13 +274,13 @@ namespace OpenRA.FileFormats else if (fieldType == typeof(Rectangle)) { - var parts = x.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new Rectangle(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3])); } else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Bits<>)) { - var parts = x.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var argTypes = new Type[] { typeof(string[]) }; var argValues = new object[] { parts }; return fieldType.GetConstructor(argTypes).Invoke(argValues); @@ -279,11 +289,11 @@ namespace OpenRA.FileFormats else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var innerType = fieldType.GetGenericArguments().First(); - var innerValue = GetValue("Nullable", innerType, x); - return fieldType.GetConstructor(new []{ innerType }).Invoke(new []{ innerValue }); + var innerValue = GetValue("Nullable", innerType, value, field); + return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue }); } - UnknownFieldAction("[Type] {0}".F(x), fieldType); + UnknownFieldAction("[Type] {0}".F(value), fieldType); return null; } @@ -312,7 +322,7 @@ namespace OpenRA.FileFormats if (loadUsing.Length != 0) ret[field] = (_1, fieldType, yaml) => loadUsing[0].LoaderFunc(field)(yaml); else if (fromYamlKey.Length != 0) - ret[field] = (f, ft, yaml) => GetValue(f, ft, yaml.Value); + ret[field] = (f, ft, yaml) => GetValue(f, ft, yaml.Value, field); else if (ignore.Length == 0) ret[field] = null; } @@ -342,81 +352,24 @@ namespace OpenRA.FileFormats return loaderFuncCache; } } + + public static string Translate(string key) + { + if (Translations == null || string.IsNullOrEmpty(key)) + return key; + + string value; + if (!Translations.TryGetValue(key, out value)) + return key; + + return value; + } + + public static Dictionary Translations = new Dictionary(); } - public static class FieldSaver - { - public static MiniYaml Save(object o) - { - var nodes = new List(); - string root = null; - - foreach (var f in o.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)) - { - if (f.HasAttribute()) - root = FormatValue(o, f); - else - nodes.Add(new MiniYamlNode(f.Name, FormatValue(o, f))); - } - - return new MiniYaml(root, nodes); - } - - public static MiniYaml SaveDifferences(object o, object from) - { - if (o.GetType() != from.GetType()) - throw new InvalidOperationException("FieldLoader: can't diff objects of different types"); - - var fields = o.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance) - .Where(f => FormatValue(o, f) != FormatValue(from, f)); - - return new MiniYaml(null, fields.Select(f => new MiniYamlNode(f.Name, FormatValue(o, f))).ToList()); - } - - public static MiniYamlNode SaveField(object o, string field) - { - return new MiniYamlNode(field, FieldSaver.FormatValue(o, o.GetType().GetField(field))); - } - - public static string FormatValue(object v, Type t) - { - if (v == null) - return ""; - - // Color.ToString() does the wrong thing; force it to format as an array - if (t == typeof(Color)) - { - var c = (Color)v; - return "{0},{1},{2},{3}".F(((int)c.A).Clamp(0, 255), - ((int)c.R).Clamp(0, 255), - ((int)c.G).Clamp(0, 255), - ((int)c.B).Clamp(0, 255)); - } - - // Don't save floats in settings.yaml using country-specific decimal separators which can be misunderstood as group seperators. - if (t == typeof(float)) - return ((float)v).ToString(CultureInfo.InvariantCulture); - - if (t == typeof(Rectangle)) - { - var r = (Rectangle)v; - return "{0},{1},{2},{3}".F(r.X, r.Y, r.Width, r.Height); - } - - if (t.IsArray) - { - var elems = ((Array)v).OfType(); - return elems.JoinWith(","); - } - - return v.ToString(); - } - - public static string FormatValue(object o, FieldInfo f) - { - return FormatValue(f.GetValue(o), f.FieldType); - } - } + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class TranslateAttribute : Attribute { } public class FieldFromYamlKeyAttribute : Attribute { } diff --git a/OpenRA.FileFormats/FieldSaver.cs b/OpenRA.FileFormats/FieldSaver.cs new file mode 100644 index 0000000000..65208da2b5 --- /dev/null +++ b/OpenRA.FileFormats/FieldSaver.cs @@ -0,0 +1,93 @@ +#region Copyright & License Information +/* + * Copyright 2007-2013 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace OpenRA.FileFormats +{ + public static class FieldSaver + { + public static MiniYaml Save(object o) + { + var nodes = new List(); + string root = null; + + foreach (var f in o.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (f.HasAttribute()) + root = FormatValue(o, f); + else + nodes.Add(new MiniYamlNode(f.Name, FormatValue(o, f))); + } + + return new MiniYaml(root, nodes); + } + + public static MiniYaml SaveDifferences(object o, object from) + { + if (o.GetType() != from.GetType()) + throw new InvalidOperationException("FieldLoader: can't diff objects of different types"); + + var fields = o.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance) + .Where(f => FormatValue(o, f) != FormatValue(from, f)); + + return new MiniYaml(null, fields.Select(f => new MiniYamlNode(f.Name, FormatValue(o, f))).ToList()); + } + + public static MiniYamlNode SaveField(object o, string field) + { + return new MiniYamlNode(field, FieldSaver.FormatValue(o, o.GetType().GetField(field))); + } + + public static string FormatValue(object v, Type t) + { + if (v == null) + return ""; + + // Color.ToString() does the wrong thing; force it to format as an array + if (t == typeof(Color)) + { + var c = (Color)v; + return "{0},{1},{2},{3}".F(((int)c.A).Clamp(0, 255), + ((int)c.R).Clamp(0, 255), + ((int)c.G).Clamp(0, 255), + ((int)c.B).Clamp(0, 255)); + } + + // Don't save floats in settings.yaml using country-specific decimal separators which can be misunderstood as group seperators. + if (t == typeof(float)) + return ((float)v).ToString(CultureInfo.InvariantCulture); + + if (t == typeof(Rectangle)) + { + var r = (Rectangle)v; + return "{0},{1},{2},{3}".F(r.X, r.Y, r.Width, r.Height); + } + + if (t.IsArray) + { + var elems = ((Array)v).OfType(); + return elems.JoinWith(","); + } + + return v.ToString(); + } + + public static string FormatValue(object o, FieldInfo f) + { + return FormatValue(f.GetValue(o), f.FieldType); + } + } +} diff --git a/OpenRA.FileFormats/Manifest.cs b/OpenRA.FileFormats/Manifest.cs index 363357e333..63ec0b0c38 100644 --- a/OpenRA.FileFormats/Manifest.cs +++ b/OpenRA.FileFormats/Manifest.cs @@ -21,7 +21,7 @@ namespace OpenRA.FileFormats public readonly string[] Mods, Folders, Rules, ServerTraits, Sequences, VoxelSequences, Cursors, Chrome, Assemblies, ChromeLayout, - Weapons, Voices, Notifications, Music, Movies, TileSets, + Weapons, Voices, Notifications, Music, Movies, Translations, TileSets, ChromeMetrics, PackageContents; public readonly Dictionary Packages; @@ -53,6 +53,7 @@ namespace OpenRA.FileFormats Notifications = YamlList(yaml, "Notifications"); Music = YamlList(yaml, "Music"); Movies = YamlList(yaml, "Movies"); + Translations = YamlList(yaml, "Translations"); TileSets = YamlList(yaml, "TileSets"); ChromeMetrics = YamlList(yaml, "ChromeMetrics"); PackageContents = YamlList(yaml, "PackageContents"); diff --git a/OpenRA.FileFormats/OpenRA.FileFormats.csproj b/OpenRA.FileFormats/OpenRA.FileFormats.csproj index 95e2f659fa..09c4962242 100644 --- a/OpenRA.FileFormats/OpenRA.FileFormats.csproj +++ b/OpenRA.FileFormats/OpenRA.FileFormats.csproj @@ -80,6 +80,7 @@ + diff --git a/OpenRA.Game/GameRules/Settings.cs b/OpenRA.Game/GameRules/Settings.cs index b1313777a8..0d9f81578c 100644 --- a/OpenRA.Game/GameRules/Settings.cs +++ b/OpenRA.Game/GameRules/Settings.cs @@ -98,6 +98,9 @@ namespace OpenRA.GameRules public int BatchSize = 8192; public int NumTempBuffers = 8; public int SheetSize = 2048; + + public string Language = "english"; + public string DefaultLanguage = "english"; } public class SoundSettings diff --git a/OpenRA.Game/Map.cs b/OpenRA.Game/Map.cs index ca92bbe7bf..fd28aec8c0 100644 --- a/OpenRA.Game/Map.cs +++ b/OpenRA.Game/Map.cs @@ -99,6 +99,7 @@ namespace OpenRA [FieldLoader.Ignore] public List Weapons = new List(); [FieldLoader.Ignore] public List Voices = new List(); [FieldLoader.Ignore] public List Notifications = new List(); + [FieldLoader.Ignore] public List Translations = new List(); // Binary map data [FieldLoader.Ignore] public byte TileFormat = 1; @@ -191,6 +192,7 @@ namespace OpenRA Weapons = MiniYaml.NodesOrEmpty(yaml, "Weapons"); Voices = MiniYaml.NodesOrEmpty(yaml, "Voices"); Notifications = MiniYaml.NodesOrEmpty(yaml, "Notifications"); + Translations = MiniYaml.NodesOrEmpty(yaml, "Translations"); CustomTerrain = new string[MapSize.X, MapSize.Y]; @@ -247,6 +249,7 @@ namespace OpenRA root.Add(new MiniYamlNode("Weapons", null, Weapons)); root.Add(new MiniYamlNode("Voices", null, Voices)); root.Add(new MiniYamlNode("Notifications", null, Notifications)); + root.Add(new MiniYamlNode("Translations", null, Translations)); var entries = new Dictionary(); entries.Add("map.bin", SaveBinaryData()); diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index 7af68550f8..138d75978d 100755 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -44,6 +44,7 @@ namespace OpenRA public ModData(params string[] mods) { + Languages = new string[0]; Manifest = new Manifest(mods); ObjectCreator = new ObjectCreator(Manifest); LoadScreen = ObjectCreator.CreateObject(Manifest.LoadScreen.Value); @@ -71,6 +72,47 @@ namespace OpenRA CursorProvider.Initialize(Manifest.Cursors); } + public IEnumerable Languages { get; private set; } + + void LoadTranslations(Map map) + { + var selectedTranslations = new Dictionary(); + var defaultTranslations = new Dictionary(); + + if (!Manifest.Translations.Any()) + { + Languages = new string[0]; + FieldLoader.Translations = new Dictionary(); + return; + } + + var yaml = Manifest.Translations.Select(MiniYaml.FromFile).Aggregate(MiniYaml.MergeLiberal); + Languages = yaml.Select(t => t.Key).ToArray(); + + yaml = MiniYaml.MergeLiberal(map.Translations, yaml); + + foreach (var y in yaml) + { + if (y.Key == Game.Settings.Graphics.Language) + selectedTranslations = y.Value.NodesDict.ToDictionary(x => x.Key, x => x.Value.Value ?? ""); + if (y.Key == Game.Settings.Graphics.DefaultLanguage) + defaultTranslations = y.Value.NodesDict.ToDictionary(x => x.Key, x => x.Value.Value ?? ""); + } + + var translations = new Dictionary(); + foreach (var tkv in defaultTranslations.Concat(selectedTranslations)) + { + if (translations.ContainsKey(tkv.Key)) + continue; + if (selectedTranslations.ContainsKey(tkv.Key)) + translations.Add(tkv.Key, selectedTranslations[tkv.Key]); + else + translations.Add(tkv.Key, tkv.Value); + } + + FieldLoader.Translations = translations; + } + public Map PrepareMap(string uid) { LoadScreen.Display(); @@ -78,6 +120,8 @@ namespace OpenRA throw new InvalidDataException("Invalid map uid: {0}".F(uid)); var map = new Map(AvailableMaps[uid].Path); + LoadTranslations(map); + // Reinit all our assets InitializeLoaders(); FileSystem.LoadFromManifest(Manifest); diff --git a/OpenRA.Game/Widgets/ButtonWidget.cs b/OpenRA.Game/Widgets/ButtonWidget.cs index f80c9958f3..364e45474a 100644 --- a/OpenRA.Game/Widgets/ButtonWidget.cs +++ b/OpenRA.Game/Widgets/ButtonWidget.cs @@ -25,7 +25,7 @@ namespace OpenRA.Widgets set { GetKey = _ => value; } } - public string Text = ""; + [Translate] public string Text = ""; public bool Depressed = false; public int VisualHeight = ChromeMetrics.Get("ButtonDepth"); public string Font = ChromeMetrics.Get("ButtonFont"); diff --git a/OpenRA.Game/Widgets/LabelWidget.cs b/OpenRA.Game/Widgets/LabelWidget.cs index 3aaedc3d78..230f0ed955 100644 --- a/OpenRA.Game/Widgets/LabelWidget.cs +++ b/OpenRA.Game/Widgets/LabelWidget.cs @@ -10,6 +10,7 @@ using System; using System.Drawing; +using OpenRA.FileFormats; using OpenRA.Graphics; namespace OpenRA.Widgets @@ -19,7 +20,7 @@ namespace OpenRA.Widgets public class LabelWidget : Widget { - public string Text = null; + [Translate] public string Text = null; public TextAlign Align = TextAlign.Left; public TextVAlign VAlign = TextVAlign.Middle; public string Font = "Regular"; diff --git a/OpenRA.Mods.Cnc/Widgets/Logic/CncSettingsLogic.cs b/OpenRA.Mods.Cnc/Widgets/Logic/CncSettingsLogic.cs index 709bf66136..dac3ed06ab 100644 --- a/OpenRA.Mods.Cnc/Widgets/Logic/CncSettingsLogic.cs +++ b/OpenRA.Mods.Cnc/Widgets/Logic/CncSettingsLogic.cs @@ -104,6 +104,10 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic var windowHeight = generalPane.Get("WINDOW_HEIGHT"); windowHeight.Text = graphicsSettings.WindowedSize.Y.ToString(); + var languageDropDownButton = generalPane.Get("LANGUAGE_DROPDOWNBUTTON"); + languageDropDownButton.OnMouseDown = _ => SettingsMenuLogic.ShowLanguageDropdown(languageDropDownButton); + languageDropDownButton.GetText = () => FieldLoader.Translate(Game.Settings.Graphics.Language); + // Audio var soundSlider = generalPane.Get("SOUND_SLIDER"); soundSlider.OnChange += x => { soundSettings.SoundVolume = x; Sound.SoundVolume = x; }; diff --git a/OpenRA.Mods.Cnc/Widgets/Logic/ProductionTooltipLogic.cs b/OpenRA.Mods.Cnc/Widgets/Logic/ProductionTooltipLogic.cs index 632f7eb0b9..ddf6fbe8dd 100644 --- a/OpenRA.Mods.Cnc/Widgets/Logic/ProductionTooltipLogic.cs +++ b/OpenRA.Mods.Cnc/Widgets/Logic/ProductionTooltipLogic.cs @@ -54,7 +54,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic nameLabel.GetText = () => tooltip.Name; var prereqs = buildable.Prerequisites.Select(a => ActorName(a)); - var requiresString = prereqs.Any() ? "Requires {0}".F(prereqs.JoinWith(", ")) : ""; + var requiresString = prereqs.Any() ? requiresLabel.Text.F(prereqs.JoinWith(", ")) : ""; requiresLabel.GetText = () => requiresString; var power = bi != null ? bi.Power : 0; diff --git a/OpenRA.Mods.Cnc/Widgets/ProductionPaletteWidget.cs b/OpenRA.Mods.Cnc/Widgets/ProductionPaletteWidget.cs index 3d174acd16..8dea9c77eb 100755 --- a/OpenRA.Mods.Cnc/Widgets/ProductionPaletteWidget.cs +++ b/OpenRA.Mods.Cnc/Widgets/ProductionPaletteWidget.cs @@ -37,8 +37,8 @@ namespace OpenRA.Mods.Cnc.Widgets public readonly string TooltipContainer; public readonly string TooltipTemplate = "PRODUCTION_TOOLTIP"; - public readonly string ReadyText = ""; - public readonly string HoldText = ""; + [Translate] public readonly string ReadyText = ""; + [Translate] public readonly string HoldText = ""; public string TooltipActor { get; private set; } public readonly World World; diff --git a/OpenRA.Mods.RA/Tooltip.cs b/OpenRA.Mods.RA/Tooltip.cs index e014b9b30b..b7cd7d35b2 100644 --- a/OpenRA.Mods.RA/Tooltip.cs +++ b/OpenRA.Mods.RA/Tooltip.cs @@ -16,8 +16,8 @@ namespace OpenRA.Mods.RA [Desc("Shown in the build palette widget.")] public class TooltipInfo : ITraitInfo { - public readonly string Description = ""; - public readonly string Name = ""; + [Translate] public readonly string Description = ""; + [Translate] public readonly string Name = ""; [Desc("Sequence of the actor that contains the cameo.")] public readonly string Icon = "icon"; diff --git a/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs b/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs index 30402a14ef..a18a7fb18d 100755 --- a/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs +++ b/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs @@ -26,9 +26,9 @@ namespace OpenRA.Mods.RA.Widgets public int Columns = 3; public int Rows = 5; - public string ReadyText = ""; - public string HoldText = ""; - public string RequiresText = ""; + [Translate] public string ReadyText = ""; + [Translate] public string HoldText = ""; + [Translate] public string RequiresText = ""; public int IconWidth = 64; public int IconHeight = 48; @@ -484,7 +484,7 @@ namespace OpenRA.Mods.RA.Widgets var prereqs = buildable.Prerequisites.Select(Description); if (prereqs.Any()) { - Game.Renderer.Fonts["Regular"].DrawText("{0} {1}".F(RequiresText, prereqs.JoinWith(", ")), p.ToInt2(), Color.White); + Game.Renderer.Fonts["Regular"].DrawText(RequiresText.F(prereqs.JoinWith(", ")), p.ToInt2(), Color.White); p += new int2(0, 8); } diff --git a/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs index 9e42300687..dcae5aa040 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OpenRA.FileFormats; using OpenRA.FileFormats.Graphics; using OpenRA.GameRules; using OpenRA.Widgets; @@ -142,6 +143,10 @@ namespace OpenRA.Mods.RA.Widgets.Logic var maxFrameRate = display.Get("MAX_FRAMERATE"); maxFrameRate.Text = gs.MaxFramerate.ToString(); + var languageDropDownButton = display.Get("LANGUAGE_DROPDOWNBUTTON"); + languageDropDownButton.OnMouseDown = _ => ShowLanguageDropdown(languageDropDownButton); + languageDropDownButton.GetText = () => FieldLoader.Translate(Game.Settings.Graphics.Language); + // Keys var keys = bg.Get("KEYS_PANE"); var keyConfig = Game.Settings.Keys; @@ -270,6 +275,20 @@ namespace OpenRA.Mods.RA.Widgets.Logic return true; } + public static bool ShowLanguageDropdown(DropDownButtonWidget dropdown) + { + Func setupItem = (o, itemTemplate) => + { + var item = ScrollItemWidget.Setup(itemTemplate, + () => Game.Settings.Graphics.Language == o, + () => Game.Settings.Graphics.Language = o); + item.Get("LABEL").GetText = () => FieldLoader.Translate(o); + return item; + }; + + dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, Game.modData.Languages, setupItem); + return true; + } public static bool ShowSoundTickDropdown(DropDownButtonWidget dropdown, SoundSettings audio) { diff --git a/mods/cnc/chrome/ingame.yaml b/mods/cnc/chrome/ingame.yaml index ec39311aac..23645383f4 100644 --- a/mods/cnc/chrome/ingame.yaml +++ b/mods/cnc/chrome/ingame.yaml @@ -279,8 +279,8 @@ Container@PLAYER_WIDGETS: X:WINDOW_RIGHT - 204 Y:287 TooltipContainer:TOOLTIP_CONTAINER - ReadyText:Ready - HoldText:On Hold + ReadyText:@ready@ + HoldText:@on-hold@ Background@FMVPLAYER: Width:WINDOW_RIGHT Height:WINDOW_BOTTOM diff --git a/mods/cnc/chrome/settings.yaml b/mods/cnc/chrome/settings.yaml index 586d9db2e2..58226a5550 100644 --- a/mods/cnc/chrome/settings.yaml +++ b/mods/cnc/chrome/settings.yaml @@ -3,7 +3,7 @@ Container@SETTINGS_PANEL: X:(WINDOW_RIGHT - WIDTH)/2 Y:(WINDOW_BOTTOM - 250)/2 Width:740 - Height:535 + Height:565 Children: ColorPreviewManager@COLOR_MANAGER: Label@TITLE: @@ -15,7 +15,7 @@ Container@SETTINGS_PANEL: Text:Settings Background@GENERAL_CONTROLS: Width:740 - Height:290 + Height:320 Background:panel-black Children: Label@TITLE: @@ -66,35 +66,35 @@ Container@SETTINGS_PANEL: Text:Shellmap Music Label@DEBUG_TITLE: X:15 - Y:150 + Y:180 Width:340 Font:Bold Text:Debug Align:Center Checkbox@PERFTEXT_CHECKBOX: X:15 - Y:170 + Y:200 Width:300 Height:20 Font:Regular Text:Show Performance Text Checkbox@PERFGRAPH_CHECKBOX: X:15 - Y:200 + Y:230 Width:300 Height:20 Font:Regular Text:Show Performance Graph Checkbox@CHECKUNSYNCED_CHECKBOX: X:15 - Y:230 + Y:260 Width:300 Height:20 Font:Regular Text:Check Sync around Unsynced Code Checkbox@SHOW_FATAL_ERROR_DIALOG_CHECKBOX: X:15 - Y:260 + Y:290 Width:300 Height:20 Font:Regular @@ -149,70 +149,82 @@ Container@SETTINGS_PANEL: Width:45 Height:25 MaxLength:5 + Label@LANGUAGE_LABEL: + X:375 + Y:70 + Width:75 + Height:25 + Align:Right + Text:Language: + DropDownButton@LANGUAGE_DROPDOWNBUTTON: + X:455 + Y:70 + Width:140 + Height:25 Label@VIDEO_DESC: X:375 - Y:68 + Y:100 Width:340 Height:25 Font:Tiny Align:Center - Text:Mode/Resolution changes will be applied after the game is restarted + Text:Mode/Resolution/Language changes will be applied after the game is restarted Checkbox@PIXELDOUBLE_CHECKBOX: X:375 - Y:110 + Y:140 Width:200 Height:20 Font:Regular Text:Enable Pixel Doubling Label@AUDIO_TITLE: X:375 - Y:150 + Y:180 Width:340 Font:Bold Text:Sound Align:Center Label@SOUND_LABEL: X:375 - Y:164 + Y:194 Width:95 Height:25 Align:Right Text:Sound Volume: Slider@SOUND_SLIDER: X:475 - Y:170 + Y:200 Width:240 Height:20 Ticks:5 Label@MUSIC_LABEL: X:375 - Y:194 + Y:224 Width:95 Height:25 Align:Right Text:Music Volume: Slider@MUSIC_SLIDER: X:475 - Y:200 + Y:230 Width:240 Height:20 Ticks:5 Label@AUDIO_DEVICE_LABEL: X:375 - Y:229 + Y:259 Width:75 Height:20 Text:Audio Device: DropDownButton@AUDIO_DEVICE: X:475 - Y:230 + Y:260 Width:240 Height:25 Font:Regular Text:Default Device Label@AUDIO_DESC: X:375 - Y:258 + Y:288 Width:340 Height:25 Font:Tiny @@ -296,20 +308,20 @@ Container@SETTINGS_PANEL: Font:Regular Text:Shift-Enter Toggles Team Chat Button@GENERAL_BUTTON: - Y:289 + Y:319 Width:140 Height:35 Text:General Button@INPUT_BUTTON: X:150 - Y:289 + Y:319 Width:140 Height:35 Text:Input Button@BACK_BUTTON: Key:escape X:600 - Y:289 + Y:319 Width:140 Height:35 Text:Back diff --git a/mods/cnc/chrome/tooltips.yaml b/mods/cnc/chrome/tooltips.yaml index 9531f7d3e9..215e2ec11f 100644 --- a/mods/cnc/chrome/tooltips.yaml +++ b/mods/cnc/chrome/tooltips.yaml @@ -60,6 +60,7 @@ Background@PRODUCTION_TOOLTIP: Y:19 Height:23 Font:TinyBold + Text:@requires@ Label@DESC: X:5 Y:39 diff --git a/mods/cnc/languages/english.yaml b/mods/cnc/languages/english.yaml new file mode 100644 index 0000000000..e3e78412b8 --- /dev/null +++ b/mods/cnc/languages/english.yaml @@ -0,0 +1,6 @@ +english: + english: English + + ready: Ready + on-hold: On Hold + requires: Requires {0} diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 36f1d46b36..5779f2d1ac 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -94,6 +94,9 @@ Movies: mods/cnc/movies-gdi.yaml mods/cnc/movies-nod.yaml +Translations: + mods/cnc/languages/english.yaml + Voices: mods/cnc/voices.yaml diff --git a/mods/d2k/chrome/ingame.yaml b/mods/d2k/chrome/ingame.yaml index 96e19730c9..3d1dd5e046 100644 --- a/mods/d2k/chrome/ingame.yaml +++ b/mods/d2k/chrome/ingame.yaml @@ -155,9 +155,9 @@ Container@PLAYER_WIDGETS: Y:280 Width:238 Height:500 - ReadyText: READY - HoldText: ON HOLD - RequiresText: Requires + ReadyText:@ready@ + HoldText:@on-hold@ + RequiresText:@requires@ IconWidth: 60 IconHeight: 48 Columns: 3 diff --git a/mods/d2k/languages/english.yaml b/mods/d2k/languages/english.yaml new file mode 100644 index 0000000000..7eba7d3299 --- /dev/null +++ b/mods/d2k/languages/english.yaml @@ -0,0 +1,6 @@ +english: + english: English + + ready: READY + on-hold: ON HOLD + requires: Requires {0} diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index d6a2554605..15b269af13 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -95,6 +95,9 @@ Music: Movies: +Translations: + mods/d2k/languages/english.yaml + LoadScreen: DefaultLoadScreen Image: mods/d2k/uibits/loadscreen.png InstallerMenuWidget: INSTALL_PANEL diff --git a/mods/ra/chrome/ingame.yaml b/mods/ra/chrome/ingame.yaml index afc3e46fa9..21280cc850 100644 --- a/mods/ra/chrome/ingame.yaml +++ b/mods/ra/chrome/ingame.yaml @@ -148,16 +148,16 @@ Container@PLAYER_WIDGETS: SupportPowerBin@INGAME_POWERS_BIN: X:0 Y:25 - ReadyText: READY - HoldText: ON HOLD + ReadyText: @ready@ + HoldText: @on-hold@ BuildPalette@INGAME_BUILD_PALETTE: X:WINDOW_RIGHT - 250 Y:280 Width:250 Height:500 - ReadyText: READY - HoldText: ON HOLD - RequiresText: Requires + ReadyText: @ready@ + HoldText: @on-hold@ + RequiresText: @requires@ Container@OBSERVER_WIDGETS: Children: diff --git a/mods/ra/chrome/settings.yaml b/mods/ra/chrome/settings.yaml index 95c0a563e8..6c4ed0af40 100644 --- a/mods/ra/chrome/settings.yaml +++ b/mods/ra/chrome/settings.yaml @@ -138,7 +138,7 @@ Background@SETTINGS_MENU: Label@SOUND_VOLUME_LABEL: X:0 Y:10 - Text: Sound Volume + Text: Sound Volume Slider@SOUND_VOLUME: X:100 Y:0 @@ -255,7 +255,7 @@ Background@SETTINGS_MENU: Height:25 Font:Tiny Align:Center - Text:Renderer/Mode/Resolution changes will be applied after the game is restarted. + Text:Mode/Resolution changes will be applied after the game is restarted. Checkbox@PIXELDOUBLE_CHECKBOX: Y:60 Width:200 @@ -274,6 +274,24 @@ Background@SETTINGS_MENU: Width:45 Height:25 MaxLength:3 + Label@LANGUAGE_LABEL: + X:0 + Y:130 + Width:75 + Height:25 + Text:Language: + DropDownButton@LANGUAGE_DROPDOWNBUTTON: + X:80 + Y:130 + Width:140 + Height:25 + Label@LANGUAGE_DESC: + Y:160 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:Language changes will be applied after the game is restarted. Container@KEYS_PANE: X:37 Y:100 diff --git a/mods/ra/languages/english.yaml b/mods/ra/languages/english.yaml new file mode 100644 index 0000000000..7eba7d3299 --- /dev/null +++ b/mods/ra/languages/english.yaml @@ -0,0 +1,6 @@ +english: + english: English + + ready: READY + on-hold: ON HOLD + requires: Requires {0} diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 4163c419b3..085986af9a 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -113,6 +113,9 @@ Movies: mods/ra/movies1.yaml mods/ra/movies2.yaml +Translations: + mods/ra/languages/english.yaml + LoadScreen: DefaultLoadScreen Image: mods/ra/uibits/loadscreen.png InstallerMenuWidget: INSTALL_PANEL diff --git a/mods/ts/languages/english.yaml b/mods/ts/languages/english.yaml new file mode 100644 index 0000000000..7eba7d3299 --- /dev/null +++ b/mods/ts/languages/english.yaml @@ -0,0 +1,6 @@ +english: + english: English + + ready: READY + on-hold: ON HOLD + requires: Requires {0} diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 9f6146bc08..5fd0dd1f3c 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -136,6 +136,9 @@ Music: Movies: +Translations: + mods/ts/languages/english.yaml + LoadScreen: DefaultLoadScreen Image: mods/ts/uibits/loadscreen.png InstallerMenuWidget: INSTALL_PANEL