Expose hotkeys to localisation.

Allows the Settings > Hotkeys screen to be localised, including hotkey decriptions, groups and contexts.

The hotkey names are exposed to localisation via KeycodeExts. Hotkey modifiers are similarly exposed via ModifersExts.

The Settings > Input screen has a Zoom Modifier dropdown, which shows the localised modifier name.

The --check-yaml utility command is taught to recognise all hotkey translation, so it can validate their usage.
This commit is contained in:
RoosterDragon
2024-09-17 19:42:42 +01:00
committed by Gustas
parent 10856ccfd0
commit 6f6fb5b393
39 changed files with 1284 additions and 722 deletions

View File

@@ -21,6 +21,7 @@ using OpenRA.Mods.Common.Scripting;
using OpenRA.Mods.Common.Scripting.Global;
using OpenRA.Mods.Common.Traits;
using OpenRA.Mods.Common.Warheads;
using OpenRA.Mods.Common.Widgets.Logic;
using OpenRA.Scripting;
using OpenRA.Traits;
using OpenRA.Widgets;
@@ -249,7 +250,7 @@ namespace OpenRA.Mods.Common.Lint
testedFields.AddRange(
modData.ObjectCreator.GetTypes()
.Where(t => t.IsSubclassOf(typeof(TraitInfo)) || t.IsSubclassOf(typeof(Warhead)))
.SelectMany(t => t.GetFields().Where(f => f.HasAttribute<FluentReferenceAttribute>())));
.SelectMany(t => Utility.GetFields(t).Where(Utility.HasAttribute<FluentReferenceAttribute>)));
// TODO: linter does not work with LoadUsing
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
@@ -278,6 +279,36 @@ namespace OpenRA.Mods.Common.Lint
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
usedKeys, testedFields, Utility.GetFields(typeof(ModContent.ModPackage)), modContent.Packages.Values);
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
usedKeys, testedFields, Utility.GetFields(typeof(HotkeyDefinition)), modData.Hotkeys.Definitions);
// All keycodes and modifiers should be marked as used, as they can all be configured for use at hotkeys at runtime.
GetUsedTranslationKeysFromFieldsWithTranslationReferenceAttribute(
usedKeys, testedFields, Utility.GetFields(typeof(KeycodeExts)).Concat(Utility.GetFields(typeof(ModifiersExts))), new[] { (object)null });
foreach (var filename in modData.Manifest.ChromeLayout)
CheckHotkeysSettingsLogic(usedKeys, MiniYaml.FromStream(modData.DefaultFileSystem.Open(filename), filename));
static void CheckHotkeysSettingsLogic(Keys usedKeys, IEnumerable<MiniYamlNode> nodes)
{
foreach (var node in nodes)
{
if (node.Value.Nodes != null)
CheckHotkeysSettingsLogic(usedKeys, node.Value.Nodes);
if (node.Key != "Logic" || node?.Value.Value != "HotkeysSettingsLogic")
continue;
var hotkeyGroupsNode = node.Value.NodeWithKeyOrDefault("HotkeyGroups");
if (hotkeyGroupsNode == null)
continue;
var hotkeyGroupsKeys = hotkeyGroupsNode?.Value.Nodes.Select(n => n.Key);
foreach (var key in hotkeyGroupsKeys)
usedKeys.Add(key, new FluentReferenceAttribute(), $"`{nameof(HotkeysSettingsLogic)}.HotkeyGroups`");
}
}
return (usedKeys, testedFields);
}

View File

@@ -37,7 +37,9 @@ namespace OpenRA.Mods.Common.Lint
return expr != null ? expr.Variables : Enumerable.Empty<string>();
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
if (type.IsGenericType &&
(type.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))
{
// Use an intermediate list to cover the unlikely case where both keys and values are lintable.
var dictionaryValues = new List<string>();
@@ -60,6 +62,9 @@ namespace OpenRA.Mods.Common.Lint
"Dictionary<string, T> (LintDictionaryReference.Keys)",
"Dictionary<T, string> (LintDictionaryReference.Values)",
"Dictionary<T, IEnumerable<string>> (LintDictionaryReference.Values)",
"IReadOnlyDictionary<string, T> (LintDictionaryReference.Keys)",
"IReadOnlyDictionary<T, string> (LintDictionaryReference.Values)",
"IReadOnlyDictionary<T, IEnumerable<string>> (LintDictionaryReference.Values)",
"BooleanExpression", "IntegerExpression"
};

View File

@@ -40,7 +40,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
.Where(t => t.Name.EndsWith("Widget", StringComparison.InvariantCulture) && t.IsSubclassOf(typeof(Widget)))
.ToDictionary(
t => t.Name[..^6],
t => t.GetFields().Where(f => f.HasAttribute<FluentReferenceAttribute>()).Select(f => f.Name).ToArray())
t => Utility.GetFields(t).Where(Utility.HasAttribute<FluentReferenceAttribute>).Select(f => f.Name).ToArray())
.Where(t => t.Value.Length > 0)
.ToDictionary(t => t.Key, t => t.Value);

View File

@@ -43,7 +43,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
.Where(t => t.Name.EndsWith("Info", StringComparison.InvariantCulture) && t.IsSubclassOf(typeof(TraitInfo)))
.ToDictionary(
t => t.Name[..^4],
t => t.GetFields().Where(f => f.HasAttribute<FluentReferenceAttribute>()).Select(f => f.Name).ToArray())
t => Utility.GetFields(t).Where(Utility.HasAttribute<FluentReferenceAttribute>).Select(f => f.Name).ToArray())
.Where(t => t.Value.Length > 0)
.ToDictionary(t => t.Key, t => t.Value);

