public void YarnImporter_OnValidYarnFile_GetExpectedStrings() { string fileName = Path.GetRandomFileName(); List <StringTableEntry> expectedStrings = GetExpectedStrings(fileName); string path = Application.dataPath + "/" + fileName + ".yarn"; createdFilePaths.Add(path); File.WriteAllText(path, TestYarnScriptSource); AssetDatabase.Refresh(); var result = AssetImporter.GetAtPath("Assets/" + fileName + ".yarn") as YarnImporter; // Importing this Yarn script will have produced a CSV // TextAsset containing the string table extracted from this // script. Parse that - we'll check that it contains what we // expect. var generatedStringsTable = StringTableEntry.ParseFromCSV(result.baseLanguage.text); // Simplify the results so that we can compare these string // table entries based only on specific fields System.Func <StringTableEntry, (string id, string text)> simplifier = e => (id : e.ID, text : e.Text); var simpleResult = expectedStrings.Select(simplifier); var simpleExpected = generatedStringsTable.Select(simplifier); Assert.AreEqual(simpleExpected, simpleResult); }
public static void UpdateContents(LocalizationDatabase database) { var allTextAssets = database.TrackedPrograms .SelectMany(p => p.localizations) .Select(localization => new { localization.languageName, localization.text }); foreach (var localization in database.Localizations) { if (localization == null) { // Ignore any null entries continue; } localization.Clear(); } foreach (var localizedTextAsset in allTextAssets) { string languageName = localizedTextAsset.languageName; Localization localization; try { localization = database.GetLocalization(languageName); } catch (KeyNotFoundException) { Debug.LogWarning($"{localizedTextAsset.text.name} is marked for language {languageName}, but this {nameof(LocalizationDatabase)} isn't set up for that language. TODO: offer a quick way to create one here."); continue; } TextAsset textAsset = localizedTextAsset.text; if (textAsset == null) { // A null reference. Early out here. continue; } var records = StringTableEntry.ParseFromCSV(textAsset.text); foreach (var record in records) { AddLineEntryToLocalization(localization, record); } EditorUtility.SetDirty(localization); } AssetDatabase.SaveAssets(); }
public void YarnImporterUtility_CanUpdateLocalizedCSVs_WhenBaseScriptChanges() { // Arrange: // Set up a project with a Yarn file filled with tagged lines. var project = SetUpProject(TestYarnScriptSource); var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(project)) as YarnProjectImporter; var scriptPath = AssetDatabase.GetAssetPath(importer.sourceScripts[0]); var destinationStringsFilePath = "Assets/" + Path.GetRandomFileName() + ".csv"; // Act: // Create a .CSV File, and add it to the Yarn project. YarnProjectUtility.WriteStringsFile(destinationStringsFilePath, importer); createdFilePaths.Add(destinationStringsFilePath); AssetDatabase.Refresh(); var stringsAsset = AssetDatabase.LoadAssetAtPath <TextAsset>(destinationStringsFilePath); importer.languagesToSourceAssets.Add(new YarnProjectImporter.LanguageToSourceAsset { languageID = "test", stringsFile = stringsAsset }); EditorUtility.SetDirty(importer); importer.SaveAndReimport(); // Capture the strings tables. We'll use them later. var unmodifiedBaseStringsTable = importer.GenerateStringsTable(); var unmodifiedLocalizedStringsTable = StringTableEntry.ParseFromCSV(File.ReadAllText(destinationStringsFilePath)); // Next, modify the original source script. File.WriteAllText(scriptPath, TestYarnScriptSourceModified); AssetDatabase.Refresh(); // Finally, update the CSV. LogAssert.Expect(LogType.Log, $"Updated the following files: {destinationStringsFilePath}"); YarnProjectUtility.UpdateLocalizationCSVs(importer); AssetDatabase.Refresh(); // Doing it again should result in a no-op. LogAssert.Expect(LogType.Log, "No files needed updating."); YarnProjectUtility.UpdateLocalizationCSVs(importer); // Capture the updated strings tables, so we can compare them. var modifiedBaseStringsTable = importer.GenerateStringsTable(); var modifiedLocalizedStringsTable = StringTableEntry.ParseFromCSV(File.ReadAllText(destinationStringsFilePath)); // Assert: verify the base language string table contains the // string table entries we expect, verify the localized string // table contains the string table entries we expect System.Func <StringTableEntry, string> CompareIDs = t => t.ID; System.Func <StringTableEntry, string> CompareLocks = t => t.Lock; var tests = new[] {
/// <summary> /// Returns an <see cref="IEnumerable"/> containing the string table /// entries for the base language for the specified Yarn script. /// </summary> /// <param name="serializedObject">A serialized object that represents /// a <see cref="YarnProgram"/>.</param> /// <returns>The string table entries.</returns> private static IEnumerable <StringTableEntry> GetBaseLanguageStringsForSelectedObject(SerializedObject serializedObject) { var baseLanguageProperty = serializedObject.FindProperty("baseLanguage"); // Get the TextAsset that contains the base string table CSV TextAsset textAsset = baseLanguageProperty.objectReferenceValue as TextAsset; if (textAsset == null) { throw new System.NullReferenceException($"The base language table asset for {serializedObject.targetObject.name} is either null or not a TextAsset. Did the script fail to compile?"); } var baseLanguageTableText = textAsset.text; // Parse this CSV into StringTableEntry structs return(StringTableEntry.ParseFromCSV(baseLanguageTableText) .OrderBy(entry => entry.File) .ThenBy(entry => int.Parse(entry.LineNumber))); }
public void YarnImporterUtility_CanUpdateLocalizedCSVs_WhenBaseScriptChanges() { // Arrange: Import a yarn script and create a localization // database for it, create an alternate localization for it string fileName = Path.GetRandomFileName(); string path = Path.Combine("Assets", fileName + ".yarn"); createdFilePaths.Add(path); File.WriteAllText(path, TestYarnScriptSource); AssetDatabase.Refresh(); var importer = AssetImporter.GetAtPath(path) as YarnImporter; var importerSerializedObject = new SerializedObject(importer); YarnImporterUtility.CreateNewLocalizationDatabase(importerSerializedObject); var localizationDatabaseSerializedObject = new SerializedObject(importer.localizationDatabase); LocalizationDatabaseUtility.CreateLocalizationWithLanguage(localizationDatabaseSerializedObject, AlternateLocaleCode); YarnImporterUtility.CreateLocalizationForLanguageInProgram(importerSerializedObject, AlternateLocaleCode); var unmodifiedBaseStringsTable = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).baseLanguage.text); var unmodifiedLocalizedStringsTable = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).localizations.First(l => l.languageName == AlternateLocaleCode).text.text); // Act: modify the imported script so that lines are added, // changed and deleted, and then update the localized CSV // programmatically File.WriteAllText(path, TestYarnScriptSourceModified); AssetDatabase.Refresh(); YarnImporterUtility.UpdateLocalizationCSVs(importerSerializedObject); var modifiedBaseStringsTable = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).baseLanguage.text); var modifiedLocalizedStringsTable = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).localizations.First(l => l.languageName == AlternateLocaleCode).text.text); // Assert: verify the base language string table contains the // string table entries we expect, verify the localized string // table contains the string table entries we expect System.Func <StringTableEntry, string> CompareIDs = t => t.ID; System.Func <StringTableEntry, string> CompareLocks = t => t.Lock; var tests = new[] {
public static void UpdateContents(LineDatabase database) { // First, get all scripts whose importers are configured to use // this database - we need to add them to our TrackedPrograms list foreach (var updatedGUID in database.RecentlyUpdatedGUIDs) { var path = AssetDatabase.GUIDToAssetPath(updatedGUID); if (string.IsNullOrEmpty(path)) { // The corresponding asset can't be found! No-op. continue; } var importer = AssetImporter.GetAtPath(path); if (!(importer is YarnImporter yarnImporter)) { Debug.LogWarning($"Yarn Spinner internal error: line database was told to load asset {path}, but this does not have a {nameof(YarnImporter)}. Ignoring."); continue; } var textAsset = AssetDatabase.LoadAssetAtPath <TextAsset>(path); if (textAsset == null) { Debug.LogWarning($"Yarn Spinner internal error: failed to get a {nameof(TextAsset)} at {path}. Ignoring."); continue; } if (yarnImporter.lineDatabase == database) { // We need to add or update content based on this asset. database.AddTrackedProject(textAsset); } else { // This asset used to refer to this database, but now no // longer does. Remove the reference. database.RemoveTrackedProject(textAsset); } } database.RecentlyUpdatedGUIDs.Clear(); var allTrackedImporters = database.TrackedScripts .Select(p => AssetDatabase.GetAssetPath(p)) .Select(path => AssetImporter.GetAtPath(path) as YarnImporter); var allLocalizationAssets = allTrackedImporters .Where(p => p != null) .SelectMany(p => p.AllLocalizations) .Select(localization => new { languageID = localization.languageName, text = localization.text }); // Erase the contents of all localizations, because we're about to // replace them foreach (var localization in database.Localizations) { if (localization == null) { // Ignore any null entries continue; } localization.Clear(); } foreach (var localizedTextAsset in allLocalizationAssets) { string languageName = localizedTextAsset.languageID; Localization localization; try { localization = database.GetLocalization(languageName); } catch (KeyNotFoundException) { Debug.LogWarning($"{localizedTextAsset.text.name} is marked for language {languageName}, but this {nameof(LineDatabase)} isn't set up for that language. TODO: offer a quick way to create one here."); continue; } TextAsset textAsset = localizedTextAsset.text; if (textAsset == null) { // A null reference. Early out here. continue; } var records = StringTableEntry.ParseFromCSV(textAsset.text); foreach (var record in records) { AddLineEntryToLocalization(localization, record); } EditorUtility.SetDirty(localization); } AssetDatabase.SaveAssets(); }
/// <summary> /// Verifies the TextAsset referred to by <paramref name="loc"/>, and /// updates it if necessary. /// </summary> /// <param name="baseLocalizationStrings">A collection of <see /// cref="StringTableEntry"/></param> /// <param name="language">The language that <paramref name="loc"/> /// provides strings for.false</param> /// <param name="loc">A TextAsset containing localized strings in CSV /// format.</param> /// <returns>Whether <paramref name="loc"/> was modified.</returns> private static bool UpdateLocalizationFile(IEnumerable <StringTableEntry> baseLocalizationStrings, string language, TextAsset loc) { var translatedStrings = StringTableEntry.ParseFromCSV(loc.text); // Convert both enumerables to dictionaries, for easier lookup var baseDictionary = baseLocalizationStrings.ToDictionary(entry => entry.ID); var translatedDictionary = translatedStrings.ToDictionary(entry => entry.ID); // The list of line IDs present in each localisation var baseIDs = baseLocalizationStrings.Select(entry => entry.ID); var translatedIDs = translatedStrings.Select(entry => entry.ID); // The list of line IDs that are ONLY present in each localisation var onlyInBaseIDs = baseIDs.Except(translatedIDs); var onlyInTranslatedIDs = translatedIDs.Except(baseIDs); // Tracks if the translated localisation needed modifications // (either new lines added, old lines removed, or changed lines // flagged) var modificationsNeeded = false; // Remove every entry whose ID is only present in the translated // set. This entry has been removed from the base localization. foreach (var id in onlyInTranslatedIDs.ToList()) { translatedDictionary.Remove(id); modificationsNeeded = true; } // Conversely, for every entry that is only present in the base // localisation, we need to create a new entry for it. foreach (var id in onlyInBaseIDs) { StringTableEntry baseEntry = baseDictionary[id]; var newEntry = new StringTableEntry(baseEntry) { // Empty this text, so that it's apparent that a translated // version needs to be provided. Text = string.Empty, }; translatedDictionary.Add(id, newEntry); modificationsNeeded = true; } // Finally, we need to check for any entries in the translated // localisation that: // 1. have the same line ID as one in the base, but // 2. have a different Lock (the hash of the text), which indicates // that the base text has changed. // First, get the list of IDs that are in both base and translated, // and then filter this list to any where the lock values differ var outOfDateLockIDs = baseDictionary.Keys .Intersect(translatedDictionary.Keys) .Where(id => baseDictionary[id].Lock != translatedDictionary[id].Lock); // Now loop over all of these, and update our translated dictionary // to include a note that it needs attention foreach (var id in outOfDateLockIDs) { // Get the translated entry as it currently exists var entry = translatedDictionary[id]; // Include a note that this entry is out of date entry.Text = $"(NEEDS UPDATE) {entry.Text}"; // Update the lock to match the new one entry.Lock = baseDictionary[id].Lock; // Put this modified entry back in the table translatedDictionary[id] = entry; modificationsNeeded = true; } // We're all done! if (modificationsNeeded == false) { // No changes needed to be done to the translated string table // entries. Stop here. return(false); } // We need to produce a replacement CSV file for the translated // entries. var outputStringEntries = translatedDictionary.Values .OrderBy(entry => entry.File) .ThenBy(entry => int.Parse(entry.LineNumber)); var outputCSV = StringTableEntry.CreateCSV(outputStringEntries); // Write out the replacement text to this existing file, replacing // its existing contents var outputFile = AssetDatabase.GetAssetPath(loc); File.WriteAllText(outputFile, outputCSV); // Tell the asset database that the file needs to be reimported AssetDatabase.ImportAsset(outputFile); // Signal that the file was changed return(true); }