Skip to content

Instantly share code, notes, and snippets.

@leventeren
Created October 11, 2023 20:41
Show Gist options
  • Save leventeren/c55ea11338ea8c4d676d71004f67d4ff to your computer and use it in GitHub Desktop.
Save leventeren/c55ea11338ea8c4d676d71004f67d4ff to your computer and use it in GitHub Desktop.
Translator
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[System.Serializable]
public struct SerializedDictionary<T, U> {
public List<T> Keys;
public List<U> Values;
public Dictionary<T, U> getDictionary()
{
Dictionary<T, U> res = new Dictionary<T, U>();
for (int i = 0; i < Keys.Count; i++)
{
res[Keys[i]] = Values[i];
}
return res;
}
public SerializedDictionary(Dictionary<T, U> dict)
{
Keys = dict.Keys.ToList();
Values = dict.Values.ToList();
}
public static implicit operator Dictionary<T, U>(SerializedDictionary<T, U> test)
{
return test.getDictionary();
}
public static implicit operator SerializedDictionary<T, U>(Dictionary<T, U> test)
{
return new SerializedDictionary<T,U>(test);
}
}
[System.Serializable]
public struct SerializedVector
{
public float x, y, z;
public Vector3 GetPos()
{
return new Vector3(x, y, z);
}
public SerializedVector(Vector3 v)
{
x = v.x;
y = v.y;
z = v.z;
}
public static implicit operator Vector3(SerializedVector test)
{
return test.GetPos();
}
public static implicit operator SerializedVector(Vector3 test)
{
return new SerializedVector(test);
}
}
[System.Serializable]
public class CSSerializedObject
{
public float serializationTime;
public int version = 0;
public bool isValid = false;
}
public class SerializableObject : MonoBehaviour
{
public virtual void Save(SerializedGame save)
{
}
public virtual void Load(SerializedGame save)
{
}
}
using UnityEngine;
/// <summary>
/// Inherit from this base class to create a singleton.
/// e.g. public class MyClassName : Singleton<MyClassName> {}
/// </summary>
public class Singleton<T> : SerializableObject where T : MonoBehaviour
{
// Check to see if we're about to be destroyed.
private static bool m_ShuttingDown = false;
private static object m_Lock = new object();
private static T m_Instance;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this.gameObject);
}
//else
//{
// m_Instance = this;
//}
}
/// <summary>
/// Access singleton instance through this propriety.
/// </summary>
public static T Instance
{
get
{
//if (m_ShuttingDown)
//{
// Debug.LogWarning("[Singleton] Instance '" + typeof(T) +
// "' already destroyed. Returning null.");
// return null;
//}
lock (m_Lock)
{
if (m_Instance == null)
{
// Search for existing instance.
m_Instance = (T)FindObjectOfType(typeof(T));
// Create new instance if one doesn't already exist.
if (m_Instance == null)
{
// Need to create a new GameObject to attach the singleton to.
var singletonObject = new GameObject();
m_Instance = singletonObject.AddComponent<T>();
singletonObject.name = typeof(T).ToString() + " (Singleton)";
// Make instance persistent.
// DontDestroyOnLoad(singletonObject);
}
}
return m_Instance;
}
}
}
private void OnApplicationQuit()
{
m_ShuttingDown = true;
}
private void OnDestroy()
{
m_ShuttingDown = true;
m_Instance = null;
}
}
// TermData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Obtains the ( audio / picture / translation ) that corresponds to a given term
public static class TermData
{
public class Terms
{
// < language, index of a non-English language's term within a array-value of termTranslations >
// English is paired to -1 to convey that it is the key and not within termTranslations's array-value
public Dictionary<string, int> languageIndicies;
// < englishText, [frenchText, italianText, ...] >
// Example: < "Nope.", ["Nan.", "No.", "Nee."..] >
public Dictionary<string, string[]> termTranslations;
public Terms()
{
languageIndicies = new Dictionary<string, int>();
termTranslations = new Dictionary<string, string[]>();
}
}
}
// TranslateText.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
[DisallowMultipleComponent]
//[RequireComponent(typeof(Text),typeof(TMP_Text))]
// Place on all t:TextMeshProUGUI or t:Text
public class TranslateText : MonoBehaviour
{
bool init = false;
string englishText;
int lastLanguageUpdated = -1;
Text txtDisplay;
TMP_Text txtDisplay_TMP;
void Start()
{
Init();
UpdateTextDisplay();
}
private void OnEnable()
{
if (init)
UpdateTextDisplay();
}
public void SetEnglishText(string newText)
{
Init();
englishText = newText;
if (txtDisplay_TMP)
{
txtDisplay_TMP.text = englishText;
}
else
{
txtDisplay.text = englishText;
}
lastLanguageUpdated = -1;
UpdateTextDisplay();
}
void Init()
{
if (init)
return;
txtDisplay = GetComponent<Text>();
txtDisplay_TMP = GetComponent<TMP_Text>();
if (!txtDisplay && !txtDisplay_TMP)
{
Debug.LogError(" no text! "+name);
}
// UnityEngine.Assertions.Assert.IsNotNull(txtDisplay);
if (txtDisplay_TMP)
{
englishText = txtDisplay_TMP.text;
}
else
{
englishText = txtDisplay.text;
}
lastLanguageUpdated = -1;
init = true;
Translator.Instance.DisplayLanguageChanged += UpdateTextDisplay;
}
internal void ManuallySetTextToLoading(int displayLanguage)
{
Init();
string[] loadingTranslations = {"Loading...", "Chargement...", "Caricamento in corso...", "Wird geladen...",
"Cargando...", "载入中...", "Loading...", "Carregando..."};
int langIndex = displayLanguage + 1;
UnityEngine.Assertions.Assert.IsTrue(langIndex >= 0 && langIndex < loadingTranslations.Length);
if (txtDisplay_TMP)
{
txtDisplay_TMP.text = loadingTranslations[langIndex];
}
else
{
txtDisplay.text = loadingTranslations[langIndex];
}
}
void UpdateTextDisplay()
{
if (this == null)
return;
Init();
Translator translator = Translator.Instance;
int displayLanguage = translator.GetUserDisplayLanguageIndex();
if (displayLanguage == lastLanguageUpdated)
return;
lastLanguageUpdated = displayLanguage;
if (translator.IsDisplayingEnglish())
{
if (txtDisplay_TMP)
{
txtDisplay_TMP.text = englishText;
}
else
{
txtDisplay.text = englishText;
}
}
else
{
string translatedText = translator.Translate(englishText.Trim(), displayLanguage);
UnityEngine.Assertions.Assert.IsNotNull(translatedText);
if (txtDisplay_TMP)
{
UnityEngine.Assertions.Assert.IsNotNull(txtDisplay_TMP, gameObject.name);
txtDisplay_TMP.text = translatedText;
}
else
{
UnityEngine.Assertions.Assert.IsNotNull(txtDisplay, gameObject.name);
txtDisplay.text = translatedText;
}
}
}
}
English 中文
en zh
Continue 继续
New Game 新游戏
Support Us 支持我们
Settings 设置
Credits 制作人员
// TranslationLoader.cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Assertions;
public class TranslationLoader : Singleton<TranslationLoader>
{
private int progress = 0;
List<string> languages = new List<string>();
System.Action<string> callbackOnCompleted;
private void Awake()
{
System.Action<string> localCallback = new System.Action<string>(delegate (string error)
{
Debug.Log("loaded translation");
//if (error == null)
//{
// ServiceLocator.instance.GetService<Translator>().SetDisplayLanguage(index);
// if (ServiceLocator.instance.GetService<Settings>().collectAllDialogueTermData)
// ServiceLocator.instance.GetService<TranslationLoader>().DebugCollectAllDialogueTermData();
//}
//else
//{
// Debug.LogError("Langauge set failed: " + error);
//}
//if (globalCallback != null)
// globalCallback(error);
});
Load(localCallback);
}
// Returns an error string (if any)
public void Load(System.Action<string> callback)
{
callbackOnCompleted = callback;
AfterDownload(Resources.Load<TextAsset>("translation").text);
// StartCoroutine(CSVDownloader.DownloadData(AfterDownload));
}
public void AfterDownload(string data)
{
if (string.IsNullOrEmpty(data))
{
Debug.LogError("Was not able to download data or retrieve stale data.");
// TODO: Display a notification that this is likely due to poor internet connectivity
}
else
{
ProcessData(data, AfterProcessData);
}
}
private void AfterProcessData(string errorMessage)
{
if (null != errorMessage)
{
Debug.LogError("Was not able to process data: " + errorMessage);
}
UnityEngine.Assertions.Assert.IsNotNull(callbackOnCompleted);
callbackOnCompleted(errorMessage);
}
public void ProcessData(string data, System.Action<string> onCompleted)
{
Translator.Instance.termData = new TermData.Terms();
// Line level
int currLineIndex = 0;
bool inQuote = false;
int linesSinceUpdate = 0;
int kLinesBetweenUpdate = 15;
// Entry level
string currEntry = "";
int currCharIndex = 0;
bool currEntryContainedQuote = false;
List<string> currLineEntries = new List<string>();
// "\r\n" means end of line and should be only occurence of '\r' (unless on macOS/iOS in which case lines ends with just \n)
char lineEnding = '\r';// Utils.IsIOS() ? '\n' : '\r';
int lineEndingLength = 2; //Utils.IsIOS() ? 1 : 2;
while (currCharIndex < data.Length)
{
if (!inQuote && (data[currCharIndex] == lineEnding))
{
// Skip the line ending
currCharIndex += lineEndingLength;
// Wrap up the last entry
// If we were in a quote, trim bordering quotation marks
if (currEntryContainedQuote)
{
currEntry = currEntry.Substring(1, currEntry.Length - 2);
}
currLineEntries.Add(currEntry);
currEntry = "";
currEntryContainedQuote = false;
// Line ended
ProcessLineFromCSV(currLineEntries, currLineIndex);
currLineIndex++;
currLineEntries = new List<string>();
linesSinceUpdate++;
if (linesSinceUpdate > kLinesBetweenUpdate)
{
linesSinceUpdate = 0;
}
}
else
{
if (data[currCharIndex] == '"')
{
inQuote = !inQuote;
currEntryContainedQuote = true;
}
// Entry level stuff
{
if (data[currCharIndex] == ',')
{
if (inQuote)
{
currEntry += data[currCharIndex];
}
else
{
// If we were in a quote, trim bordering quotation marks
if (currEntryContainedQuote)
{
currEntry = currEntry.Substring(1, currEntry.Length - 2);
}
currLineEntries.Add(currEntry);
currEntry = "";
currEntryContainedQuote = false;
}
}
else if(data[currCharIndex] == '\\')
{
currEntry += '\n';
currCharIndex++;
}
else
{
currEntry += data[currCharIndex];
}
}
currCharIndex++;
}
progress = (int)((float)currCharIndex / data.Length * 100.0f);
}
onCompleted(null);
}
// Extracts translation data from a CSV line into Manager.instance.translator.termData.termTranslations
private void ProcessLineFromCSV(List<string> currLineElements, int currLineIndex)
{
Translator translator = Translator.Instance;
// This line contains the column headers, telling us which languages are in which column
if (currLineIndex == 0)
{
languages = new List<string>();
for (int columnIndex = 0; columnIndex < currLineElements.Count; columnIndex++)
{
string currLanguage = currLineElements[columnIndex];
Assert.IsTrue((columnIndex != 0 || currLanguage == "English"), "First column first row was:" + currLanguage);
Assert.IsFalse(translator.termData.languageIndicies.ContainsKey(currLanguage));
languages.Add(currLanguage);
// English is given a -1 index to convey it is the key of termTranslations rather than within its array-value
translator.termData.languageIndicies.Add(currLanguage, columnIndex - 1);
}
UnityEngine.Assertions.Assert.IsFalse(languages.Count == 0);
}
// This is a normal node
else if (currLineElements.Count > 1)
{
string englishSpelling = null;
string[] nonEnglishSpellings = new string[languages.Count - 1];
for (int columnIndex = 0; columnIndex < currLineElements.Count; columnIndex++)
{
string currentTerm = currLineElements[columnIndex];
if (columnIndex == 0)
{
Assert.IsFalse(translator.termData.termTranslations.ContainsKey(currentTerm), "Saw this term twice: " + currentTerm);
// Note: optionally compare with case insensitivity
englishSpelling = currentTerm;
}
else
{
nonEnglishSpellings[columnIndex - 1] = currentTerm;
}
}
translator.termData.termTranslations[englishSpelling] = nonEnglishSpellings;
}
else
{
Debug.LogError("Database line did not fall into one of the expected categories.");
}
}
//public void DebugCollectAllDialogueTermData()
//{
// List<string> dialogueNames = DebugGetAllDialogueNames();
// foreach (string dialogue in dialogueNames)
// {
// var twineText = Resources.Load<TextAsset>("Conversations/" + dialogue);
// UnityEngine.Assertions.Assert.IsNotNull(twineText);
// var curDialogue = new DialogueObject.Dialogue(twineText.text);
// curDialogue.DebugTranslateAllNodes();
// }
//}
//private List<string> DebugGetAllDialogueNames()
//{
// List<string> names = new List<string>();
// string assetsFolder = Application.dataPath;
// DirectoryInfo dir = new DirectoryInfo(assetsFolder + "/Resources/Conversations/");
// FileInfo[] info = dir.GetFiles("*.txt");
// foreach (FileInfo f in info)
// {
// names.Add(f.Name.Substring(0, f.Name.IndexOf(".")));
// }
// return names;
//}
}
// Translator.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class Translator : Singleton<Translator>
{
public static readonly string kUnTranslatedTermsListPath = "unTranslatedTermsList.txt";
public delegate void OnDisplayLanguageChangedHandler();
public event OnDisplayLanguageChangedHandler DisplayLanguageChanged = delegate { };
public TermData.Terms termData;
private const int kEnglishIndex = -1;
private int currentLanguageIndex = -1;// kEnglishIndex;
private string unTranslatedTerms = "";
public int GetUserDisplayLanguageIndex()
{
return currentLanguageIndex;
}
public bool IsDisplayingEnglish()
{
return currentLanguageIndex == kEnglishIndex;
}
public void InvokeDisplayLanguageChanged()
{
DisplayLanguageChanged();
}
//public void EnsureLoadThenSetDisplayLanguage(int index, Action<string> globalCallback)
//{
// System.Action<string> localCallback = new System.Action<string>(delegate (string error)
// {
// if (error == null)
// {
// ServiceLocator.instance.GetService<Translator>().SetDisplayLanguage(index);
// if (ServiceLocator.instance.GetService<Settings>().collectAllDialogueTermData)
// ServiceLocator.instance.GetService<TranslationLoader>().DebugCollectAllDialogueTermData();
// }
// else
// {
// Debug.LogError("Langauge set failed: " + error);
// }
// if (globalCallback != null)
// globalCallback(error);
// });
// ServiceLocator.instance.GetService<Translator>().EnsureTranslationsLoaded(localCallback);
//}
private void SetDisplayLanguage(int index)
{
PlayerPrefs.SetInt("displayLanguage", index);
UnityEngine.Assertions.Assert.IsTrue(termData != null);
currentLanguageIndex = index;
DisplayLanguageChanged();
}
public bool NeedsLoad()
{
return termData == null;
}
// Returns an error string (if any)
//public void EnsureTranslationsLoaded(Action<string> callback)
//{
// if (termData == null)
// ServiceLocator.instance.GetService<TranslationLoader>().Load(callback);
// else
// callback(null);
//}
// Deprecated
private void SetDisplayLanguage(string selectedLanguage)
{
UnityEngine.Assertions.Assert.IsTrue(termData.languageIndicies.ContainsKey(selectedLanguage));
int chosenIndex = termData.languageIndicies[selectedLanguage];
SetDisplayLanguage(chosenIndex);
}
internal string Translate(string englishText, int targetLanguageIndex)
{
if (string.IsNullOrEmpty(englishText))
return "";
if (englishText == "\"\"")
return englishText;
if (englishText.Length < 2)
return englishText;
if (englishText.StartsWith("<"))
{
//Debug.LogWarning("Ignoring translation request for: " + englishText);
return englishText;
}
//if (englishText.Contains("\n"))
//UnityEngine.Assertions.Assert.IsTrue(true, "Term included line breaks: '" + englishText + "'");
// Ignore translation requests that come before the termData has loaded
if (termData == null)
return englishText;
// Note: optionally compare with case insensitivity
string englishTextLower = englishText;
if (termData.termTranslations.ContainsKey(englishTextLower))
{
if (targetLanguageIndex == kEnglishIndex)
return englishText;
bool inBounds = termData.termTranslations[englishTextLower].Length > targetLanguageIndex && targetLanguageIndex >= 0;
UnityEngine.Assertions.Assert.IsTrue(inBounds);
return termData.termTranslations[englishTextLower][targetLanguageIndex];
}
else
{
//remove later
Debug.LogWarning("No translation for: '" + englishTextLower + "'");
AddStringToListOfTermsToTranslate(englishTextLower);
//englishTextLower = "<<" + englishTextLower + ">>";
return englishTextLower;
}
}
internal string Translate(string englishText)
{
return Translate(englishText, currentLanguageIndex);
}
internal string GetDisplayLanguage()
{
foreach (var lang in termData.languageIndicies.Keys)
{
if (termData.languageIndicies[lang] == currentLanguageIndex)
return lang;
}
Debug.LogError("Display language not found");
return "";
}
void AddStringToListOfTermsToTranslate(string term)
{
if (term.Length < 2)
return;
UnityEngine.Assertions.Assert.IsFalse(termData.termTranslations.ContainsKey(term));
string unTranslatedTermsListPath = Path.Combine(Application.persistentDataPath, kUnTranslatedTermsListPath);
try
{
if (!File.Exists(unTranslatedTermsListPath))
File.Create(unTranslatedTermsListPath);
if (string.IsNullOrEmpty(unTranslatedTerms))
unTranslatedTerms = File.ReadAllText(unTranslatedTermsListPath);
if (!unTranslatedTerms.Contains(term))
{
unTranslatedTerms += term;
File.AppendAllText(unTranslatedTermsListPath, term + System.Environment.NewLine);
}
}
catch (Exception e)
{
Debug.LogError("Could not write untranslated term:" + e.Message);
}
}
internal string[] SupportedLanguages()
{
string[] keys = new string[termData.languageIndicies.Keys.Count];
termData.languageIndicies.Keys.CopyTo(keys, 0);
return keys;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment