Localización usando .PO

Traducir un juego a múltiples idiomas es una forma muy efectiva de exponerlo a nuevas audiencias. Sin embargo, puede convertirse en un verdadero dolor de cabeza si no lo hace de forma extensible y mantenible. Un sistema .po es una gran manera de administrar traducciones, y es un sistema útil para saber más que solo juegos. Analizaré formas en que puede ampliar el sistema para que sea más robusto, pero la mayoría de los detalles de estos dependen demasiado de su configuración para ser detallados por completo.

Algo a tener en cuenta cuando decida traducir, es que su juego ya debería estar en un estado bastante maduro, ya que hacer traducciones (especialmente por los fanáticos) es difícil lograr que se actualicen cada vez que cambia o agrega texto.

Algunas cosas que ya debería haber configurado al entrar en esto son algún tipo de sistema de configuración, así como un sistema de interfaz de usuario y un objeto global DontDestroyOnLoad al que tiene acceso su UI.

Lo más probable es que lo actualice en algún momento, ya que incluso al escribir esto noté algunas optimizaciones que podría estar haciendo.

Traducciones

Omita esta sección si solo quiere ver el flujo de trabajo, pero encontré que este sistema es útil para que los fanáticos me traduzcan las cosas. Lo mejor que puedes hacer es crear una hoja de cálculo de Google Drive y compartir el enlace con los fanáticos, que puedes encontrar haciendo publicaciones en los distintos centros para tu juego.

El archivo .PO

¿Qué es un archivo .po (objeto portátil)?. Es una parte del estándar gettext, y se describe en el proyecto GNU de esta manera:

Un archivo PO se compone de muchas entradas, cada entrada mantiene la relación entre una cadena original no traducida y su correspondiente traducción. Todas las entradas en un archivo PO generalmente pertenecen a un solo proyecto, y todas las traducciones se expresan en un solo idioma de destino. Una entrada de archivo PO tiene la siguiente estructura esquemática:


white-space
#  translator-comments
#. extracted-comments
#: reference…
#, flag…
#| msgid previous-untranslated-string
msgid untranslated-string
msgstr translated-string

¿Y qué? ¿Es solo un archivo con una clave y un valor y algunos metadatos? No del todo, hay dos partes más para usar un sistema .po que lo haga brillar realmente. Los archivos .pot y los editores.

Un .pot (Portable Object Template) es lo que se usa para generar sus archivos .po en un editor, y se puede usar para actualizar sus archivos .po cuando agrega nuevas traducciones. Tiene la misma estructura exacta que un archivo .po normal, pero con una cadena traducida vacía.

¡No intente administrar traducciones agregando entradas a archivos .po, actualice desde .pot para garantizar la coherencia!

El editor más popular es Poedit. Que se ve así, he marcado con un círculo el botón ‘Actualizar desde POT’:

/img/c/po.jpg
.

Implementación con Unity3D

Es probable que este sistema necesite ajustes en su situación exacta, pero intentaré mantenerlo lo más genérico posible. Aunque algunos de los pasos que tendrá que averiguar sera, dónde llamar algunas cosas por su cuenta. Vamos a usar las cadenas en inglés como nuestras teclas, ¡esto es muy importante! Mantenga su .pot actualizado. La idea es que dado que el juego comenzará en inglés cada vez que podamos tomar los valores predeterminados para el cuadro de texto y usarlos como ambas claves y los valores del idioma inglés.

Los archivos principales con los que trabajaremos son:

  • template.pot - Almacenado en Recursos/Idiomas (crear si no existe)
  • spanish.po - Almacenado en Recursos/Idiomas (crear si no existe)
  • LanguageManager.cs: adjunto y referenciado por el administrador de juego global persistente
  • UILocalizeText.cs: se adjunta a cada objeto de texto de UI, controla la actualización de ese texto

Debe adjuntar UILocalizeText.cs a cada cuadro de texto que desee traducir y tener un valor para él en los archivos .po y .pot.

Convierta LanguageManger en un objeto global no destruir en la carga, llame a LoadLanguage() cuando cambie de idioma y luego UpdateAllTextBoxes() cuando necesite actualizar su UI.

Nota: desde que actualicé Unity, parece que no reconoce los archivos .po como archivos de texto y no puede encontrar los archivos de traducción. Agregar .txt después de .po lo corrige (es decir, spanish.po.txt), pero para cargarlo en Poedit debe escribir el nombre en el cuadro de diálogo.

Scripts

En los siguientes scripts todo lo que dice YourGameManager debe ser reemplazado por sus sistemas. Intenté comentar los archivos para ser bastante descriptivos.

Para configurar y usar estos: 1 - Crea tres cuadros de texto con valores que corresponden a los valores en template.pot 2 - Adjunte UILocalizeText.cs a estos cuadros de texto 3 - Init LevelManager.cs 4 - LoadLanguage (“español”) 5 - UpdateAllTextBoxes()

Template.POT


#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Test\n"
"POT-Creation-Date: 2017-02-12 09:12-0500\n"
"PO-Revision-Date: 2015-10-27 19:11-0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.11\n"
"X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-KeywordsList: --keyword[-]\n"
"X-Poedit-SearchPath-0: .\n"

msgid "GOLD"
msgstr ""

msgid "SILVER"
msgstr ""

msgid "BRONZE"
msgstr ""

Spanish.po.txt


