Localizing Unity Games with the Official Localization Package

Phrase
Software Localization Tutorials
18 min readFeb 23, 2021
Learn how to use Unity’s first-party localization package to make your games available to the world.

Unity is arguably one of the most popular off-the-shelf engines for independent game developers. Breakout indies like Cuphead, Overcooked, Hollow Knight, Ori and the Blind Forest, and Monument Valley all have Unity at the heart of their technology. Even some AAA goliaths like Blizzard’s Hearthstone are made with the engine.

If you’re making commercial games with Unity, and have been kind enough to land on our little article here, you’re probably looking at expanding your game’s global reach through internationalization (i18n) and localization (l10n). So how do you go about internationalizing and localizing a Unity game?

You could roll your own solution, use an open-source library, or maybe pay for a package from the Unity Asset Store. Another option: the good people at Unity Technologies have been hard at work on a first-party localization package. It’s in preview as we write this, but it’s not far from being released according to the Unity team, so we think it’s one to consider.

In this article, we’ll go through how to use the official Unity package to localize our games. We’ll build a small demo, primarily focused on UI and text, and proceed to install, set up, and utilize the package to localize this demo.

Our Demo Project

Our demo starts with some UI that represents some messages we might typically display in a game.

Here’s a look at our hierarchy. Nothing too crazy going on here: we’re using TextMeshPro (TMP) for our text UI rendering.

🔗 Resource » Grab the Unity project from our GitHub repo. The start branch has the demo as it is here, before localizing. The main branch has the completed project after localization.

Versions of Unity & Packages Used

Here are the versions of Unity and packages we’re using in this article:

🔗 Resource » We’re using the Pixel Art GUI Elements provided by the talented Mounir Tohami. Two Google Fonts are utilized in our project as well: Jost for Latin alphabets (English and French) and Cairo for Arabic.

A Note On Addressables

The Unity localization package is built on Addressables, a system that allows us to load assets asynchronously, locally or from the network. Covering addressables is a bit outside the scope of this guide, and the localization package is designed so we don’t need to understand addressables fully before we start localizing. In those cases where we need to interact with the addressable system directly, we’ll be sure to mention it.

Installation and Setup

Ok, let’s install the localization package. In Unity’s main menu, we’ll go to Window ➞ Package Manager.

You’ll be utterly shocked to realize that this opens the Package Manager window. On this window, let’s click the plus sign and select Add package from git URL. In the ensuing text field, we’ll enter com.unity.localization and click the Add button to install the package.

Creating the Localization Settings Asset

Once the package is installed we’ll need to create our project’s localization settings. We can do this by navigating to Edit ➞ Project Settings ➞ Localization and clicking the Create button.

This will both create the settings asset and activate it in our project. If you’re organizing your Unity project by asset types, you might want to create a Localization folder to keep your settings assets file, as well as future localization assets we’ll be creating.

Adding Supported Locales

Let’s generate the locales our game will support. We can always change this later, but for now, we’ll support Arabic (ar), English (en), and French (fr). After making sure we’re at the Edit ➞ Project Settings ➞ Localization window, we can click the Locale Generator button to create our locale assets.

We’ll be presented with a list of locales; we can check the ones we want to support and click Generate.

This will create locale assets for us, which we can place in our Localization folder.

Active Locale Resolution

The localization package will attempt to resolve the active locale at runtime depending on the Locale Selectors order in our Localization Settings.

By default, the package will:

  1. look for a locale specified with a command-line flag (this could be useful for automated testing, for example), and if that fails
  2. attempt to determine the operating system locale (System Locale) and use that, and if that fails
  3. use an explicitly-set locale, which we need to provide as our default locale.

✋🏽 Heads up » The package will fall back on more generalized locales if it needs to. For example, if the locale resolution settles on en-CA (Canadian English) as the active locale, and doesn’t find that exact locale in the list of supported locales, it will attempt to fall back to the more general en if en is supported.

Setting the Default Locale

We can specify the default locale the package will use when it can’t determine the locale another way by going expanding the Specific Locale Selector section under Locale Selectors. From there, we can click the search target circle at the end of the Locale Id field and select one of our supported locales.

We’ll select English for our project. Now, if all other locale-resolution strategies fail, our game will default to English as the default runtime locale.

🗒 Note » You can alter the order of the resolution strategies by dragging their rows in the Localization Settings window. We’ll use this later to force a default locale instead of using the one set in the user’s operating system when testing our production builds.

Managing Translations

The official localization package will have us creating string table collections and populating them with translatable strings that we can then use in our components.

Creating a Strings Table Collection

