📜 ⬆️ ⬇️

Simple plugin for localization of applications on Unity3D

I think every developer on Unity3D sooner or later faces the need to localize the application into several languages. In any case, it is better to lay it in advance in the architecture, even if at the start of the application several languages ​​are not required.

In this article I will describe the development of a simple plug-in for localizing UI Text-components with the ability to dynamically change the language and edit the text in the editor.

For the enumeration of the languages ​​used (we will consider Russian and English in the article) we will use enum Unity SystemLanguage.

Unfortunately, as far as I know, Unity out of the box does not support serialization of a Dictionary or key-value-pair-classes. So, to keep things simple, we will write a couple of our classes for the needs of the plugin.
')

Translation


The Translation structure is essentially a key-value pair:

public struct Translation { public SystemLanguage key; public string value; public Translation(SystemLanguage key, string value) { this.key = key; this.value = value; } } 

Label


Next step: create a Label class. Its task is to keep in itself a unique integer id and a list of Translation:

 [System.Serializable] public class Label{ [SerializeField] int _id; [SerializeField] List<Translation> translations = new List<Translation>(); public int id { get { return _id; } private set { _id = value; } } public Label(int id) { this.id = id; } } 

Since the Label class essentially implements the logic of the Dictionary; you need to add two public methods: Get and Set.
In the Get method, we look for whether there is a string in the requested language, if there is, return it, if not, return an empty string.
In the Set method it is the same - if we have a string in the required language - change it, if not - add it.

 public string Get(SystemLanguage language) { for (int i = 0; i < translations.Count; i++) { if (translations[i].key == language) { return translations[i].value; } } translations.Add(new Translation(language, string.Empty)); return translations[translations.Count - 1].value; } public void Set(SystemLanguage language, string str) { for (int i = 0; i < translations.Count; i++) { if (translations[i].key == language){ translations[i] = new Translation(language, str); return; } } translations.Add(new Translation(language, str)); } 

LabelsData


All Label instances need to be stored and edited somewhere in the Unity inspector. Use the Unity tools for this and create our own LabelsData class inherited from ScriptableObject. ScriptableObject allows you to store the necessary data in the project file structure and is often used as a small game database.
The LabelsData class will store the List with all the game translations and the default Label for errors.

To create an instance of LabelsData, we will add the CreateAssetMenu attribute before the class declaration:

 [CreateAssetMenu(fileName="LabelsData", menuName="SimpleLocalizator/LabelsData")] public class LabelsData : ScriptableObject { [SerializeField] List<Label> _labels=new List<Label>(); Label _defaultLabel = new Label (-1, "not translated"); public static Label defaultLabel { get { return instance._defaultLabel; } } public static List<Label> labels { get { return instance._labels; } private set { instance._labels = value; } } } 

To ensure the availability of an instance of LabelsData, we use lazy initialization according to the following logic:

1. Public fields are getters that return field values ​​of an instance of an instance;
2. instance - global reference to the LabelsData instance. If it is not initialized, a load is applied from the Resources folder, or a new instance of LabelsData is created.
3. From this it follows that our translation database should be placed in Resources.

 static LabelsData _instance; public static LabelsData instance { get { if (_instance==null) { _instance = (LabelsData)Resources.Load ("LabelsData"); if (_instance == null) { _instance = CreateInstance<LabelsData> (); } } return _instance; } } 

Thus, we get the ability to easily edit our translations in the Unity editor:

Screenshot


We can create as many new Labels as desired, assign them an id for identification and fill them with translations into the required languages.

LanguageManager


The next step is to write the static class LanguageManager, which provides a convenient interface for all components that implement the display of multilingual content.

 public static class LanguageManager{ static SystemLanguage _currentLanguage = SystemLanguage.English; public static SystemLanguage currentLanguage { get { return _currentLanguage; } set { _currentLanguage = value; if (onLanguageChanged != null) onLanguageChanged (); } } public static Action onLanguageChanged; } 

Through the currentLanguage property of this class, we can change the current language, and all components that subscribe to the onLanguageChanged event change their content.
For the initial definition of the current system language, add the Init method, which will be called only once:

 public static bool autoDetectLanguage=true; private static bool init = false; static void Init() { if (!init) { init = true; if (autoDetectLanguage) { currentLanguage = Application.systemLanguage; } else { currentLanguage = currentLanguage; } Debug.Log("LanguageManager: initialized. Current language: " + currentLanguage); } } 

To get the string with the desired id, add the GetString method, in which we will search for the desired Label among the LabelsData data, and if it is not there, return the default string “not translated”:

 public static string GetString(int labelID) { return GetString(labelID, currentLanguage); } public static string GetString(int labelID, SystemLanguage language) { Init(); for (int i = 0; i < LabelsData.labels.Count; i++) { if (LabelsData.labels[i].id == labelID) { return LabelsData.labels[i].Get(language); } } return LabelsData.defaultLabel.Get(language); } 

Representation


It now remains to write the plug-in components responsible for displaying content. What content can we display in Unity? Lines for UI.Text and TextMesh, any pictures (for example, icons and banners in Russian and English). Within the framework of the article, we consider the display of multilingual strings for UI.Text.

Create an abstract class MultiLanguageComponent to display content, from which we will inherit further. Its tasks are simple - keep the current language, subscribe to LanguageManager.onLanguageChanged and update the content in OnValidate (for tests in the editor):

 public abstract class MultiLanguageComponent : MonoBehaviour { [SerializeField] SystemLanguage _currentLanguage = SystemLanguage.English; protected SystemLanguage currentLanguage { get { return _currentLanguage; } set { _currentLanguage = value; Refresh (); } } public void OnValidate() { currentLanguage = _currentLanguage; } void OnEnable() { OnLanguageRefresh (); LanguageManager.onLanguageChanged += OnLanguageRefresh; } void OnDisable() { LanguageManager.onLanguageChanged -= OnLanguageRefresh; } void OnLanguageRefresh() { currentLanguage = LanguageManager.currentLanguage; } protected virtual void Refresh() { } } 

Here the Refresh method is virtual, which we will override in the descendant classes.

Create a descendant class MultiLanguageTextBase, which holds the integer labelID:

 public abstract class MultiLanguageTextBase : MultiLanguageComponent{ [SerializeField] int _labelID; [SerializeField] bool toUpper=false; public int labelID { get { return _labelID; } set { _labelID = value; Refresh(); } } } 

Override the Refresh method in it. Since Refresh will be called when the application language changes or when the labelID changes — we get the string in the required language from LanguageManager and call the VisualizeString method (in which the string will be displayed on the application screen using UI.Text or TextMesh). The local variable is needed to determine whether an update occurs in the editor before launching the application — in this case, a debug will be received from LanguageManager in a line in the current language of the specific component, and not in the system language.

 protected override void Refresh() { bool local = (Application.isEditor && !Application.isPlaying); string str = local ? LanguageManager.GetString(labelID, currentLanguage) : LanguageManager.GetString(labelID); if (toUpper) str = str.ToUpper(); VisualizeString(str); } protected abstract void VisualizeString(string str); 

Let's create the last MultiLanguageTextUI class, which already directly prints a string to the screen and inherits from MultiLanguageTextBase. In it, we will override the VisualizeString method to display text in UI.Text:

 [RequireComponent(typeof(Text))] public class MultiLanguageTextUI : MultiLanguageTextBase { Text _text; public Text text { get { if (_text == null && gameObject!=null) _text = GetComponent<Text> (); return _text; } } protected override void VisualizeString(string str) { if (text != null) text.text = str; } } 

Now we can simply add the MultiLanguageTextUI component to the object with the text and set the required labelID:

Screenshot


Demonstration


Gif


Total


Thus, we have a simple localization system for the application. In the future, you can follow the MultiLanguageComponent and add your own components for translation.

The repository on GitHub (here added some additional features - export / import to csv, components for TextMesh, Image, AudioSource, VideoPlayer, MeshFilter).

Full code


Translation.cs
 using UnityEngine; namespace SimpleLocalizator { [System.Serializable] public struct Translation { public SystemLanguage key; public string value; public Translation(SystemLanguage key, string value) { this.key = key; this.value = value; } } } 


