You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
587 lines
20 KiB
587 lines
20 KiB
// This code is part of the Fungus library (http://fungusgames.com) maintained by Chris Gregan (http://twitter.com/gofungus). |
|
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE) |
|
|
|
using UnityEngine; |
|
#if UNITY_EDITOR |
|
using UnityEditor; |
|
#endif |
|
using System.Collections.Generic; |
|
using System.Text.RegularExpressions; |
|
using System.Text; |
|
using Ideafixxxer.CsvParser; |
|
|
|
namespace Fungus |
|
{ |
|
/// <summary> |
|
/// Multi-language localization support. |
|
/// </summary> |
|
public class Localization : MonoBehaviour, ISubstitutionHandler |
|
{ |
|
/// <summary> |
|
/// Temp storage for a single item of standard text and its localizations. |
|
/// </summary> |
|
protected class TextItem |
|
{ |
|
public string description = ""; |
|
public string standardText = ""; |
|
public Dictionary<string, string> localizedStrings = new Dictionary<string, string>(); |
|
} |
|
|
|
[Tooltip("Language to use at startup, usually defined by a two letter language code (e.g DE = German)")] |
|
[SerializeField] protected string activeLanguage = ""; |
|
|
|
[Tooltip("CSV file containing localization data which can be easily edited in a spreadsheet tool")] |
|
[SerializeField] protected TextAsset localizationFile; |
|
|
|
protected Dictionary<string, ILocalizable> localizeableObjects = new Dictionary<string, ILocalizable>(); |
|
|
|
protected string notificationText = ""; |
|
|
|
protected bool initialized; |
|
|
|
protected static Dictionary<string, string> localizedStrings = new Dictionary<string, string>(); |
|
|
|
#if UNITY_5_4_OR_NEWER |
|
#else |
|
public virtual void OnLevelWasLoaded(int level) |
|
{ |
|
LevelWasLoaded(); |
|
} |
|
#endif |
|
|
|
protected virtual void LevelWasLoaded() |
|
{ |
|
// Check if a language has been selected using the Set Language command in a previous scene. |
|
if (SetLanguage.mostRecentLanguage != "") |
|
{ |
|
// This language will be used when Start() is called |
|
activeLanguage = SetLanguage.mostRecentLanguage; |
|
} |
|
} |
|
|
|
private void SceneManager_activeSceneChanged(UnityEngine.SceneManagement.Scene arg0, UnityEngine.SceneManagement.Scene arg1) |
|
{ |
|
LevelWasLoaded(); |
|
} |
|
|
|
protected virtual void OnEnable() |
|
{ |
|
StringSubstituter.RegisterHandler(this); |
|
#if UNITY_5_4_OR_NEWER |
|
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += SceneManager_activeSceneChanged; |
|
#endif |
|
} |
|
|
|
protected virtual void OnDisable() |
|
{ |
|
StringSubstituter.UnregisterHandler(this); |
|
#if UNITY_5_4_OR_NEWER |
|
UnityEngine.SceneManagement.SceneManager.activeSceneChanged -= SceneManager_activeSceneChanged; |
|
#endif |
|
} |
|
|
|
protected virtual void Start() |
|
{ |
|
Init(); |
|
} |
|
|
|
/// <summary> |
|
/// String subsitution can happen during the Start of another component, so we |
|
/// may need to call Init() from other methods. |
|
/// </summary> |
|
protected virtual void Init() |
|
{ |
|
if (initialized) |
|
{ |
|
return; |
|
} |
|
|
|
CacheLocalizeableObjects(); |
|
|
|
if (localizationFile != null && |
|
localizationFile.text.Length > 0) |
|
{ |
|
SetActiveLanguage(activeLanguage); |
|
} |
|
|
|
initialized = true; |
|
} |
|
|
|
// Build a cache of all the localizeable objects in the scene |
|
protected virtual void CacheLocalizeableObjects() |
|
{ |
|
UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll(typeof(Component)); |
|
for (int i = 0; i < objects.Length; i++) |
|
{ |
|
var o = objects[i]; |
|
ILocalizable localizable = o as ILocalizable; |
|
if (localizable != null) |
|
{ |
|
localizeableObjects[localizable.GetStringId()] = localizable; |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Builds a dictionary of localizable text items in the scene. |
|
/// </summary> |
|
protected Dictionary<string, TextItem> FindTextItems() |
|
{ |
|
Dictionary<string, TextItem> textItems = new Dictionary<string, TextItem>(); |
|
|
|
// Add localizable commands in same order as command list to make it |
|
// easier to localise / edit standard text. |
|
var flowcharts = GameObject.FindObjectsOfType<Flowchart>(); |
|
for (int i = 0; i < flowcharts.Length; i++) |
|
{ |
|
var flowchart = flowcharts[i]; |
|
var blocks = flowchart.GetComponents<Block>(); |
|
|
|
for (int j = 0; j < blocks.Length; j++) |
|
{ |
|
var block = blocks[j]; |
|
var commandList = block.CommandList; |
|
for (int k = 0; k < commandList.Count; k++) |
|
{ |
|
var command = commandList[k]; |
|
ILocalizable localizable = command as ILocalizable; |
|
if (localizable != null) |
|
{ |
|
TextItem textItem = new TextItem(); |
|
textItem.standardText = localizable.GetStandardText(); |
|
textItem.description = localizable.GetDescription(); |
|
textItems[localizable.GetStringId()] = textItem; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Add everything else that's localizable (including inactive objects) |
|
UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll(typeof(Component)); |
|
for (int i = 0; i < objects.Length; i++) |
|
{ |
|
var o = objects[i]; |
|
ILocalizable localizable = o as ILocalizable; |
|
if (localizable != null) |
|
{ |
|
string stringId = localizable.GetStringId(); |
|
if (textItems.ContainsKey(stringId)) |
|
{ |
|
// Already added |
|
continue; |
|
} |
|
TextItem textItem = new TextItem(); |
|
textItem.standardText = localizable.GetStandardText(); |
|
textItem.description = localizable.GetDescription(); |
|
textItems[stringId] = textItem; |
|
} |
|
} |
|
|
|
return textItems; |
|
} |
|
|
|
/// <summary> |
|
/// Adds localized strings from CSV file data to a dictionary of text items in the scene. |
|
/// </summary> |
|
protected virtual void AddCSVDataItems(Dictionary<string, TextItem> textItems, string csvData) |
|
{ |
|
CsvParser csvParser = new CsvParser(); |
|
string[][] csvTable = csvParser.Parse(csvData); |
|
|
|
if (csvTable.Length <= 1) |
|
{ |
|
// No data rows in file |
|
return; |
|
} |
|
|
|
// Parse header row |
|
string[] columnNames = csvTable[0]; |
|
|
|
for (int i = 1; i < csvTable.Length; ++i) |
|
{ |
|
string[] fields = csvTable[i]; |
|
if (fields.Length < 3) |
|
{ |
|
// No standard text or localized string fields present |
|
continue; |
|
} |
|
|
|
string stringId = fields[0]; |
|
|
|
if (!textItems.ContainsKey(stringId)) |
|
{ |
|
if (stringId.StartsWith("CHARACTER.") || |
|
stringId.StartsWith("SAY.") || |
|
stringId.StartsWith("MENU.") || |
|
stringId.StartsWith("WRITE.") || |
|
stringId.StartsWith("SETTEXT.")) |
|
{ |
|
// If it's a 'built-in' type this probably means that item has been deleted from its flowchart, |
|
// so there's no need to add a text item for it. |
|
continue; |
|
} |
|
|
|
// Key not found. Assume it's a custom string that we want to retain, so add a text item for it. |
|
TextItem newTextItem = new TextItem(); |
|
newTextItem.description = CSVSupport.Unescape(fields[1]); |
|
newTextItem.standardText = CSVSupport.Unescape(fields[2]); |
|
textItems[stringId] = newTextItem; |
|
} |
|
|
|
TextItem textItem = textItems[stringId]; |
|
|
|
for (int j = 3; j < fields.Length; ++j) |
|
{ |
|
if (j >= columnNames.Length) |
|
{ |
|
continue; |
|
} |
|
string languageCode = columnNames[j]; |
|
string languageEntry = CSVSupport.Unescape(fields[j]); |
|
|
|
if (languageEntry.Length > 0) |
|
{ |
|
textItem.localizedStrings[languageCode] = languageEntry; |
|
} |
|
} |
|
} |
|
} |
|
|
|
#region Public members |
|
|
|
/// <summary> |
|
/// Looks up the specified string in the localized strings table. |
|
/// For this to work, a localization file and active language must have been set previously. |
|
/// Return null if the string is not found. |
|
/// </summary> |
|
public static string GetLocalizedString(string stringId) |
|
{ |
|
if (localizedStrings == null) |
|
{ |
|
return null; |
|
} |
|
|
|
if (localizedStrings.ContainsKey(stringId)) |
|
{ |
|
return localizedStrings[stringId]; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/// <summary> |
|
/// Language to use at startup, usually defined by a two letter language code (e.g DE = German). |
|
/// </summary> |
|
public virtual string ActiveLanguage { get { return activeLanguage; } } |
|
|
|
/// <summary> |
|
/// CSV file containing localization data which can be easily edited in a spreadsheet tool. |
|
/// </summary> |
|
public virtual TextAsset LocalizationFile { get { return localizationFile; } set { localizationFile = value; } } |
|
|
|
/// <summary> |
|
/// Stores any notification message from export / import methods. |
|
/// </summary> |
|
public virtual string NotificationText { get { return notificationText; } set { notificationText = value; } } |
|
|
|
/// <summary> |
|
/// Clears the cache of localizeable objects. |
|
/// </summary> |
|
public virtual void ClearLocalizeableCache() |
|
{ |
|
localizeableObjects.Clear(); |
|
} |
|
|
|
/// <summary> |
|
/// Convert all text items and localized strings to an easy to edit CSV format. |
|
/// </summary> |
|
public virtual string GetCSVData() |
|
{ |
|
// Collect all the text items present in the scene |
|
Dictionary<string, TextItem> textItems = FindTextItems(); |
|
|
|
// Update text items with localization data from CSV file |
|
if (localizationFile != null && |
|
localizationFile.text.Length > 0) |
|
{ |
|
AddCSVDataItems(textItems, localizationFile.text); |
|
} |
|
|
|
// Build CSV header row and a list of the language codes currently in use |
|
string csvHeader = "Key,Description,Standard"; |
|
var languageCodes = new List<string>(); |
|
var values = textItems.Values; |
|
foreach (var textItem in values) |
|
{ |
|
foreach (string languageCode in textItem.localizedStrings.Keys) |
|
{ |
|
if (!languageCodes.Contains(languageCode)) |
|
{ |
|
languageCodes.Add(languageCode); |
|
csvHeader += "," + languageCode; |
|
} |
|
} |
|
} |
|
|
|
// Build the CSV file using collected text items |
|
int rowCount = 0; |
|
string csvData = csvHeader + "\n"; |
|
var keys = textItems.Keys; |
|
foreach (var stringId in keys) |
|
{ |
|
TextItem textItem = textItems[stringId]; |
|
|
|
string row = CSVSupport.Escape(stringId); |
|
row += "," + CSVSupport.Escape(textItem.description); |
|
row += "," + CSVSupport.Escape(textItem.standardText); |
|
|
|
for (int i = 0; i < languageCodes.Count; i++) |
|
{ |
|
var languageCode = languageCodes[i]; |
|
if (textItem.localizedStrings.ContainsKey(languageCode)) |
|
{ |
|
row += "," + CSVSupport.Escape(textItem.localizedStrings[languageCode]); |
|
} |
|
else |
|
{ |
|
row += ","; |
|
// Empty field |
|
} |
|
} |
|
|
|
csvData += row + "\n"; |
|
rowCount++; |
|
} |
|
|
|
notificationText = "Exported " + rowCount + " localization text items."; |
|
|
|
return csvData; |
|
} |
|
|
|
/// <summary> |
|
/// Scan a localization CSV file and copies the strings for the specified language code |
|
/// into the text properties of the appropriate scene objects. |
|
/// </summary> |
|
public virtual void SetActiveLanguage(string languageCode, bool forceUpdateSceneText = false) |
|
{ |
|
if (!Application.isPlaying) |
|
{ |
|
// This function should only ever be called when the game is playing (not in editor). |
|
return; |
|
} |
|
|
|
if (localizationFile == null) |
|
{ |
|
// No localization file set |
|
return; |
|
} |
|
|
|
localizedStrings.Clear(); |
|
|
|
CsvParser csvParser = new CsvParser(); |
|
string[][] csvTable = csvParser.Parse(localizationFile.text); |
|
|
|
if (csvTable.Length <= 1) |
|
{ |
|
// No data rows in file |
|
return; |
|
} |
|
|
|
// Parse header row |
|
string[] columnNames = csvTable[0]; |
|
|
|
if (columnNames.Length < 3) |
|
{ |
|
// No languages defined in CSV file |
|
return; |
|
} |
|
|
|
// First assume standard text column and then look for a matching language column |
|
int languageIndex = 2; |
|
for (int i = 3; i < columnNames.Length; ++i) |
|
{ |
|
if (columnNames[i] == languageCode) |
|
{ |
|
languageIndex = i; |
|
break; |
|
} |
|
} |
|
|
|
if (languageIndex == 2) |
|
{ |
|
// Using standard text column |
|
// Add all strings to the localized strings dict, but don't replace standard text in the scene. |
|
// This allows string substitution to work for both standard and localized text strings. |
|
for (int i = 1; i < csvTable.Length; ++i) |
|
{ |
|
string[] fields = csvTable[i]; |
|
if (fields.Length < 3) |
|
{ |
|
continue; |
|
} |
|
|
|
localizedStrings[fields[0]] = fields[languageIndex]; |
|
} |
|
|
|
// Early out unless we've been told to force the scene text to update. |
|
// This happens when the Set Language command is used to reset back to the standard language. |
|
if (!forceUpdateSceneText) |
|
{ |
|
return; |
|
} |
|
} |
|
|
|
// Using a localized language text column |
|
// 1. Add all localized text to the localized strings dict |
|
// 2. Update all scene text properties with localized versions |
|
for (int i = 1; i < csvTable.Length; ++i) |
|
{ |
|
string[] fields = csvTable[i]; |
|
|
|
if (fields.Length < languageIndex + 1) |
|
{ |
|
continue; |
|
} |
|
|
|
string stringId = fields[0]; |
|
string languageEntry = CSVSupport.Unescape(fields[languageIndex]); |
|
|
|
if (languageEntry.Length > 0) |
|
{ |
|
localizedStrings[stringId] = languageEntry; |
|
PopulateTextProperty(stringId, languageEntry); |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Populates the text property of a single scene object with a new text value. |
|
/// </summary> |
|
public virtual bool PopulateTextProperty(string stringId, string newText) |
|
{ |
|
// Ensure that all localizeable objects have been cached |
|
if (localizeableObjects.Count == 0) |
|
{ |
|
CacheLocalizeableObjects(); |
|
} |
|
|
|
ILocalizable localizable = null; |
|
localizeableObjects.TryGetValue(stringId, out localizable); |
|
if (localizable != null) |
|
{ |
|
localizable.SetStandardText(newText); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/// <summary> |
|
/// Returns all standard text for localizeable text in the scene using an |
|
/// easy to edit custom text format. |
|
/// </summary> |
|
public virtual string GetStandardText() |
|
{ |
|
// Collect all the text items present in the scene |
|
Dictionary<string, TextItem> textItems = FindTextItems(); |
|
|
|
string textData = ""; |
|
int rowCount = 0; |
|
var keys = textItems.Keys; |
|
foreach (var stringId in keys) |
|
{ |
|
TextItem languageItem = textItems[stringId]; |
|
|
|
textData += "#" + stringId + "\n"; |
|
textData += languageItem.standardText.Trim() + "\n\n"; |
|
rowCount++; |
|
} |
|
|
|
notificationText = "Exported " + rowCount + " standard text items."; |
|
|
|
return textData; |
|
} |
|
|
|
/// <summary> |
|
/// Sets standard text on scene objects by parsing a text data file. |
|
/// </summary> |
|
public virtual void SetStandardText(string textData) |
|
{ |
|
var lines = textData.Split('\n'); |
|
|
|
int updatedCount = 0; |
|
|
|
string stringId = ""; |
|
string buffer = ""; |
|
for (int i = 0; i < lines.Length; i++) |
|
{ |
|
// Check for string id line |
|
var line = lines[i]; |
|
if (line.StartsWith("#")) |
|
{ |
|
if (stringId.Length > 0) |
|
{ |
|
// Write buffered text to the appropriate text property |
|
if (PopulateTextProperty(stringId, buffer.Trim())) |
|
{ |
|
updatedCount++; |
|
} |
|
} |
|
// Set the string id for the follow text lines |
|
stringId = line.Substring(1, line.Length - 1); |
|
buffer = ""; |
|
} |
|
else |
|
{ |
|
buffer += line + "\n"; |
|
} |
|
} |
|
|
|
// Handle last buffered entry |
|
if (stringId.Length > 0) |
|
{ |
|
if (PopulateTextProperty(stringId, buffer.Trim())) |
|
{ |
|
updatedCount++; |
|
} |
|
} |
|
|
|
notificationText = "Updated " + updatedCount + " standard text items."; |
|
} |
|
|
|
#endregion |
|
|
|
#region StringSubstituter.ISubstitutionHandler imlpementation |
|
|
|
public virtual bool SubstituteStrings(StringBuilder input) |
|
{ |
|
// This method could be called from the Start method of another component, so we |
|
// may need to initilize the localization system. |
|
Init(); |
|
|
|
// Instantiate the regular expression object. |
|
Regex r = new Regex(Flowchart.SubstituteVariableRegexString); |
|
|
|
bool modified = false; |
|
|
|
// Match the regular expression pattern against a text string. |
|
var results = r.Matches(input.ToString()); |
|
for (int i = 0; i < results.Count; i++) |
|
{ |
|
Match match = results[i]; |
|
string key = match.Value.Substring(2, match.Value.Length - 3); |
|
// Next look for matching localized string |
|
string localizedString = Localization.GetLocalizedString(key); |
|
if (localizedString != null) |
|
{ |
|
input.Replace(match.Value, localizedString); |
|
modified = true; |
|
} |
|
} |
|
|
|
return modified; |
|
} |
|
|
|
#endregion |
|
} |
|
} |