11 changed files with 667 additions and 6 deletions
@ -0,0 +1,75 @@ |
|||||||
|
using UnityEditor; |
||||||
|
using UnityEngine; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using Rotorz.ReorderableList; |
||||||
|
|
||||||
|
namespace Fungus |
||||||
|
{ |
||||||
|
|
||||||
|
[CustomEditor(typeof(Language))] |
||||||
|
public class LanguageEditor : Editor |
||||||
|
{ |
||||||
|
protected SerializedProperty activeLanguageProp; |
||||||
|
protected SerializedProperty localizedObjectsProp; |
||||||
|
|
||||||
|
protected virtual void OnEnable() |
||||||
|
{ |
||||||
|
activeLanguageProp = serializedObject.FindProperty("activeLanguage"); |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnInspectorGUI() |
||||||
|
{ |
||||||
|
serializedObject.Update(); |
||||||
|
|
||||||
|
Language t = target as Language; |
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(activeLanguageProp); |
||||||
|
|
||||||
|
GUILayout.BeginHorizontal(); |
||||||
|
|
||||||
|
if (GUILayout.Button(new GUIContent("Export Strings File"))) |
||||||
|
{ |
||||||
|
ExportStrings(t); |
||||||
|
} |
||||||
|
|
||||||
|
GUILayout.Space(8); |
||||||
|
|
||||||
|
if (GUILayout.Button(new GUIContent("Import Strings File"))) |
||||||
|
{ |
||||||
|
ImportStrings(t); |
||||||
|
} |
||||||
|
|
||||||
|
GUILayout.EndHorizontal(); |
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties(); |
||||||
|
} |
||||||
|
|
||||||
|
public virtual void ExportStrings(Language language) |
||||||
|
{ |
||||||
|
string path = EditorUtility.SaveFilePanel("Export strings", "", |
||||||
|
"strings.csv", ""); |
||||||
|
if (path.Length == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
string csvData = language.ExportCSV(); |
||||||
|
File.WriteAllText(path, csvData); |
||||||
|
} |
||||||
|
|
||||||
|
public virtual void ImportStrings(Language language) |
||||||
|
{ |
||||||
|
string path = EditorUtility.OpenFilePanel("Import strings", "", ""); |
||||||
|
if (path.Length == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
string stringsFile = File.ReadAllText(path); |
||||||
|
language.ImportCSV(stringsFile); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
fileFormatVersion: 2 |
||||||
|
guid: 73e0ffd2fe9ba4afb995e1587c027556 |
||||||
|
timeCreated: 1427887512 |
||||||
|
licenseType: Free |
||||||
|
MonoImporter: |
||||||
|
serializedVersion: 2 |
||||||
|
defaultReferences: [] |
||||||
|
executionOrder: 0 |
||||||
|
icon: {instanceID: 0} |
||||||
|
userData: |
||||||
|
assetBundleName: |
||||||
|
assetBundleVariant: |
@ -0,0 +1,73 @@ |
|||||||
|
using UnityEngine; |
||||||
|
using System.Collections; |
||||||
|
using System.Text.RegularExpressions; |
||||||
|
using System.Linq; |
||||||
|
using System; |
||||||
|
|
||||||
|
namespace Fungus |
||||||
|
{ |
||||||
|
|
||||||
|
// Some CSV utilities cobbled together from stack overflow answers |
||||||
|
// CSV escape & unescape from http://stackoverflow.com/questions/769621/dealing-with-commas-in-a-csv-file |
||||||
|
// http://answers.unity3d.com/questions/144200/are-there-any-csv-reader-for-unity3d-without-needi.html |
||||||
|
public static class CSVSupport |
||||||
|
{ |
||||||
|
public static string Escape( string s ) |
||||||
|
{ |
||||||
|
s = s.Replace("\n", "\\n"); |
||||||
|
|
||||||
|
if ( s.Contains( QUOTE ) ) |
||||||
|
s = s.Replace( QUOTE, ESCAPED_QUOTE ); |
||||||
|
|
||||||
|
//if ( s.IndexOfAny( CHARACTERS_THAT_MUST_BE_QUOTED ) > -1 ) |
||||||
|
s = QUOTE + s + QUOTE; |
||||||
|
|
||||||
|
return s; |
||||||
|
} |
||||||
|
|
||||||
|
public static string Unescape( string s ) |
||||||
|
{ |
||||||
|
s = s.Replace("\\n", "\n"); |
||||||
|
|
||||||
|
if ( s.StartsWith( QUOTE ) && s.EndsWith( QUOTE ) ) |
||||||
|
{ |
||||||
|
s = s.Substring( 1, s.Length - 2 ); |
||||||
|
|
||||||
|
if ( s.Contains( ESCAPED_QUOTE ) ) |
||||||
|
s = s.Replace( ESCAPED_QUOTE, QUOTE ); |
||||||
|
} |
||||||
|
|
||||||
|
return s; |
||||||
|
} |
||||||
|
|
||||||
|
public static string[] SplitCSVLine(string line) |
||||||
|
{ |
||||||
|
|
||||||
|
// '(?<val>[^'\\]*(?:\\[\S\s][^'\\]*)*)' # Either $1: Single quoted string, |
||||||
|
|
||||||
|
string pattern = @"
|
||||||
|
# Match one value in valid CSV string. |
||||||
|
(?!\s*$) # Don't match empty last value. |
||||||
|
\s* # Strip whitespace before value. |
||||||
|
(?: # Group for value alternatives. |
||||||
|
| ""(?<val>[^""\\]*(?:\\[\S\s][^""\\]*)*)"" # or $2: Double quoted string, |
||||||
|
| (?<val>[^,'""\s\\]*(?:\s+[^,'""\s\\]+)*) # or $3: Non-comma, non-quote stuff. |
||||||
|
) # End group of value alternatives. |
||||||
|
\s* # Strip whitespace after value. |
||||||
|
(?:,|$) # Field ends on comma or EOS. |
||||||
|
";
|
||||||
|
|
||||||
|
string[] values = (from Match m in Regex.Matches(line, pattern, |
||||||
|
RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline) |
||||||
|
select m.Groups[1].Value).ToArray(); |
||||||
|
|
||||||
|
return values; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private const string QUOTE = "\""; |
||||||
|
private const string ESCAPED_QUOTE = "\"\""; |
||||||
|
// private static char[] CHARACTERS_THAT_MUST_BE_QUOTED = { ',', '"', '\n' }; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
fileFormatVersion: 2 |
||||||
|
guid: 4468f4dcfdfbf46b088949ea57ed6135 |
||||||
|
timeCreated: 1427897861 |
||||||
|
licenseType: Free |
||||||
|
MonoImporter: |
||||||
|
serializedVersion: 2 |
||||||
|
defaultReferences: [] |
||||||
|
executionOrder: 0 |
||||||
|
icon: {instanceID: 0} |
||||||
|
userData: |
||||||
|
assetBundleName: |
||||||
|
assetBundleVariant: |
@ -0,0 +1,379 @@ |
|||||||
|
using UnityEngine; |
||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace Fungus |
||||||
|
{ |
||||||
|
|
||||||
|
interface ILocalizable |
||||||
|
{ |
||||||
|
string GetLocalizationID(); |
||||||
|
|
||||||
|
string GetStandardText(); |
||||||
|
|
||||||
|
void SetStandardText(string standardText); |
||||||
|
|
||||||
|
string GetTimestamp(); |
||||||
|
|
||||||
|
string GetDescription(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Multi-language localization support. |
||||||
|
*/ |
||||||
|
public class Language : MonoBehaviour, ISerializationCallbackReceiver |
||||||
|
{ |
||||||
|
/** |
||||||
|
* Currently active language, usually defined by a two letter language code (e.g DE = German) |
||||||
|
*/ |
||||||
|
public string activeLanguage = ""; |
||||||
|
|
||||||
|
[SerializeField] |
||||||
|
protected List<string> keys; |
||||||
|
|
||||||
|
[SerializeField] |
||||||
|
protected List<string> values; |
||||||
|
|
||||||
|
// We store the localized strings in a dictionary for easy lookup, but use lists for serialization |
||||||
|
// http://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.OnBeforeSerialize.html |
||||||
|
protected Dictionary<string, string> localizedStrings = new Dictionary<string, string>(); |
||||||
|
|
||||||
|
// Gameobjects that are being managed for localization. |
||||||
|
// Each game object should have a child component that implements ISerializable |
||||||
|
// As Unity doesn't provide a persistant object identifier, we use the index |
||||||
|
// in this list as a way to uniquely identify string objects |
||||||
|
public List<GameObject> localizedObjects = new List<GameObject>(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Temp storage for a single item of standard text read from a scene object. |
||||||
|
*/ |
||||||
|
protected class LanguageItem |
||||||
|
{ |
||||||
|
public string timeStamp; |
||||||
|
public string description; |
||||||
|
public string standardText; |
||||||
|
} |
||||||
|
|
||||||
|
public virtual void Start() |
||||||
|
{ |
||||||
|
if (activeLanguage.Length > 0) |
||||||
|
{ |
||||||
|
SetActiveLanguage(activeLanguage); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Export all localized strings to an easy to edit CSV file. |
||||||
|
*/ |
||||||
|
public virtual string ExportCSV() |
||||||
|
{ |
||||||
|
// Build a list of the language codes currently in use |
||||||
|
string csvHeader = "Key,Timestamp,Description,Standard"; |
||||||
|
List<string> languageCodes = FindLanguageCodes(); |
||||||
|
foreach (string languageCode in languageCodes) |
||||||
|
{ |
||||||
|
csvHeader += "," + languageCode; |
||||||
|
} |
||||||
|
|
||||||
|
// Collect all the language items present in the scene |
||||||
|
Dictionary<string, LanguageItem> languageItems = FindLanguageItems(); |
||||||
|
|
||||||
|
// Build the CSV file using collected language items and the corresponding store localized strings |
||||||
|
string csvData = csvHeader + "\n"; |
||||||
|
foreach (string stringId in languageItems.Keys) |
||||||
|
{ |
||||||
|
LanguageItem languageItem = languageItems[stringId]; |
||||||
|
|
||||||
|
string row = CSVSupport.Escape(stringId); |
||||||
|
row += "," + CSVSupport.Escape(languageItem.timeStamp); |
||||||
|
row += "," + CSVSupport.Escape(languageItem.description); |
||||||
|
row += "," + CSVSupport.Escape(languageItem.standardText); |
||||||
|
|
||||||
|
foreach (string languageCode in languageCodes) |
||||||
|
{ |
||||||
|
string key = stringId + "." + languageCode; |
||||||
|
if (localizedStrings.ContainsKey(key)) |
||||||
|
{ |
||||||
|
row += "," + CSVSupport.Escape(localizedStrings[key]); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
row += ","; // Empty field |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
csvData += row + "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
return csvData; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Import strings from a CSV file. |
||||||
|
* 1. Any changes to standard text items will be applied to the corresponding scene object. |
||||||
|
* 2. Any localized strings will be added to the localization dictionary. |
||||||
|
*/ |
||||||
|
public virtual void ImportCSV(string csvData) |
||||||
|
{ |
||||||
|
// Split into lines |
||||||
|
// Excel on Mac exports csv files with \r line endings, so we need to support that too. |
||||||
|
string[] lines = csvData.Split('\n', '\r'); |
||||||
|
|
||||||
|
if (lines.Length == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
localizedStrings.Clear(); |
||||||
|
|
||||||
|
// Parse header row |
||||||
|
string[] columnNames = CSVSupport.SplitCSVLine(lines[0]); |
||||||
|
|
||||||
|
for (int i = 1; i < lines.Length; ++i) |
||||||
|
{ |
||||||
|
string line = lines[i]; |
||||||
|
|
||||||
|
string[] fields = CSVSupport.SplitCSVLine(line); |
||||||
|
if (fields.Length < 4) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
string stringId = fields[0]; |
||||||
|
// Ignore timestamp & notes fields |
||||||
|
string standardText = CSVSupport.Unescape(fields[3]); |
||||||
|
|
||||||
|
PopulateGameString(stringId, standardText); |
||||||
|
|
||||||
|
// Store localized string in stringDict |
||||||
|
for (int j = 4; j < fields.Length; ++j) |
||||||
|
{ |
||||||
|
if (j >= columnNames.Length) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
string languageCode = columnNames[j]; |
||||||
|
string languageEntry = CSVSupport.Unescape(fields[j]); |
||||||
|
|
||||||
|
if (languageEntry.Length > 0) |
||||||
|
{ |
||||||
|
// The dictionary key is the basic string id with .<LanguageCode> appended |
||||||
|
localizedStrings[stringId + "." + languageCode] = languageEntry; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Search through the scene |
||||||
|
*/ |
||||||
|
protected Dictionary<string, LanguageItem> FindLanguageItems() |
||||||
|
{ |
||||||
|
Dictionary<string, LanguageItem> languageItems = new Dictionary<string, LanguageItem>(); |
||||||
|
|
||||||
|
// Export all Say and Menu commands in the scene |
||||||
|
Flowchart[] flowcharts = GameObject.FindObjectsOfType<Flowchart>(); |
||||||
|
foreach (Flowchart flowchart in flowcharts) |
||||||
|
{ |
||||||
|
Block[] blocks = flowchart.GetComponentsInChildren<Block>(); |
||||||
|
foreach (Block block in blocks) |
||||||
|
{ |
||||||
|
foreach (Command command in block.commandList) |
||||||
|
{ |
||||||
|
string stringID = ""; |
||||||
|
string standardText = ""; |
||||||
|
|
||||||
|
System.Type type = command.GetType(); |
||||||
|
if (type == typeof(Say)) |
||||||
|
{ |
||||||
|
stringID = "SAY." + flowchart.name + "." + command.itemId; |
||||||
|
Say sayCommand = command as Say; |
||||||
|
standardText = sayCommand.storyText; |
||||||
|
} |
||||||
|
else if (type == typeof(Menu)) |
||||||
|
{ |
||||||
|
stringID = "MENU." + flowchart.name + "." + command.itemId; |
||||||
|
Menu menuCommand = command as Menu; |
||||||
|
standardText = menuCommand.text; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
LanguageItem languageItem = null; |
||||||
|
if (languageItems.ContainsKey(stringID)) |
||||||
|
{ |
||||||
|
languageItem = languageItems[stringID]; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
languageItem = new LanguageItem(); |
||||||
|
languageItems[stringID] = languageItem; |
||||||
|
} |
||||||
|
|
||||||
|
// Update basic properties,leaving localised strings intact |
||||||
|
languageItem.timeStamp = "10/10/2015"; |
||||||
|
languageItem.description = "Note"; |
||||||
|
languageItem.standardText = standardText; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return languageItems; |
||||||
|
} |
||||||
|
|
||||||
|
public virtual void PopulateGameString(string stringId, string text) |
||||||
|
{ |
||||||
|
string[] idParts = stringId.Split('.'); |
||||||
|
if (idParts.Length == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
string stringType = idParts[0]; |
||||||
|
if (stringType == "SAY") |
||||||
|
{ |
||||||
|
if (idParts.Length != 3) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
string flowchartName = idParts[1]; |
||||||
|
int itemId = int.Parse(idParts[2]); |
||||||
|
|
||||||
|
GameObject go = GameObject.Find(flowchartName); |
||||||
|
Flowchart flowchart = go.GetComponentInChildren<Flowchart>(); |
||||||
|
if (flowchart != null) |
||||||
|
{ |
||||||
|
foreach (Say say in flowchart.GetComponentsInChildren<Say>()) |
||||||
|
{ |
||||||
|
if (say.itemId == itemId) |
||||||
|
{ |
||||||
|
say.storyText = text; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else if (stringType == "MENU") |
||||||
|
{ |
||||||
|
if (idParts.Length != 3) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
string flowchartName = idParts[1]; |
||||||
|
int itemId = int.Parse(idParts[2]); |
||||||
|
|
||||||
|
GameObject go = GameObject.Find(flowchartName); |
||||||
|
Flowchart flowchart = go.GetComponentInChildren<Flowchart>(); |
||||||
|
if (flowchart != null) |
||||||
|
{ |
||||||
|
foreach (Menu menu in flowchart.GetComponentsInChildren<Menu>()) |
||||||
|
{ |
||||||
|
if (menu.itemId == itemId) |
||||||
|
{ |
||||||
|
menu.text = text; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public virtual void SetActiveLanguage(string languageCode) |
||||||
|
{ |
||||||
|
// This function should only ever be called when the game is playing (not in editor). |
||||||
|
// If it was called in the editor it would permanently modify the text properties in the scene objects. |
||||||
|
if (!Application.isPlaying) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
List<string> languageCodes = FindLanguageCodes(); |
||||||
|
if (!languageCodes.Contains(languageCode)) |
||||||
|
{ |
||||||
|
Debug.LogWarning("Language code " + languageCode + " not found."); |
||||||
|
} |
||||||
|
|
||||||
|
// Find all string keys that match the language code and populate the corresponding game object |
||||||
|
foreach (string key in localizedStrings.Keys) |
||||||
|
{ |
||||||
|
if (GetLanguageId(key) == languageCode) |
||||||
|
{ |
||||||
|
PopulateGameString(GetStringId(key), localizedStrings[key]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void OnBeforeSerialize() |
||||||
|
{ |
||||||
|
keys.Clear(); |
||||||
|
values.Clear(); |
||||||
|
foreach (string key in localizedStrings.Keys) |
||||||
|
{ |
||||||
|
string value = localizedStrings[key]; |
||||||
|
keys.Add(key); |
||||||
|
values.Add(value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void OnAfterDeserialize() |
||||||
|
{ |
||||||
|
// Both arrays should be the same length, but use the min length just in case |
||||||
|
int minCount = Math.Min(keys.Count, values.Count); |
||||||
|
|
||||||
|
// Populate the string dict |
||||||
|
localizedStrings.Clear(); |
||||||
|
for (int i = 0; i < minCount; ++i) |
||||||
|
{ |
||||||
|
string key = keys[i]; |
||||||
|
string value = values[i]; |
||||||
|
localizedStrings[key] = value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected virtual string GetStringId(string key) |
||||||
|
{ |
||||||
|
int lastDotIndex = key.LastIndexOf("."); |
||||||
|
if (lastDotIndex <= 0 || |
||||||
|
lastDotIndex == key.Length - 1) |
||||||
|
{ |
||||||
|
// Malformed key |
||||||
|
return ""; |
||||||
|
} |
||||||
|
|
||||||
|
return key.Substring(0, lastDotIndex); |
||||||
|
} |
||||||
|
|
||||||
|
protected virtual string GetLanguageId(string key) |
||||||
|
{ |
||||||
|
int lastDotIndex = key.LastIndexOf("."); |
||||||
|
if (lastDotIndex <= 0 || |
||||||
|
lastDotIndex == key.Length - 1) |
||||||
|
{ |
||||||
|
// Malformed key |
||||||
|
return ""; |
||||||
|
} |
||||||
|
|
||||||
|
return key.Substring(lastDotIndex + 1, key.Length - lastDotIndex - 1); |
||||||
|
} |
||||||
|
|
||||||
|
protected virtual List<string> FindLanguageCodes() |
||||||
|
{ |
||||||
|
// Build a list of the language codes actually in use |
||||||
|
List<string> languageCodes = new List<string>(); |
||||||
|
foreach (string key in keys) |
||||||
|
{ |
||||||
|
string languageId = GetLanguageId(key); |
||||||
|
if (!languageCodes.Contains(languageId)) |
||||||
|
{ |
||||||
|
languageCodes.Add(languageId); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return languageCodes; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
fileFormatVersion: 2 |
||||||
|
guid: e5724422a635e425bae0af9ffe2615d6 |
||||||
|
timeCreated: 1427886378 |
||||||
|
licenseType: Free |
||||||
|
MonoImporter: |
||||||
|
serializedVersion: 2 |
||||||
|
defaultReferences: [] |
||||||
|
executionOrder: 0 |
||||||
|
icon: {instanceID: 0} |
||||||
|
userData: |
||||||
|
assetBundleName: |
||||||
|
assetBundleVariant: |
Loading…
Reference in new issue