#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Test\n"
"POT-Creation-Date: 2017-02-12 09:12-0500\n"
"PO-Revision-Date: 2015-10-27 19:11-0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.11\n"
"X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-KeywordsList: --keyword[-]\n"
"X-Poedit-SearchPath-0: .\n"

msgid "GOLD"
msgstr "ORO"

msgid "SILVER"
msgstr "PLATA"

msgid "BRONZE"
msgstr "BRONCE"

LanguajeManager.cs


using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;
using System;

public class LanguageManager : MonoBehaviour
{
    //This is where the current loaded language will go
    private static Hashtable textTable;

    [HideInInspector]
    //Just a reference for the current language, default to english
    public string CurrentLanguage = "english";

    //Run this when you are ready to start the language process, you usually want to do this after everything has loaded
    public void Init()
    {
        //You should use an enum for storing settings that are a unique list,
        //This gets a string representation of an enum, 
        CurrentLanguage = Enum.GetName(typeof(Settings.Languages), (int)YourGameManager.Settings.Language);

        //Pass that language into LoadLanguage, remember we are in init so this should only run once.
        LoadLanguage(CurrentLanguage);
    }

    //You call this when you want to update all text boxes with the new translation.
    //Run this after Init
    //Run this whenever you run LoadLanguage
    //Run this whenever you load a new scene and want to translate the new UI
    public void UpdateAllTextBoxes()
    {      
        //Find all active and inactive text boxes and loop through 'em
        UILocalizeText[] temp = Resources.FindObjectsOfTypeAll<UILocalizeText>();
        foreach (UILocalizeText text_box in temp)
        {
            //Run the update translation function on each text 
            text_box.UpdateTranslation();
        }
    }

    //Run this whenever a language changes, like in when a setting is changed - then run UpdateAllTextBoxes
    //This is based off of http://wiki.unity3d.com/index.php?title=TextManager, though heavily modified and expanded
    public void LoadLanguage(string lang)
    {
        CurrentLanguage = lang;

        if(lang == "english")
        {
            UpdateAllTextBoxes();
        }
        else if (lang != "english")
        {
            string fullpath = "Languages/" + lang + ".po"; // the file is actually ".txt" in the end

            TextAsset textAsset = (TextAsset)Resources.Load(fullpath);
            if (textAsset == null)
            {
                Debug.Log("[TextManager] " + fullpath + " file not found.");
                return;
            } else
            {
                Debug.Log("[TextManager] loading: " + fullpath);

                if (textTable == null)
                {
                    textTable = new Hashtable();
                }

                textTable.Clear();

                StringReader reader = new StringReader(textAsset.text);
                string key = null;
                string val = null;
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (line.StartsWith("msgid \""))
                    {
                        key = line.Substring(7, line.Length - 8).ToUpper();
                    }
                    else if (line.StartsWith("msgstr \""))
                    {
                        val = line.Substring(8, line.Length - 9);
                    }
                    else
                    {
                        if (key != null && val != null)
                        {
                            // TODO: add error handling here in case of duplicate keys
                            textTable.Add(key, val);
                            key = val = null;
                        }
                    }
                }

                reader.Close();
            }
        }
    }

    //This handles selecting the value from the translation array and returning it, the UILocalizeText calls this
    public string GetText(string key)
    {
        string result = "";
        if (key != null && textTable != null)
        {
            if (textTable.ContainsKey(key))
            {
                result = (string)textTable[key];

            } else
            {

            }
        }
        return (string)result;
    }
}

UILocalizeText.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UILocalizeText : MonoBehaviour {

    //This instances key, as well as the english translation
    [HideInInspector]
    public string TranslationKey = "";

    [HideInInspector]
    public Text TextToTranslate;

    //References to text values
    string OriginalText = "";
    string TranslatedText = "";

    //This gets run automatically if the original text hasn't been set when you go to update it. 
    //You shouldn't need to manually run this from anywhere
    public void Init()
    {
        //Grab the TextToTranslate if we haven't
        if(TextToTranslate == null)
            TextToTranslate = GetComponent<Text>();

        //Grab the original value of the text before we update it
        if(TextToTranslate != null)
            OriginalText = TextToTranslate.text;

        //Set the translation key to the original english text
        if(TranslationKey == "")
            TranslationKey = OriginalText;
    }

    //This gets called from LanguageManager
    //One thing I noticed is that it might be nicer to just pass in the correct string to this rather than go grap it from LanguageManager
    public void UpdateTranslation()
    {
        //If original text is empty, then this object hasn't been initiated so it should do that 
        if (OriginalText == "")
            Init();

        //If the object has no Text object then we shouldn't try to set the text so just stop
        if (TextToTranslate == null) return;

        if(YourGameManager.LanguageManager.CurrentLanguage != "english" && TranslationKey != "")
        {
            string new_text = YourGameManager.LanguageManager.GetText(TranslationKey.ToUpper());

            if (new_text != "")
            {
                TextToTranslate.text = new_text;
            } else
            {
               // Debug.Log("Key " + OriginalText + " doesn't have an entry in this language");
            }
        } else if(YourGameManager.LanguageManager.CurrentLanguage == "english")
        {
            if(TextToTranslate != null && OriginalText != null)
                TextToTranslate.text = OriginalText;
        }

    }

}

Bueno con esto, puedes hacer la prueba y comprobar que ya tienes un sistema simple para traducir tu juego a diversos idiomas.


/img/ref.png
.