To create a new collection, we can go to Window ➞ Asset Management ➞ Localization Tables and click the New Table Collection button.

After selecting which locales will be covered by the table, we can give our table a name and click Create String Table Collection. I’ve called my table UI and, when prompted, opted to save it under Localizations/Table Collections/UI. A handful of files related to this table will now live in that folder.

🗒 Note » You may have noticed a Create Asset Table Collection button on the Localization Tables window. This is because the official localization package can localize not only strings, but textures, audio, ScriptableObjects, and more. Check out the official Quick Start Guide for more info.

Adding Translated Strings

With our strings table collection in place, we can now start adding our translations to it.

Clicking Add New Entry creates a new row in our table. We should give our row a Key, which we’ll use to refer to this entry in our components. And we can add a translation string for each of our supported locales.

🗒 Note » You can open localized table collections at any time by going to Window ➞ Asset Management ➞ Localization Tables.

Right-to-Left Text

Before we go further, I want to make a short stop and tackle right-to-left (RTL) text rendering using TextMeshPro in Unity. TMP currently has no official support for RTL. The Unity team are working on it, but in the meantime, we have to use third-party packages for RTL rendering. I’ve opted to use Peyman Narimani’s open-source RTL Text Mesh Pro since it worked well for me. Let’s go over how to use it in our project.

🗒 Note » If you’re not supporting a RTL language in your game, feel free to skip this section.

Installing RTL Text Mesh Pro

To get started with RTL Text Mesh Pro (RTLTMP), we can head over to the library’s releases page and grab the latest release. The .unitypacakge file associated with the release makes for easy installation into our project.

🗒 Note » As I write this v3.3 is the latest stable release. However, there’s an issue with this release that causes Arabic numbers to be rendered right-to-left. While Arabic is a right-to-left language, its numbers are written left-to-right. So I’ve stuck with v3.2.4, which worked fine for me. By the time you read this, the RTL number issue may have been fixed in more recent releases.

✋🏽 Heads up » If you are using v3.2.4 of RTLTMP, make sure to update this line of code to match our repo to avoid compile errors in newer versions of Unity.

With the .unitypackage file on hand, we can head over to Unity and go to Assets ➞ Import Package ➞ Custom Package. We can then select the .unitypackage file, keep all the folders and files checked, and click Import. This should install the package, creating a new folder in our project called RTLTMPro.

The RTL Text Mesh Pro Component

