Localized Text with Scriptable Objects.
Updated 8 months ago
A quick tutorial on an intuitive way to localize your texts.
The common aproach is to use an external json file to save your texts, then loading the correct one and then populating your Game Objects with the necessary texts. This is for example, the aproach used in the live training:
I wanted another approach that was simpler and where I could using the Unity Editor to edit my texts.
My solution was using Scriptable Objects for it. In this quick tutorial I won’t delve into the basics of the SO topic. If you are completely new to the subject, watch the following two videos:
Let's Start: Inside Unity, make a new C# script called Language and edit it as follow:
using UnityEngine; [CreateAssetMenu(menuName = "Localization/Language")] public class Language : ScriptableObject { public string LanguageName; }
We could use Enums as well, but since this project is using scriptable objects, I'll use them all the way. This helps with less code when adding or removing languages as well.
Now go into the Project Window and Create/Localization/Language and click on it. This will create a new language. Duplicate it and name the first. "language_eng" and the second "language_ptbr" . Any language you'll have in your game will be created in this manner, but for this example only these two will suffice.
Any game will have a script with game preferences saved on it. Since this is a demo, I'll create one to explain how to save and use the Language options. Once again this settings class can be a Singleton script or a static variable as well, but I'll choose Scriptable Objects for the sake of training (and because I love them).
I'll create a new c# script with the name SystemOptions.cs as follow:
using UnityEngine; [CreateAssetMenu(menuName = "Localization/SystemOptions")] public class SystemOptions : ScriptableObject { public Language currentLanguage; }
This is enough. Go into Project Window then Create/Localization/SystemOptions . I'll name the file DefaultOptions. After creating it, click on the file and then drag and drog the language_eng to the Current Language drop box on the Inspector.
This was only the preparation for the actual localized string. Now for the actual meat of this system, let us create another C# script, called LocalString.cs and edit it as follows:
using System.Collections.Generic; using UnityEngine; [CreateAssetMenu(menuName = "Localization/LocalString")] public class LocalString : ScriptableObject { [SerializeField] SystemOptions MyOptions; [SerializeField] LocalStringWrapper English; [SerializeField] LocalStringWrapper PortugueseBrazilian; List<LocalStringWrapper> lsw ; bool initialized = false; public string Content() { if (!initialized) { lsw = new List<LocalStringWrapper>{English, PortugueseBrazilian}; initialized = true; } for (int i = 0; i < lsw.Count; i++) { if (MyOptions.currentLanguage == lsw[i].language) return lsw[i].content; } return "Language not found."; } } [System.Serializable] public class LocalStringWrapper { public Language language; public string content; }
Go to Project Window then Create/Localization/Local String . I'll call it "localString_Begin". After that drag and drop the proper language at the equivalent drop box, and type the text into the Content window. It's shown below:
Now, some of you might ask why I created the LocalStringWrapper class as it would be easier to create an array so that the designer could edit the amount of languages by himself.
The answer is simple: Human mistakes. By using the array approach the designer will initialize the amount of languages at each localstring, if one of those has less than the others, you'll have an out of range runtime error. Second, if, let's say, you populated the array with 15 languages and their respective texts, and then by mistake typed 1 at the array field and pressed enter... yeah... you just lost those 13 remaining typed fields. If you correct it to 15, all of those 13 will be duplicates of the last valid entry.
Therefore, be aware that using arrays can increase the chance of human error. I gave up this approach after losing data several times by making small mistakes like those above. Also I though that, with this wrapper class, things got clean and intuitive on the Editor, which is a big plus in my opinion. The downside is that when you are adding another language you must manually add them to the code.
For example, let's add Spanish. Go to Create/Localization/Language . Rename the file to language_spa. Now in the LocalString.cs add the following line:
[SerializeField] LocalStringWrapper Spanish;
Then edit the Content() method on the script as bellow:
public string Content() { if (!initialized) { lsw = new List<LocalStringWrapper>{English, PortugueseBrazilian, Spanish}; initialized = true; } for (int i = 0; i < lsw.Count; i++) { if (MyOptions.currentLanguage == lsw[i].language) return lsw[i].content; } return "Language not found."; }
If you paid attention, all you gotta do is to add the new LocalStringWrapper into the lsw list initialization after creating it and that's it. A new field will appear in each LocalString that you created. Drag and drop the language_spa to the Language field and type the correct text.
Believe it or not, but this is it! Now everytime you want to create any text for your game, you will Create a new LocalString and populate it or just duplicate any working one and change the "Content" Values.
Let's give an example of how to use it on your game.
Create a TextMeshPro Text on the Hierarchy. A canvas will be created as well. Also create three buttons, Name them EnglishButton, PTBRButton and SpaButton
Create a new C# Script called UseLocalString.cs and edit it as follows:
using UnityEngine; using TMPro; public class UseLocalString : MonoBehaviour { TextMeshProUGUI myText; public LocalString mystring; void Start () { myText = GetComponent<TextMeshProUGUI>(); SetText(); } void SetText() { UnityEditor.EditorUtility.SetDirty(mystring) // When editing SO in the Editor's Inspector, sometimes the changes won't be saved to the .asset right away and therefore not show in the game. Adding this line before using the .asset will make sure to prevent it. myText.text = mystring.Content(); } }
Add this script to the TextMeshPro Text Game object, and then drag and drop the localstring_Begin to the Mystring drop box.
Press Play and you will realize that the Text already changed to "Begin" which is the current language. Go out from play mode, then to the DefaultOptions of the project folder and drag and drop any of the languages onto the Current Language dropbox in the inspector.
Press play again and you'll see that the Text changes as well.
But hey, I have all these buttons and I also want to change the language text at runtime! Easy! First, lets change the SystemOptions.cs. Add the following code to it:
public delegate void LanguageChange(); public event LanguageChange OnLanguageChange; public void Subscribe(LanguageChange lang) { OnLanguageChange += lang; } public void Unsubscribe(LanguageChange lang) { OnLanguageChange -=lang; } public void ChangeLanguage( Language lang) { currentLanguage = lang; OnLanguageChange(); }
Then change the LocalString.cs as well, adding both of these methods:
public void Subscribe(SystemOptions.LanguageChange method) { MyOptions.Subscribe(method); } public void Unsubscribe(SystemOptions.LanguageChange method) { MyOptions.Unsubscribe(method); }
Done! Now for testing, edit the UseLocalString.cs as well adding the following code to it:
void OnEnable() {mystring.Subscribe(SetText);} void OnDisable() {mystring.Unsubscribe(SetText);}
As for the buttons, let's create a simple C# script called ChangeCurrentLanguage.cs as below:
using UnityEngine; public class ChangeCurrentLanguage : MonoBehaviour { public SystemOptions myOptions; public Language langToChange; public void Change() { myOptions.ChangeLanguage(langToChange); } }
Add the script to each of the buttons. Drag and drop the respective Language to langtochange drop box as well as the CurrentOptions to the myOptions drop box. Go to the OnClick() event of each button, select the current Button Game Object and then select the ChangeCurrentLanguage.Change method.
Press Play and now when you click on each button, the text language will change accordingly. Keep in mind that since this is a SO the changes done to the CurrentSettings, when done in the Unity Editor, are persistent.
That's all that I have for examples. The workflow is like mentioned above. After setting your game languages, create a LocalString and populate it using the Unity Editor and the Inspector.
When you want to use the LocalString in any Game Object or other ScriptableObject asset. Create a LocalString variable in your script and drag and drop the string you want to use in the inspector.
Use the string in any way you want to with the LocalString.Content() method. If you want for the text language to change at runtime subscribe to the OnLanguageChange event using the LocalString.Subscribe and LocalString.Unsubscribe methods during OnEnable() and OnDisable() callbacks in any MonoBehaviour or ScriptableObject.
That's all you need.
That being said, there are Pros and Cons to this approach as usual.
  • All strings in the LocalString asset will be loaded onto memory. So, if you have 15 languages in your game, each string will be loaded 15 times, including their fonts if you add them in the LocalString.cs. But if you use one json file for all the strings in your game like in the live training tutorial, odds are that this approach will still be quite lighter on memory consumption.
  • Shared State: Using the same Localstring in several GOs is cheaper with no duplicates, as all of them are pointing to the same file. If you hard code a string to a script, for example, each copy of that GO will have a different string with the same content, increasing memory consumption.
  • No External Editors or external files that must be loaded. Since everything is native, the strings will be loaded along with your Game Objects and the scene and discarded with them. Also, you can edit your text at runtime with persistant changes on the inspector, since were talking about Scriptable Objects. No more of alt tab to the text editor, saving and then starting the game again to test it. In my opinion it's a much better workflow.
Finally, this system can be improved and adapted to your game.
The first major improvement is to add a TMP Font asset to the LocalString if you want a specific font for each string.
Or just add a TMP Font asset for each language at the Language.cs file, in this way you'll have one font for each Language and manually override it when necessary.
The second is to use the LocalString outside of playmode, so that you can freely edit any scene looking at the end result. One option is just adding the [ExecuteInEditMode] attribute to any script.(
This is already a long article, so I won't delve into any of these improvements.
So that's it for this small tutorial. Hope it helps someone out there. Any corrections or critics are welcome.
Game Dev - Programmer
Manuel Bär
4 months ago
Thanks for this tutorial. I implemented something similar and wanted to have a look if others have done localisation with scriptable objects and found your blog. However, I opted for a version were I have scriptable objects for languages holding the file path of a JSON. This allows me to easily store and change values in the JSON. I also implemented that the JSON only gets loaded when needed so I dont have to load all texts from all languages on start. The individual localisedText scripts then just hold an ID which corresponds to the respective strings in the JSON. I am also using a scriptable object event system to reload the text when someone changes langauges. I see the advantage of using JSONs that if I need a new language I can just send the whole JSON to a translator who can translate everything not having to know anything at all about Unity and I only load the texts that are needed. Best regards
Manuel Bär
4 months ago
Hi Hugo
8 months ago
Game Dev
@Riddhi Sharma Okay, to answer both questions. First, dropdown menu is similar to buttons, but instead of using the onClick callback it uses the OnValueChange. It's a little more complicated and out of the scope to this tutorial so I suggest you look for videos on the matter, but the logic for this localization remains the same. Second, I tested changing the local string asset on the fly and it worked, but after some tries the bug you mentioned did showed up. It's an unity editor "bug", btw. I solved it by adding UnityEditor.EditorUtility.SetDirty(mystring) at the first line of SetText() in the UseLocalString.cs Basically you just have to tell the editor to update the .asset after making changes to it.
Riddhi Sharma
8 months ago
There's a glitch in your implementation. So if I change the content in the local string ( asset) and run it again, the text doesn't change. Let's take for example I want to edit my English Text to Hello. It still says "Bye" ( the one before). Also, I feel if I change a text in English, it should automatically change with other translations as well. Let's suppose I used the word "Bye" in English, it should also translate in Portugese and French during runtime.
Riddhi Sharma
8 months ago
Hello Hugo, I was able to implement this tutorial. However, I would like to add a dropdown menu to be able to edit the text during runtime. Could you please help?