Einfaches Plugin zur Lokalisierung von Anwendungen unter Unity3D

  • Tutorial
Ich denke, dass jeder Unity3D-Entwickler früher oder später die Notwendigkeit hat, die Anwendung in mehrere Sprachen zu lokalisieren. In jedem Fall ist es besser, dies vorab in die Architektur einzubinden, auch wenn zu Beginn der Anwendung nicht mehrere Sprachen benötigt werden.

In diesem Artikel beschreibe ich die Entwicklung eines einfachen Plug-Ins zur Lokalisierung von UI-Textkomponenten mit der Möglichkeit, die Sprache dynamisch zu ändern und Text im Editor zu bearbeiten.

Um die verwendeten Sprachen aufzulisten (in dem Artikel werden Russisch und Englisch berücksichtigt), verwenden wir die Enumeration Unity SystemLanguage.

Leider unterstützt Unity out of the box meines Wissens keine serialisierenden Dictionary- oder Schlüssel-Wert-Paar-Klassen. Um die Dinge nicht zu komplizieren, werden wir einige unserer Klassen für die Bedürfnisse des Plugins schreiben.

Übersetzung


Übersetzungsstruktur - im Wesentlichen ein Schlüssel-Wert-Paar:

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

Label


Nächster Schritt: Erstellen Sie eine Label-Klasse. Seine Aufgabe ist es, eine eindeutige Ganzzahl-ID und eine Übersetzungsliste zu speichern:

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

Weil Die Label-Klasse implementiert im Wesentlichen die Dictionary-Logik, wobei zwei öffentliche Methoden hinzugefügt werden müssen: Get und Set.
Bei der Get-Methode wird geprüft, ob eine Zeichenfolge in der angeforderten Sprache vorhanden ist. Wenn dies der Fall ist, geben Sie eine leere Zeichenfolge zurück.
Bei der Set-Methode ist es ähnlich - wenn wir eine Zeichenfolge in der gewünschten Sprache haben -, ändern wir sie, wenn nicht, fügen wir sie hinzu.

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


Alle Label-Instanzen müssen im Unity-Inspector gespeichert und bearbeitet werden. Wir werden hierfür die Unity-Tools verwenden und unsere eigene LabelsData-Klasse erstellen, die von ScriptableObject geerbt wurde. Mit ScriptableObject können Sie die erforderlichen Daten in der Dateistruktur des Projekts speichern und werden häufig als kleine Spieledatenbank verwendet.
Die LabelsData-Klasse speichert standardmäßig eine Liste mit allen Übersetzungen des Spiels und der Bezeichnung für Fehler.

Um eine Instanz von LabelsData zu erstellen, fügen wir das CreateAssetMenu-Attribut hinzu, bevor Sie die Klasse deklarieren:

[CreateAssetMenu(fileName="LabelsData", menuName="SimpleLocalizator/LabelsData")]
public class LabelsData : ScriptableObject {
	[SerializeField] List

Um die Verfügbarkeit der LabelsData-Instanz sicherzustellen, verwenden wir eine verzögerte Initialisierung gemäß der folgenden Logik:

1. Öffentliche Felder sind Getter, die die Werte der Instanzfelder zurückgeben .
2. Instanz - Ein globaler Verweis auf eine Instanz von LabelsData. Wenn es nicht initialisiert ist, wird das Laden aus dem Ressourcenordner angewendet oder eine neue Instanz von LabelsData wird erstellt.
3. Daraus folgt, dass unsere Übersetzungsdatenbank in Resources gehostet werden sollte.

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

So haben wir die Möglichkeit, unsere Übersetzungen bequem im Unity-Editor zu bearbeiten:

Screenshot


Wir können beliebig viele neue Labels erstellen, ihnen eine ID zur Identifizierung zuweisen und sie mit Übersetzungen in die erforderlichen Sprachen füllen.

Sprachmanager


Der nächste Schritt besteht darin, eine statische LanguageManager-Klasse zu schreiben, die eine komfortable Oberfläche für alle Komponenten bietet, die mehrsprachigen Inhalt anzeigen.

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;
}