Ok, let’s add our RTLTMP components so that we can get RTL text rendered in our game. In our hierarchy, we can right-click the game object we want to add our component to, and select one of the new UI/* - RTLTMP components.

You’ll notice that the RTLTMP package has added a corresponding RTL component for each native TextMeshPro component. Let’s add a Text - RTLTMP component.

That’s about it. The new text component can be used exactly like a normal TextMeshPro component. We just need to make sure to provide an RTL font asset under the Font Asset field. RTLTMP comes with some RTL font assets that we can use in our projects. They reside in Assets/RTLTMPro/Fonts and support at least Arabic and Farsi. We also need to add our RTL text to the RTL Text Input Box.

Works like a charm. “What about localization,” I hear you asking? Localizing RTLTMP components is exactly like localizing TextMeshPro components, and we’ll cover that next.

🗒 Note » Instead of using the font assets that come with RTLTMP, we could make our own. I’ve created a font asset based on the Cairo Google font and added it to our GitHub repo. I should mention that I found creating my own usable RTL font to be a bit tricky, and needed to tweak the font asset settings to make it render with the Arabic characters looking correctly connected (kind of 💔). If you want me to dive deeper into custom RTL font creation, let me know in the comments below.

Localizing TextMesh Pro Text

We’ve done enough setup methinks. Let us localize, fellow devs. First, we’ll create either a TMP or RTLTMP component, depending on whether or not we’re supporting RTL text (see the previous section if you are). We localize the component by adding a Localize String Event component to the game object.

The Localized String Event component is provided by the official Unity localization package. It allows us to use a translation entry from one of our string table collections, providing it to one of our other components. We do this by hooking into the Update String event of the component.

First, we’ll make sure we have a translation in our previously created string table collection. We can head over to Window ➞ Asset Management ➞ Localization Tables and make sure that the Selected Table Collection is the one we created previously (I called mine UI).

We can then click the Add New Entry button at the bottom of the window, and enter a Key and translation for our entry. I’m adding a new_ability_discovered key to my table.

Now we can head back over to our hierarchy and provide the key we just added to the String Reference field on our Localized String Event component.

We should also add an item to the Update String list. This works like a normal Unity event: we drag the game object that we want to update to the object field. In our case, this is the object that houses our TMP component. We then use the function dropdown to select TextMeshPro ➞ Text (or RTLTextMeshPro ➞ Text).

Now, when the locale changes (or when the localization system initializes), our TMP text will render its text in the active locale. We can run our game and use the debugging locale switcher in the top-right of the Game view to test this.

Our first translation. Take pride, friends. As per usual with Unity, there’s a bit of setup and learning to get localization working, but the system is quite powerful and flexible as we’ll see.

🗒 Note » A handy shortcut: instead of adding the Localized String Event component manually, we can also right-click the TMP (or RTLTMP) component and select Localize. This adds the localizing component and wires it update event automatically. We just have to select our translation key and we’re off and running.

Interpolation

We often want to include dynamic text in our translated strings. Something like “Axion55 stole the flag!”, where “Axion55” is a username that can change at runtime. Let’s see how we can use Smart Strings to achieve this with Unity localization.

We’ll add a new entry to our string table collection.

I’m adding a string that reads “{Character} has leveled up!”. We have to check the Smart checkbox above each translation that will use interpolation. Notice that {Character} is a special sequence that will be replaced at runtime. We need to use the same {foo} sequence in all the entry’s translations to have it swapped for the actual value at runtime.

We can use the Debug toggle next to a translation to see if we’ve formatted our text correctly for the Smart Strings system. We’ll get syntax highlighting in debug mode that should indicate whether our text is correct or not.

Now we can provide the actual value to our Localized String Event component so that it will be used at runtime. First, let’s create a trivial MonoBehaviour to house our value.

Assets/_Project/Scripts/Values.cs

using System;
using UnityEngine;

public class Values : MonoBehaviour
{
public string Character = "Tinka";
}

Next, let’s wire this to our Localized String Event component.

We can add the Values script as a component to our game object and drag it into the Format Arguments collection in our Localized String Event component. Of course, we also need to make sure that we’ve selected the correct key in the String Reference field of our Localized String Event. Now our TMP component will show our translations with the Values.Character value interpolated.

🗒 Note » The string in the value-providing component must have the exact same name as the variable in our translation string (Character in the previous example).

This gives us basic interpolation, but it won’t update the TMP text if Values.Character changes at runtime. We’ll explore how to update translated text when its variable dependencies change when we add more sections to this article in the coming weeks, so check back soon.

✋🏽 Heads up » If you’re using RTLTMP (see above), make sure to select the Force Fix option on the component if your text begins with left-to-right text. Otherwise all the text the component renders will be left-to-right.

🔗 Resource » Unity’s localization package uses a fork of the popular C# Smart Strings library for its dynamic string formatting. Smart Strings is a drop-in replacement for the native .NET string.Format(). This means that any format strings that we can use with string.Format can also be used with Smart Strings. We go into this a bit later when we cover number and date formatting.

Plurals

“You have found a golden swords!” Oops. We can do better with our plurals. Luckily, Smart Strings have excellent support for dynamic plural strings. Let’s add a new key to our string table collection to see this in action.

We’re interpolating a count value in our translations. Let’s take a look at the English format and break it down.

Your party has {ComboPointCount:plural:{} combo point|{} combo points}.

ComboPointCount is the integer value that we’ll use to determine the plural format to use. English has two plural formats: one and other. We can add these to our translation in order, separated by a | character. {} is a placeholder that will be swapped out for the value of ComboPointCount at runtime. And we use the optional :plural: designation to make it clear what the intention of our format is. This will render as follows in English.

French, like English, has two plural forms, so its format is similar to the English one. Arabic, however, has six plural forms: zero, one, two, few, many, and other. As the figure above demonstrates, we provide those forms as we do in English, in order and separated by a |. Our Arabic translation then renders like so:

Notice that Smart Strings is smart enough to know that Arabic has six plural forms. We don’t have to do anything other than provide those forms.

✋🏽 Heads up » For each translation, make sure to provide all the language’s plural forms, or you’ll get errors when the localization library can’t find the form corresponding to the given count.

Of course, to get this rendering we need to add our new key to a Localized String Event that updates a TMP component. And just like we did in the previous Interpolation section, we must also provide this component with a modified Values MonoBehaviour that looks like the following.

Assets/_Project/Scripts/Values.cs

using System;
using UnityEngine;

public class Values : MonoBehaviour
{
public string Character = "Tinka";
public int ComboPointCount = 200;
}

🔗 Resource » The Unicode CLDR charts are an excellent listing of per-language plural rules for your perusal.

Number Formatting

Smart Strings can also be used to interpolate localized numbers. By default, numbers will get formatted per the rules of their locale. A new entry in our string table collection can help demonstrate.

In addition to the normal {foo} specifier, notice the trailing :C in our format string. This is a standard .NET format specifier that results in a localized currency value. Once we’ve wired up our Localized String Event to our TMP component and provided a Values.StolenAmount float for the runtime value, we’ll see our translations rendered as follows.

Note that we’re seeing the currency, thousands separator, and decimal separator in each locale. English (en) defaults to US English, so its currency is formatted as US dollars. French (fr) defaults to France French, so it gives us Euros. Arabic (ar) defaults to Saudi Arabian Arabic, so its currency is displayed as Saudi Riyals.

🔗 Resource » We have complete control over the formatting of our numbers because we can use all .NET format specifiers with Smart Strings. In the example above we’ve used a standard numeric format string to specify currency. We can also use custom numeric format strings to exert more granular control over our number formatting.

Date Formatting

Similar to number formatting, we can control date formats with Smart Strings as well. Let’s add a new entry to our string table collection that displays a date.

d MMM is a custom date format specifier that results in the numeric day of the month, followed by the abbreviated name of the month, in the given date. We can update our Values MonoBehaviour with a Date value and wire everything up to a Localized String Event to see the translated rendering.

/Assets/_Project/Scripts/Values.cs

using System;
using UnityEngine;

public class Values : MonoBehaviour
{
public string Character = "Tinka";
public int ComboPointCount = 200;
public float StolenAmount = 1014.99f;
public DateTime Today = DateTime.Now;
}

The above dates are presented in the calendars of their respective locales. English and French are using the Gregorian calendar whereas Arabic is using the Hijri calendar. This is because English (en), French (fr) and Arabic (ar) use the USA, France, and Saudi Arabic locales by default.

🔗 Resource » In addition to custom date formats, we of course also have standard date formats.

Previewing & Building

We’ve already touched upon the locale game view menu dropdown that we can use to preview different translations in play mode. This can be quite handy when developing.

🗒 Note » You can turn the locale game view menu dropdown on or off in Unity preferences under Localization.

What about standalone builds? Well the simplest solution to preview translations in standalone builds is to force a locale from our Locale Settings.

Setting the Locale Id in the Specific Locale Selector and moving the selector to the top of the list will ensure that our set locale will resolve as the active one at runtime.

Because the localization package uses addressables, we have to build our addressables groups before we can get our updated translations in our standalone builds. To do this, we head over to Window ➞ Asset Management ➞ Addressables ➞ Groups. From there, we can click Build ➞ New Build ➞ Default Build Script to build our addressables groups.

We can now build for our target platform to test our translations for production.

A standalone build with forced French translations

✋🏽 Heads up » During my research I experienced a known issue with translations not appearing when I was using the addressables package v1.16.16. Reverting the package back to v1.16.15 seemed to resolve the issue.

Fallback

Sometimes we will have missing translations in our projects. There a few ways to deal with this, and we can set our chosen strategy in Localization Settings.

The Missing Translation State field can be set to show a warning message instead of the translated string (default). This warning will appear in production as well. Another option is to Print Warning, which will show the warning in the console and render an empty string for the translation.

We can also check the Use Fallback checkbox to use locale fallbacks for the whole project. This will cause a translation missing in say, French, to “fall back” to its English counterpart. If we go this route we need to make sure to set our fallback locales on each local we want to fall back. Selecting one of the locale assets in our project reveals its details in the Inspector. From there we can find the Metadata collection and add a Fallback locale to it.

Now when we have a missing French translation, its less-cool English cousin will be shown to the player instead.

🗒 Note » We don’t have to set the Fallback option on a per-project level. We can choose to set it on individual Localized String Events instead.

GG WP

We hope you’ve enjoyed this guide to using the official Unity localization package to localize your Unity games. Kudos to the Unity team for constantly developing the engine and empowering developers to make awesome games. And we’re not done! Stay tuned in the coming weeks as we add more sections to this article. I’m especially looking forward to the sections around scripting and the localization package.

🔗 Resource » Get the project we’ve built here from our GitHub repo.

And if you’re looking for a professional localization platform for your growing team, check out Phrase. Built by developers for developers, Phrase features a powerful API, flexible CLI, GitHub/Bitbucket/GitLab sync, webhooks, machine translation, and a rich web-based translation console for your translation team. Check out all of Phrase’s features and sign up for a free 14-day trial to let Phrase do the heavy lifting in your localization process, keeping you focused on the creative code you love.

Originally published on The Phrase Blog.

--

--