Browse Source

Initial localisation support via CSV files

master
chrisgregan 10 years ago
parent
commit
eaa90aef75
  1. 75
      Assets/Fungus/Flowchart/Editor/LanguageEditor.cs
  2. 12
      Assets/Fungus/Flowchart/Editor/LanguageEditor.cs.meta
  3. 73
      Assets/Fungus/Flowchart/Scripts/CSVSupport.cs
  4. 12
      Assets/Fungus/Flowchart/Scripts/CSVSupport.cs.meta
  5. 379
      Assets/Fungus/Flowchart/Scripts/Language.cs
  6. 12
      Assets/Fungus/Flowchart/Scripts/Language.cs.meta
  7. 4
      Assets/Fungus/Narrative/Editor/MenuEditor.cs
  8. 6
      Assets/Fungus/Narrative/Editor/SayEditor.cs
  9. 34
      Assets/Fungus/Narrative/Scripts/Character.cs
  10. 32
      Assets/Fungus/Narrative/Scripts/Commands/Menu.cs
  11. 34
      Assets/Fungus/Narrative/Scripts/Commands/Say.cs

75
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);
}
}
}

12
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:

73
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)
{
// '(?<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' };
}
}

12
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:

379
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<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;
}
}
}

12
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:

4
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"),

6
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);

34
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<Character> activeCharacters = new List<Character>();
@ -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;
}
}
}

32
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;
}
}
}

34
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;
}
}
}
Loading…
Cancel
Save