Mit der Eigenschaft currentLanguage dieser Klasse können wir die aktuelle Sprache ändern, und alle Komponenten, die für das Ereignis onLanguageChanged abonniert sind, ändern ihren Inhalt.
Für die anfängliche Definition der aktuellen Systemsprache fügen wir die Init-Methode hinzu, die nur einmal aufgerufen wird:

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);
        }
}

Um die Zeile mit der gewünschten ID zu erhalten, fügen Sie die GetString-Methode hinzu, in der unter den LabelsData-Daten nach dem Label gesucht wird. Ist dies nicht der Fall, geben Sie die Standardzeichenfolge "nicht übersetzt" zurück:

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);
}

Vorlage


Jetzt müssen noch Plug-In-Komponenten geschrieben werden, die für die Anzeige von Inhalten verantwortlich sind. Welche Inhalte können wir in Unity anzeigen? Zeilen für UI.Text und TextMesh, beliebige Bilder (z. B. Symbole und Banner in russischer und englischer Sprache). Im Rahmen dieses Artikels werden mehrsprachige Zeichenfolgen für UI.Text angezeigt.

Erstellen wir eine abstrakte MultiLanguageComponent-Klasse, um den Inhalt anzuzeigen, von dem wir weiter erben werden. Die Aufgaben sind einfach: Speichern Sie die aktuelle Sprache, abonnieren Sie LanguageManager.onLanguageChanged und aktualisieren Sie den Inhalt in OnValidate (für Tests im 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() {
	}
}

Hier ist die Refresh-Methode virtuell, was wir in den abgeleiteten Klassen neu definieren werden.

Erstellen Sie eine Vererbungsklasse MultiLanguageTextBase, in der die Ganzzahl labelID gespeichert ist:

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

Definieren Sie die Refresh-Methode darin neu. Weil Refresh wird aufgerufen, wenn sich die Anwendungssprache ändert oder wenn sich die labelID ändert - darin erhalten wir eine Zeichenfolge in der gewünschten Sprache von LanguageManager und rufen die VisualizeString-Methode auf (in der die Erben die Zeichenfolge bereits mithilfe von UI.Text oder TextMesh anzeigen). Die lokale Variable wird benötigt, um zu bestimmen, ob eine Aktualisierung im Editor erfolgt, bevor die Anwendung gestartet wird. In diesem Fall erhält ein Debug vom LanguageManager eine Zeichenfolge in der aktuellen Sprache einer bestimmten Komponente und nicht in der Systemsprache.

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);

Erstellen wir die letzte Klasse MultiLanguageTextUI, die die Zeichenfolge direkt auf dem Bildschirm anzeigt und von MultiLanguageTextBase erbt. Darin überschreiben wir die VisualizeString-Methode, um Text in einer Benutzeroberfläche anzuzeigen.

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

Jetzt können wir einfach die MultiLanguageTextUI-Komponente zum Objekt mit Text hinzufügen und die gewünschte labelID festlegen:

Screenshot


Vorführung


GIF


Zusammenfassung


Somit haben wir ein einfaches Lokalisierungssystem für die Anwendung erhalten. Zukünftig können Sie von MultiLanguageComponent erben und Ihre eigenen Komponenten zur Übersetzung hinzufügen.

Repository auf GitHub (hier wurden einige zusätzliche Funktionen hinzugefügt - Export / Import in CSV, Komponenten für TextMesh, Image, AudioSource, VideoPlayer, MeshFilter).

Vollständiger 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 translations = new List();
	        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


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 ();
				return _text;
			}
		}
		protected override void VisualizeString(string str)
		{
		    if (text != null)
		        text.text = str;
		}
	}
}


Jetzt auch beliebt: