Skip to content
This repository has been archived by the owner on Dec 24, 2018. It is now read-only.

Add localization support with KSPDev Utils

Igor Zavoychinskiy edited this page Jul 11, 2018 · 15 revisions

KSPDev Utils offers a variety of tools to simplify the localization support in your mods. Moreover, if your mod is 100% compliant with the Utils approach, you can use the full potential of the localization tool to prepare the first localization friendly release.

The detailed documentation of the localization classes can be found on the Utils documentation site. In this article we will only cover the key changes that are needed to make the mod fully compatible with the KSPDev approach.

Examples of the mods that are fully compatible with the KSPDev localization approach:

You may use these projects as a reference to the best practices.

Simple GUI strings

A good programming technique assumes that you never use "magic constants" in the code. That is, you don't put numbers or string literals in the functional code. Instead, you create a constant at the top of the module, with all the comments necessary, and then use it in the functional code. E.g. consider this bad style code:

public void OnGUI() {
  GUILayout.Label("Hello world!");
}

A more nice code of the same functionality would look like this:

#region GUI strings
// The string to present each time the module presents a GUI.
const string HelloWorldTxt = "Hello world!";
#endregion

public void OnGUI() {
  GUILayout.Label(HelloWorldTxt);
}

And if your code is already this nice, it's really easy to convert it into the KSPDev localization standard:

#region GUI strings
// The string to present each time the module presents a GUI.
static readonly Message HelloWorldTxt = new Message("#tag001", "Hello world!");
#endregion

public void OnGUI() {
  GUILayout.Label(HelloWorldTxt);
}

Once you did it, the string will start reading it's localizable value from a corresponded localization file. And if there is no such a file, or the tag is not defined there, then the default text will be used. What is even more cool, is that the localization tool will be able to find this declaration and extract it when preparing a localization file!

And did you notice there is a comment to the constant, which explains when and how the string is used in the code? It would be great to provide this context to the community translators, who will be translating your mod to the languages you have no idea about. KSPDev can do it for you, and the localization tool will pick it up just fine:

#region GUI strings
static readonly Message HelloWorldTxt = new Message(
    "#tag001",
    defaultTemplate: "Hello world!",
    description: "The string to present each time the module presents a GUI.");
#endregion

public void OnGUI() {
  GUILayout.Label(HelloWorldTxt);
}

The text you've provided in the description argument will be presented in the localization file as a comment. It will help the translator to give the proper translation.

Parametrized GUI strings

It's simple while the strings are simple, but what if they are constructed on the fly with a bunch of parameters? As we figured out above, it's always good to use the constants. So your good styled code looks like this:

#region GUI strings
// The message to present a magic constant.
const string MagicConstantTxt = "The value is {0}";
#endregion

public void OnGUI() {
  GUILayout.Label(string.Format(MagicConstantTxt, 1234));
}

Now you can easily transform it to the KSPDev concept:

#region GUI strings
static readonly Message MagicConstantTxt<int> = new Message<int>(
    "#tag002",
    defaultTemplate: "The value is <<1>>",
    description: "The message to present a magic constant.");
#endregion

public void OnGUI() {
  GUILayout.Label(MagicConstantTxt.Format(1234));
}

The change is minor, but there is also a great benefit: now the argument is typed and checked during the compilation time! Note, that the C# {0} argument notion has changed to a Lingoona placeholder <<1>>.

But that's not it. You can use special formatters for the arguments for the common value types. Let's say, the "magic constant" is actually a distance in meters. You could make your own logic to present it in a nice way, but you also can use the existing type formatter: DistanceType.

#region GUI strings
static readonly Message MagicConstantTxt<DistanceType> = new Message<DistanceType>(
    "#tag002",
    defaultTemplate: "The value is <<1>>",
    description: "The message to present a magic constant.");
#endregion

public void OnGUI() {
  GUILayout.Label(MagicConstantTxt.Format(1234));
}

Now the argument in the string will be formatted as a distance in meters or kilometers (depending on the value). And the units name will be localized! E.g. for a value 1234.567 the output in en-us layout will be The value is 1.235 km. In a different layout it will have different units name and the decimal/thousands separation symbol.

See more information about the argument formatters in the documentation. You can also write your own formatters, it's really simple. Take a look at the CompactNumberType.

When dealing with a tricky argument formatter (e.g. your own), it makes sense to give the translators a context of how the final string will look like. You can do it by providing an extra argument to the Message class:

#region GUI strings
static readonly Message MagicConstantTxt<DistanceType> = new Message<DistanceType>(
    "#tag002",
    defaultTemplate: "The value is <<1>>",
    description: "The message to present a magic constant.",
    example: "The value is 1.235 km");
#endregion

public void OnGUI() {
  GUILayout.Label(MagicConstantTxt.Format(1234.567));
}

KSPDev provides generics to create a parameterized message with up to 5 arguments. However, you may make your own classes that accept more arguments, use the Utils code as an example.

Module actions, events, and field

A part module may have a set of special methods (events or actions) and fields. Those are revealed in the GUI and, hence, need translation as well. KSPDev can help here as well. However, some extra work will be needed from the coding side. Let's say your mod has the following module:

public class MyModule: PartModule {
  [KSPField(guiName = "Status")]
  public string status;

  [KSPEvent(guiName = "My Event", guiActive = true)]
  public void MyEvent() {
    Debug.Log("MyEvent!");
  }

  [KSPAction(guiName = "My Action")]
  public void MyAction(KSPActionParam param) {
    Debug.Log("MyAction!");
  }
}

Normally you just put the tags instead of the guiName strings, and it makes the localization to work. However, using the bare tags doesn't help in the code readability. And, of course, the localization tool won't be able to pick up any extra information except the tag itself. Here is how the code could be refactored to be compatible:

public class MyModule: PartModule, IsLocalizableModule {
  [KSPField]
  [LocalizableItem(
      tag = "#loc000",
      defaultTemplate = "Status",
      description = "The filed in the part's context menu to represent the current status")]
  public string status;

  [KSPEvent(guiActive = true)]
  [LocalizableItem(
      tag = "#loc001",
      defaultTemplate = "My Event",
      description = "The event on the part to trigger 'MyEvent' log record")]
  public void MyEvent() {
    Debug.Log("MyEvent!");
  }

  [KSPAction]
  [LocalizableItem(
      tag = "#loc001",
      defaultTemplate = "My Action",
      description = "The action to trigger 'MyAction' log record")]
  public void MyAction(KSPActionParam param) {
    Debug.Log("MyAction!");
  }

  public override void OnAwake() {
    LocalizeModule();
  }

  #region IsLocalizableModule implementation
  /// <inheritdoc/>
  public virtual void LocalizeModule() {
    LocalizationLoader.LoadItemsInModule(this);
  }
  #endregion
}

Now, all the information about the localization is in the file, so it can be extracted and edited as needed. Note, that a call to LocalizationLoader.LoadItemsInModule is added. It's needed to have all the events, actions and fields updated according to their localization setup. Do not hesitate to call this method: it's performance optimized and needs to be called only once per a module instance.

The implementation of IsLocalizableModule is optional, but if you do implement it, then the localization tool will be able to dynamically reload the strings without the game restart. The LocalizeModule method is also the perfect place to do caching of the strings that need localization.

Sometimes, the item (e.g. an event) is controlled dynamically from the code, and there is no such a thing as "localization" for this item since it can have different name depending on the module state. If this is the case, it makes sense to let the localization tool to know that the item doesn't need localization and it should be just skipped:

[KSPEvent(guiActive = true)]
[LocalizableItem(null)]
public void MyEvent() {
  Debug.Log("MyEvent!");
}

Cached strings

In case of your mod caches some strings by storing the localized versions in the internal fields, there can be issues for the translators who will try to refresh the strings from a new localization file. If you do nothing, their best hope will be the game restart, which is very expensive time wise. Your mod can be a bit more welcome to the community translators by reacting to the language change event, and rebuilding the cached strings in it:

public class MyModule : MonoBehaviour {
  void Awake() {
    base.OnAwake();
    GameEvents.onLanguageSwitched.Add(ReloadCachedStrings);
  }

  void OnDestroy() {
    GameEvents.onLanguageSwitched.Remove(ReloadCachedStrings);
  }

  void ReloadCachedStrings() {
    // Do the stuff.
  }
}

Every time the localization strings are refreshed, the language change event is fired. Note, that for the modules it's enough to implement the IsLocalizableModule interface.

Dynamic part menus

If you mod dynamically sets the part's menu names, then a simple string refresh won't help: your code won't get triggered, so the strings in the menu won't get updated. There is a solution, though. Just implement a special KSPDev interface in you part module: IHasContextMenu. When it's time to refresh the menu, the appropriate method will be called. It will also make the code more clear.

public class MyModule : PartModule, IHasContextMenu {
  public void UpdateContextMenu() {
    // Update the menu here.
  }
}