Label.cs
 using UnityEngine; using System.Collections.Generic; namespace SimpleLocalizator { [System.Serializable] public class Label{ #region Data [SerializeField] int _id; [SerializeField] List<Translation> translations = new List<Translation>(); private const string defaultText = "not translated"; #endregion #region Interface public int id { get { return _id; } private set { _id = value; } } public Label(int id) { this.id = id; } public string Get(SystemLanguage language) { for (int i = 0; i < translations.Count; i++) { if (translations[i].key == language) { return translations[i].value; } } translations.Add(new Translation(language, defaultText)); return translations[translations.Count - 1].value; } public void Set(SystemLanguage language, string str) { for (int i = 0; i < translations.Count; i++) { if (translations[i].key == language){ translations[i] = new Translation(language, str); return; } } translations.Add(new Translation(language, str)); } #endregion } } 


LabelsData.cs
 using System.Collections.Generic; using UnityEngine; using System.Text; namespace SimpleLocalizator { [CreateAssetMenu(fileName="LabelsData", menuName="SimpleLocalizator/LabelsData")] public class LabelsData : ScriptableObject { [SerializeField] List<Label> _labels=new List<Label>(); Label _defaultLabel = new Label (-1); public static Label defaultLabel { get { return instance._defaultLabel; } } public static List<Label> labels { get { return instance._labels; } private set { instance._labels = value; } } static LabelsData _instance; public static LabelsData instance { get { if (_instance==null) { _instance = (LabelsData)Resources.Load ("LabelsData"); if (_instance == null) { _instance = CreateInstance<LabelsData> (); Debug.Log ("LabelsData: loaded instance from resources is null, created instance"); } } return _instance; } } } } 


LanguageManager.cs
 using UnityEngine; using System; namespace SimpleLocalizator { public static class LanguageManager{ #region Data public static bool autoDetectLanguage=true; static SystemLanguage _currentLanguage = SystemLanguage.English; private static bool init = false; #endregion #region Interface public static SystemLanguage currentLanguage { get { return _currentLanguage; } set { _currentLanguage = value; if (onLanguageChanged != null) onLanguageChanged (); } } public static Action onLanguageChanged; public static string GetString(int labelID) { return GetString(labelID, currentLanguage); } public static string GetString(int labelID, SystemLanguage language) { Init(); for (int i = 0; i < LabelsData.labels.Count; i++) { if (LabelsData.labels[i].id == labelID) { return LabelsData.labels[i].Get(language); } } return LabelsData.defaultLabel.Get(language); } #endregion #region Methods static void Init() { if (!init) { init = true; if (autoDetectLanguage){ currentLanguage = Application.systemLanguage; } else { currentLanguage = currentLanguage; } Debug.Log("LanguageManager: initialized. Current language: " + currentLanguage); } } #endregion } } 


MultiLanguageComponent.cs

 using UnityEngine; namespace SimpleLocalizator { public abstract class MultiLanguageComponent : MonoBehaviour { [SerializeField] SystemLanguage _currentLanguage = SystemLanguage.English; protected SystemLanguage currentLanguage { get { return _currentLanguage; } set { _currentLanguage = value; Refresh (); } } public void OnValidate() { currentLanguage = _currentLanguage; } void OnEnable() { OnLanguageRefresh (); LanguageManager.onLanguageChanged += OnLanguageRefresh; } void OnDisable() { LanguageManager.onLanguageChanged -= OnLanguageRefresh; } void OnLanguageRefresh() { currentLanguage = LanguageManager.currentLanguage; } protected virtual void Refresh() { } } } 


MultiLanguageTextBase.cs

 using UnityEngine; namespace SimpleLocalizator { public abstract class MultiLanguageTextBase : MultiLanguageComponent{ #region Unity scene settings [SerializeField] int _labelID; [SerializeField] bool toUpper=false; #endregion #region Interface public int labelID { get { return _labelID; } set { _labelID = value; Refresh(); } } #endregion #region Methods protected override void Refresh() { bool local = (Application.isEditor && !Application.isPlaying); string str = local ? LanguageManager.GetString(labelID, currentLanguage) : LanguageManager.GetString(labelID); if (toUpper) str = str.ToUpper(); VisualizeString(str); } protected abstract void VisualizeString(string str); #endregion } } 


MultiLanguageTextUI.cs

 using UnityEngine; using UnityEngine.UI; namespace SimpleLocalizator { [RequireComponent(typeof(Text))] public class MultiLanguageTextUI : MultiLanguageTextBase { Text _text; public Text text { get { if (_text == null && gameObject!=null) _text = GetComponent<Text> (); return _text; } } protected override void VisualizeString(string str) { if (text != null) text.text = str; } } } 

Source: https://habr.com/ru/post/341744/


All Articles