diff --git a/Assets/Fungus/Flowchart/Editor/LanguageEditor.cs b/Assets/Fungus/Flowchart/Editor/LanguageEditor.cs new file mode 100644 index 00000000..4ed9798b --- /dev/null +++ b/Assets/Fungus/Flowchart/Editor/LanguageEditor.cs @@ -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); + } + } + +} diff --git a/Assets/Fungus/Flowchart/Editor/LanguageEditor.cs.meta b/Assets/Fungus/Flowchart/Editor/LanguageEditor.cs.meta new file mode 100644 index 00000000..5448fada --- /dev/null +++ b/Assets/Fungus/Flowchart/Editor/LanguageEditor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 73e0ffd2fe9ba4afb995e1587c027556 +timeCreated: 1427887512 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs b/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs new file mode 100644 index 00000000..f92ec78a --- /dev/null +++ b/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs @@ -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) + { + + // '(?[^'\\]*(?:\\[\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. + | ""(?[^""\\]*(?:\\[\S\s][^""\\]*)*)"" # or $2: Double quoted string, + | (?[^,'""\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' }; + } + +} diff --git a/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs.meta b/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs.meta new file mode 100644 index 00000000..2ddf37bd --- /dev/null +++ b/Assets/Fungus/Flowchart/Scripts/CSVSupport.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 4468f4dcfdfbf46b088949ea57ed6135 +timeCreated: 1427897861 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fungus/Flowchart/Scripts/Language.cs b/Assets/Fungus/Flowchart/Scripts/Language.cs new file mode 100644 index 00000000..8d7c7521 --- /dev/null +++ b/Assets/Fungus/Flowchart/Scripts/Language.cs @@ -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 keys; + + [SerializeField] + protected List 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 localizedStrings = new Dictionary(); + + // 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 localizedObjects = new List(); + + /** + * 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 languageCodes = FindLanguageCodes(); + foreach (string languageCode in languageCodes) + { + csvHeader += "," + languageCode; + } + + // Collect all the language items present in the scene + Dictionary 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 . appended + localizedStrings[stringId + "." + languageCode] = languageEntry; + } + } + } + } + + /** + * Search through the scene + */ + protected Dictionary FindLanguageItems() + { + Dictionary languageItems = new Dictionary(); + + // Export all Say and Menu commands in the scene + Flowchart[] flowcharts = GameObject.FindObjectsOfType(); + foreach (Flowchart flowchart in flowcharts) + { + Block[] blocks = flowchart.GetComponentsInChildren(); + 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(); + if (flowchart != null) + { + foreach (Say say in flowchart.GetComponentsInChildren()) + { + 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(); + if (flowchart != null) + { + foreach (Menu menu in flowchart.GetComponentsInChildren()) + { + 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 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 FindLanguageCodes() + { + // Build a list of the language codes actually in use + List languageCodes = new List(); + foreach (string key in keys) + { + string languageId = GetLanguageId(key); + if (!languageCodes.Contains(languageId)) + { + languageCodes.Add(languageId); + } + } + + return languageCodes; + } + } + +} \ No newline at end of file diff --git a/Assets/Fungus/Flowchart/Scripts/Language.cs.meta b/Assets/Fungus/Flowchart/Scripts/Language.cs.meta new file mode 100644 index 00000000..f3417a1f --- /dev/null +++ b/Assets/Fungus/Flowchart/Scripts/Language.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: e5724422a635e425bae0af9ffe2615d6 +timeCreated: 1427886378 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Fungus/Narrative/Editor/MenuEditor.cs b/Assets/Fungus/Narrative/Editor/MenuEditor.cs index e153b078..4a253caf 100644 --- a/Assets/Fungus/Narrative/Editor/MenuEditor.cs +++ b/Assets/Fungus/Narrative/Editor/MenuEditor.cs @@ -11,6 +11,7 @@ namespace Fungus public class MenuEditor : CommandEditor { protected SerializedProperty textProp; + protected SerializedProperty descriptionProp; protected SerializedProperty targetBlockProp; protected SerializedProperty hideIfVisitedProp; protected SerializedProperty setMenuDialogProp; @@ -18,6 +19,7 @@ namespace Fungus protected virtual void OnEnable() { textProp = serializedObject.FindProperty("text"); + descriptionProp = serializedObject.FindProperty("description"); targetBlockProp = serializedObject.FindProperty("targetBlock"); hideIfVisitedProp = serializedObject.FindProperty("hideIfVisited"); setMenuDialogProp = serializedObject.FindProperty("setMenuDialog"); @@ -34,6 +36,8 @@ namespace Fungus serializedObject.Update(); EditorGUILayout.PropertyField(textProp); + + EditorGUILayout.PropertyField(descriptionProp); BlockEditor.BlockField(targetBlockProp, new GUIContent("Target Block", "Block to call when option is selected"), diff --git a/Assets/Fungus/Narrative/Editor/SayEditor.cs b/Assets/Fungus/Narrative/Editor/SayEditor.cs index 76fee36a..d58f11f8 100644 --- a/Assets/Fungus/Narrative/Editor/SayEditor.cs +++ b/Assets/Fungus/Narrative/Editor/SayEditor.cs @@ -100,6 +100,7 @@ namespace Fungus protected SerializedProperty characterProp; protected SerializedProperty portraitProp; protected SerializedProperty storyTextProp; + protected SerializedProperty descriptionProp; protected SerializedProperty voiceOverClipProp; protected SerializedProperty showAlwaysProp; protected SerializedProperty showCountProp; @@ -114,6 +115,7 @@ namespace Fungus characterProp = serializedObject.FindProperty("character"); portraitProp = serializedObject.FindProperty("portrait"); storyTextProp = serializedObject.FindProperty("storyText"); + descriptionProp = serializedObject.FindProperty("description"); voiceOverClipProp = serializedObject.FindProperty("voiceOverClip"); showAlwaysProp = serializedObject.FindProperty("showAlways"); showCountProp = serializedObject.FindProperty("showCount"); @@ -170,7 +172,9 @@ namespace Fungus } EditorGUILayout.PropertyField(storyTextProp); - + + EditorGUILayout.PropertyField(descriptionProp); + EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(extendPreviousProp); diff --git a/Assets/Fungus/Narrative/Scripts/Character.cs b/Assets/Fungus/Narrative/Scripts/Character.cs index a4abf6af..de99b3f2 100644 --- a/Assets/Fungus/Narrative/Scripts/Character.cs +++ b/Assets/Fungus/Narrative/Scripts/Character.cs @@ -1,12 +1,14 @@ using UnityEngine; +using UnityEngine.Serialization; using System.Collections; using System.Collections.Generic; +using System; namespace Fungus { [ExecuteInEditMode] - public class Character : MonoBehaviour + public class Character : MonoBehaviour, ILocalizable { public string nameText; // We need a separate name as the object name is used for character variations (e.g. "Smurf Happy", "Smurf Sad") public Color nameColor = Color.white; @@ -16,8 +18,9 @@ namespace Fungus public FacingDirection portraitsFace; public PortraitState state; + [FormerlySerializedAs("notes")] [TextArea(5,10)] - public string notes; + public string description; static public List activeCharacters = new List(); @@ -33,6 +36,33 @@ namespace Fungus { activeCharacters.Remove(this); } + + // ILocalizable methods + + public virtual string GetLocalizationID() + { + return "CHARACTER." + nameText; + } + + public virtual string GetStandardText() + { + return nameText; + } + + public virtual void SetStandardText(string standardText) + { + nameText = standardText; + } + + public virtual string GetTimestamp() + { + return DateTime.Now.ToShortDateString(); + } + + public virtual string GetDescription() + { + return description; + } } } \ No newline at end of file diff --git a/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs b/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs index 763cd8d0..b82cd41b 100644 --- a/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs +++ b/Assets/Fungus/Narrative/Scripts/Commands/Menu.cs @@ -11,7 +11,7 @@ namespace Fungus "Menu", "Displays a multiple choice menu")] [AddComponentMenu("")] - public class Menu : Command + public class Menu : Command, ILocalizable { // Menu displays a menu button which will execute the target block when clicked @@ -22,6 +22,9 @@ namespace Fungus public string text = "Option Text"; + [Tooltip("Notes about the option text for other authors, localization, etc.")] + public string description = ""; + [FormerlySerializedAs("targetSequence")] public Block targetBlock; @@ -113,6 +116,33 @@ namespace Fungus { return false; } + + // ILocalizable methods + + public virtual string GetLocalizationID() + { + return "MENU." + itemId.ToString(); + } + + public virtual string GetStandardText() + { + return text; + } + + public virtual void SetStandardText(string standardText) + { + text = standardText; + } + + public virtual string GetTimestamp() + { + return DateTime.Now.ToShortDateString(); + } + + public virtual string GetDescription() + { + return description; + } } } \ No newline at end of file diff --git a/Assets/Fungus/Narrative/Scripts/Commands/Say.cs b/Assets/Fungus/Narrative/Scripts/Commands/Say.cs index 5af02575..f1982852 100644 --- a/Assets/Fungus/Narrative/Scripts/Commands/Say.cs +++ b/Assets/Fungus/Narrative/Scripts/Commands/Say.cs @@ -9,10 +9,13 @@ namespace Fungus "Say", "Writes text in a dialog box.")] [AddComponentMenu("")] - public class Say : Command + public class Say : Command, ILocalizable { [TextArea(5,10)] - public string storyText; + public string storyText = ""; + + [Tooltip("Notes about this story text for other authors, localization, etc.")] + public string description = ""; [Tooltip("Character that is speaking")] public Character character; @@ -161,6 +164,33 @@ namespace Fungus { executionCount = 0; } + + // ILocalizable methods + + public virtual string GetLocalizationID() + { + return "SAY." + itemId.ToString(); + } + + public virtual string GetStandardText() + { + return storyText; + } + + public virtual void SetStandardText(string standardText) + { + storyText = standardText; + } + + public virtual string GetTimestamp() + { + return DateTime.Now.ToShortDateString(); + } + + public virtual string GetDescription() + { + return description; + } } } \ No newline at end of file