Localizing user interface components should be easy from a modding perspective.
Classes in C# are organized in a hierarchical namespace layout. I am thinking of having [[UiLayers]] specify a set of string fields that hold localized text.
When the UiLayer is queried, that text should be filled in with the corresponding translations if they haven't been already.
Each element with [UILocalize]
should implement an interface, IUILocalizable
, that is passed the namespace/key for the translation and updates itself with the result from the localization store. This could be recursive, so you could have a localizable component with fields that also have [UILocalize]
set on them.
namespace OpenNefia.Core.UI.Layer
{
internal class TestLayer : BaseUiLayer<string>
{
[UILocalize]
private IUiTextNoArgs TextFromString;
[UILocalize]
private IUiTextFn1<Chara> TextFromFn1;
[UILocalize]
private IUiTextFn2<string, string> TextFromFn2;
private FontDef FontTextWhite;
public TestLayer()
{
FontTextWhite = FontDefOf.TextWhite;
TextFromString = new UiTextNoArgs(this.FontText);
TextFromFn1 = new UiTextFn1(this.FontText);
TextFromFn2 = new UiTextFn2(this.FontText);
if (Rand.OneIn(2))
{
// If a custom locale key is set in advance, UILocalize will
// use it instead of the locale key it generates based on reflection.
TextFromString.LocaleKey = "CustomNamespace.Foo.Scut";
}
}
public override void OnQuery()
{
TextFromString.ApplyArguments();
TextFromFn1.ApplyArguments(Chara.Player);
TextFromFn2.ApplyArguments("foo", "ほげ");
}
public override void SetPosition(int x, int y)
{
base.SetPosition(x, y);
this.TextFromString.SetPosition(x, y);
this.TextFromFn1.SetPosition(x, y + This.FontText.GetHeight());
this.TextFromFn2.SetPosition(x, y + This.FontText.GetHeight() * 2);
}
public override void Update(float dt)
{
this.TextFromString.Update(dt);
this.TextFromFn1.Update(dt);
this.TextFromFn2.Update(dt);
}
public override void Draw()
{
this.TextFromString.Draw();
this.TextFromFn1.Draw();
this.TextFromFn2.Draw();
}
}
}
Then there would be a corresponding translation file (Lua or XML) that would be namespaced under OpenNefia.Core.UI.Layer.TestLayer
with the translations to fill in.
OpenNefia.Core.Ui.Layer.TestLayer
{
TextFromString = "Hello, world.",
TextFromFn1 = function(_1) return ("Character: %s"):format(_1.Name) _end,
TextFromFn2 = function(_1, _2_) return ("Args: %s %s") end,
}
CustomNamespace.Foo
{
Scut = "Scut!"
}
The global tables would be autogenerated with something like automagic tables.
This would be a high-level abstraction over a bare-bones API like OpenNefia.Core.I18N.Get("CustomNamespace.Foo.Scut")
that returns a string.
A problem arises when there is a child component that needs to be passed a string, but the localized string wasn't attached yet by UILocalize
.
internal class TestLayer : BaseUiLayer<string>
{
[UILocalize]
private IUiTextNoArgs TextFromString;
private UiWindow Window;
private FontDef FontTextWhite;
public TestLayer()
{
TextFromString = new UiTextNoArgs(this.FontText);
Window = new UiWindow(TextFromString);
}
}
}
Maybe in this case, there could be a weak reference that receives a string and nothing more, instead of wrapping that inside a UiText
.
This paradigm should not be used for static classes/fields!
We shouldn't have to allocate a bunch of localized strings/functions at startup that only ever get touched once or twice in a single session.
It's just that if we know we need an IUiText
somewhere as an instance variable, we might as well provide a way for the translation system to hook into that component.
The value add of this system is diminished significantly if you just use it for string
s that aren't going to be touched at a rate of 60 frames per second.