View File

@@ -25,6 +25,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[FluentReference("key", "context")]
const string DuplicateNotice = "label-duplicate-notice";
[FluentReference]
const string AnyContext = "hotkey-context-any";
readonly ModData modData;
readonly Dictionary<string, MiniYaml> logicArgs;
@@ -37,8 +40,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
bool isHotkeyValid;
bool isHotkeyDefault;
string currentContext = "Any";
readonly HashSet<string> contexts = new() { "Any" };
string currentContext = AnyContext;
readonly HashSet<string> contexts = new() { AnyContext };
readonly Dictionary<string, HashSet<string>> hotkeyGroups = new();
TextFieldWidget filterInput;
@@ -66,7 +69,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
key.Id = hd.Name;
key.IsVisible = () => true;
key.Get<LabelWidget>("FUNCTION").GetText = () => hd.Description + ":";
var desc = FluentProvider.GetString(hd.Description) + ":";
key.Get<LabelWidget>("FUNCTION").GetText = () => desc;
var remapButton = key.Get<ButtonWidget>("HOTKEY");
WidgetUtils.TruncateButtonToTooltip(remapButton, modData.Hotkeys[hd.Name].GetValue().DisplayString());
@@ -192,7 +196,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
continue;
var header = headerTemplate.Clone();
header.Get<LabelWidget>("LABEL").GetText = () => hg.Key;
var groupName = FluentProvider.GetString(hg.Key);
header.Get<LabelWidget>("LABEL").GetText = () => groupName;
hotkeyList.AddChild(header);
var added = new HashSet<HotkeyDefinition>();
@@ -220,7 +225,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
void InitHotkeyRemapDialog(Widget panel)
{
var label = panel.Get<LabelWidget>("HOTKEY_LABEL");
var labelText = new CachedTransform<HotkeyDefinition, string>(hd => hd?.Description + ":");
var labelText = new CachedTransform<HotkeyDefinition, string>(
hd => (hd != null ? FluentProvider.GetString(hd.Description) : "") + ":");
label.IsVisible = () => selectedHotkeyDefinition != null;
label.GetText = () => labelText.Update(selectedHotkeyDefinition);
@@ -228,10 +234,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic
duplicateNotice.TextColor = ChromeMetrics.Get<Color>("NoticeErrorColor");
duplicateNotice.IsVisible = () => !isHotkeyValid;
var duplicateNoticeText = new CachedTransform<HotkeyDefinition, string>(hd =>
hd != null ?
FluentProvider.GetString(DuplicateNotice,
"key", hd.Description,
"context", hd.Contexts.First(c => selectedHotkeyDefinition.Contexts.Contains(c))) : "");
hd != null
? FluentProvider.GetString(
DuplicateNotice,
"key", FluentProvider.GetString(hd.Description),
"context", FluentProvider.GetString(hd.Contexts.First(c => selectedHotkeyDefinition.Contexts.Contains(c))))
: "");
duplicateNotice.GetText = () => duplicateNoticeText.Update(duplicateHotkeyDefinition);
var originalNotice = panel.Get<LabelWidget>("ORIGINAL_NOTICE");
@@ -328,8 +336,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
var filter = filterInput.Text;
var isFilteredByName = string.IsNullOrWhiteSpace(filter) ||
hd.Description.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
var isFilteredByContext = currentContext == "Any" || hd.Contexts.Contains(currentContext);
FluentProvider.GetString(hd.Description).Contains(filter, StringComparison.CurrentCultureIgnoreCase);
var isFilteredByContext = currentContext == AnyContext || hd.Contexts.Contains(currentContext);
return isFilteredByName && isFilteredByContext;
}
@@ -357,9 +365,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
static string GetContextDisplayName(string context)
{
if (string.IsNullOrEmpty(context))
return "Any";
context = AnyContext;
return context;
return FluentProvider.GetString(context);
}
}
}

View File

@@ -36,21 +36,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[FluentReference]
const string Joystick = "options-mouse-scroll-type.joystick";
[FluentReference]
const string Alt = "options-zoom-modifier.alt";
[FluentReference]
const string Ctrl = "options-zoom-modifier.ctrl";
[FluentReference]
const string Meta = "options-zoom-modifier.meta";
[FluentReference]
const string Shift = "options-zoom-modifier.shift";
[FluentReference]
const string None = "options-zoom-modifier.none";
static InputSettingsLogic() { }
readonly string classic;
@@ -205,11 +190,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
var options = new Dictionary<string, Modifiers>()
{
{ FluentProvider.GetString(Alt), Modifiers.Alt },
{ FluentProvider.GetString(Ctrl), Modifiers.Ctrl },
{ FluentProvider.GetString(Meta), Modifiers.Meta },
{ FluentProvider.GetString(Shift), Modifiers.Shift },
{ FluentProvider.GetString(None), Modifiers.None }
{ ModifiersExts.DisplayString(Modifiers.Alt), Modifiers.Alt },
{ ModifiersExts.DisplayString(Modifiers.Ctrl), Modifiers.Ctrl },
{ ModifiersExts.DisplayString(Modifiers.Meta), Modifiers.Meta },
{ ModifiersExts.DisplayString(Modifiers.Shift), Modifiers.Shift },
{ ModifiersExts.DisplayString(Modifiers.None), Modifiers.None }
};
ScrollItemWidget SetupItem(string o, ScrollItemWidget itemTemplate)