14 March 2015

Text localization in Unity 4.6 and Unity 5

Unity's UI system used to be very limited, so most people turned to the asset store for UI plugins. One of the most popular ones is NGUI. I've been using NGUI for more than two years now. Needless to say that I was happy to hear that the developer of NGUI was going to work for Unity on a new UI system. Apparently he did that for only a year but the result I hoped for has been achieved.

Unity 4.6 and 5 have a new UI system and it is very resemblant to NGUI. I could convert my NGUI version to the unity version with minimal changes in the code. But with all new components of course.

But there was one capital feature missing: NGUI has an easy text localization system and Unity hasn't build an alternative for that. So I rewrote the localization code from NGUI so it would work with the new unity UI. I also changed a few things so it would fit my needs better, it is not a one to one conversion. I have asked permission to post it on this blog.

NGUI expects a Localization.csv file somewhere in a Resources folder. In my version you'll see there's a Resources folder containing a single asset. In the files collection of that asset you can assign any csv anywhere in your project. That way you can have multiple csv files containing localization.

The reason to do this was that the excel sheet containing the localization became very large and hard to maintain, so I wanted the ability to split it up into multiple sheets, resulting in multiple csv's.

To be able to export these sheets easily in multiple csv's I found this page which I used to write the program below. It takes an xlsx filename and an output path as input and will generate a csv for each sheet in the excel file. In my unity project I have this excel file and my build process uses this program to generate these csv's that are linked in the localization asset. That way I always have the latest version of the localization in the excel file build into the game.

[Edit]For the program below to work you need to have Excel installed and the correct oledb drivers. If you have a 64bit version of excel make sure to compile the program as 64bit or 32bit if Excel is 32bit.[/Edit]

As always this software is free to use at will, enjoy!

using System;
using System.Data;
using System.Data.OleDb;
using System.IO;

namespace Xls2CsvConverter
{
 class Program
 {
  static void Main(string[] args)
  {
   Console.Out.WriteLine("Source file: " + args[0]);
   Console.Out.WriteLine("Target path: " + args[1]);

   string excelFile = Path.GetFullPath(args[0]);
   string csvPath = Path.GetFullPath(args[1]);


   ConvertExcelToCsv(excelFile, csvPath);

   Console.Out.WriteLine("Done");
  }

  static void ConvertExcelToCsv(string excelFilePath, string csvOutputFile)
  {
   if (!File.Exists(excelFilePath)) throw new FileNotFoundException(excelFilePath);
   //if (File.Exists(csvOutputFile)) throw new ArgumentException("File exists: " + csvOutputFile);

   // connection string
   var connectionString =
    String.Format("Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties='Excel 8.0;HDR=No'",
     excelFilePath);
   var oleDbConnection = new OleDbConnection(connectionString);

   // get schema, then data
   try
   {
    oleDbConnection.Open();
    var schemaTable = oleDbConnection.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null);
    for (int i = 0; i < schemaTable.Rows.Count; ++i)
    {
     string worksheet = schemaTable.Rows[i]["table_name"].ToString().Replace("'", "");
     string sql = String.Format("select * from [{0}]", worksheet);
     var dataTable = new DataTable();
     var dataAdapter = new OleDbDataAdapter(sql, oleDbConnection);
     dataAdapter.Fill(dataTable);

     string csvFile = Path.Combine(csvOutputFile, worksheet.Replace("$", "") + ".csv");
     MakeWritable(csvFile);
     using (var wtr = new StreamWriter(csvFile))
     {
      foreach (DataRow row in dataTable.Rows)
      {
       bool firstLine = true;
       foreach (DataColumn col in dataTable.Columns)
       {
        if (!firstLine)
        {
         wtr.Write(",");
        }
        else
        {
         firstLine = false;
        }
        var data = row[col.ColumnName].ToString().Replace("\"", "\"\"");
        wtr.Write("\"{0}\"", data);
       }
       wtr.WriteLine();
      }
     }
    }
   }
   finally
   {
    // free resources
    oleDbConnection.Close();
   }
   
  }

  private static void MakeWritable(string path)
  {
   if (!File.Exists(path)) return;
   FileAttributes attributes = File.GetAttributes(path);

   if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
   {
    // Make the file RW
    attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
    File.SetAttributes(path, attributes);
   } 
  }

  private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
  {
   return attributes & ~attributesToRemove;
  }
 }
}

Edit: As per request I added a demo project here. You'll see I have a GameController class who calls Init on the text localization class and selects the correct language. In the localization asset in the Resources folder I added a demonstration CSV file.

15 comments:

Unknown said...

Download link is not working.

Alex Vanden Abeele said...

Fixed the download link, thx for telling me.

Anonymous said...

Wow, that's amazing :)
Thanks for sharing!

I admit it took me a while to realize the first cell needed to have "KEY" written on it in order to work, and that's totally on me since I forgot to read the console :P

Having an image in this post showing a spreadsheet example would be great!

Unknown said...

would you plz give an example scene to use this package, i setup everything in the way it should be but still not working for me.
Thanks

Alex Vanden Abeele said...

I've added a demo project, let me know if you still run into problems.

Unknown said...

Thanks for demo project and sharing this amazing package ;)

Unknown said...

Hi again
i have a question
is it possible to add localization file from code and not from inspector ? if localization file changes, it will disappear from elements in Localization and i have to re-add it from inspector.
Thanks

Alex Vanden Abeele said...

I don't really understand what you're trying to achieve. In our project we use this method to automatically update the localization files before we start a build:

public static void UpdateLoca()
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.FileName = Path.GetFullPath("Xls2CsvConverter.exe");
string exe = Path.GetFullPath("Assets/Loca/Localization.xlsx");
if (!File.Exists(exe))
{
Debug.LogWarning(string.Format("Failed to find {0}", exe));
return;
}
string path = Path.GetFullPath("Assets/Loca/");

startInfo.Arguments = string.Format("{0} {1}", exe, path);

try
{
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
}
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}

News Team said...

Intersting Post. Unfortunately the demo breaks under Unity 5.11. Any plans to update?

Alex Vanden Abeele said...

I've fixed the demo, thanks for telling me! The GameController initiated the localization in a Start method while that should've been an Awake method.

Unknown said...

Hi,
i have written this little function in your demo scene:
public void SelectLanguage(string name)
{
TextLocalization.SelectLanguage(name);
}
Added it to a button as event, but nothing happens on click. The value of "name" is English and the language on startup is Dutch.

What is going wrong?

Alex Vanden Abeele said...

Indeed, I never had this issue in my personal project since all my UI was a child of the text localization parent, but if this is not the case the the broadcast doesn't reach the textlocalize monobehaviours.
I changed the demo to use a delegate, but you can use any other method if you wish (in Kweetet we use the Messenger class from http://wiki.unity3d.com/index.php?title=CSharpMessenger_Extended)

Unknown said...

Thanks, this is what i needed.

I changed the Awake-Function a bit, maby someone search for that.

private void Awake () {
var syslang = Application.systemLanguage;
var isLangAvailable = false;
TextLocalization.Init();
foreach (var item in TextLocalization.AvailableLanguages)
{
if (item == syslang.ToString()) {
isLangAvailable = true;
break;
}
}
if (isLangAvailable)
{
SelectLanguage(syslang.ToString());
} else
{
SelectLanguage("English");
}
}

Unknown said...

Hello, please help me.
I use 5 scenes in my project, but when i start (play) game in console i have this warning but it stiil working.
"Localization key not found: 'Perfect'"
But when i open new scene in my project i have this error
"MissingReferenceException: The object of type 'Text' has been destroyed but you are still trying to access it. Your script schould either check if it is a null or you should not destroy the object."
and i can not switch languages. That I doing wrong? Please help me. I use Unity 5.4 and make android platform game. Sorry for my English.

Alex Vanden Abeele said...

It might be easier if you just e-mailed me about this, it's a bit weird to have a debugging conversation in comments.
Provide a sample or at least some form of call stack if possible.