/// <summary> /// Updates every localization .CSV file associated with this /// .yarnproject file. /// </summary> /// <remarks> /// This method updates each localization file by performing the /// following operations: /// /// - Inserts new entries if they're present in the base /// localization and not in the translated localization /// /// - Removes entries if they're present in the translated /// localization and not in the base localization /// /// - Detects if a line in the base localization has changed its /// Lock value from when the translated localization was created, /// and update its Comment /// </remarks> /// <param name="serializedObject">A serialized object that /// represents a <see cref="YarnProjectImporter"/>.</param> internal static void UpdateLocalizationCSVs(YarnProjectImporter yarnProjectImporter) { if (yarnProjectImporter.CanGenerateStringsTable == false) { Debug.LogError($"Can't update localization CSVs for Yarn Project \"{yarnProjectImporter.name}\" because not every line has a tag."); return; } var baseLocalizationStrings = yarnProjectImporter.GenerateStringsTable(); var localizations = yarnProjectImporter.languagesToSourceAssets; var modifiedFiles = new List <TextAsset>(); try { AssetDatabase.StartAssetEditing(); foreach (var loc in localizations) { var fileWasChanged = UpdateLocalizationFile(baseLocalizationStrings, loc.languageID, loc.stringsFile); if (fileWasChanged) { modifiedFiles.Add(loc.stringsFile); } } if (modifiedFiles.Count > 0) { Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles.Select(f => AssetDatabase.GetAssetPath(f)))}"); } else { Debug.Log($"No files needed updating."); } } finally { AssetDatabase.StopAssetEditing(); } }
/// <summary> /// Writes a .csv file to disk at the path indicated by <paramref /// name="destination"/>, containing all of the lines found in the /// scripts referred to by <paramref name="yarnProjectImporter"/>. /// </summary> /// <remarks> /// The file generated is in a format ready to be added to the <see /// cref="YarnProjectImporter.languagesToSourceAssets"/> list. /// </remarks> /// <param name="yarnProjectImporter">The YarnProjectImporter to /// extract strings from.</param> /// <param name="destination">The path to write the file /// to.</param> /// <returns><see langword="true"/> if the file was written /// successfully, <see langword="false"/> otherwise.</returns> /// <exception cref="CsvHelper.CsvHelperException">Thrown when an /// error is encountered when generating the CSV data.</exception> /// <exception cref="IOException">Thrown when an error is /// encountered when writing the data to disk.</exception> internal static bool WriteStringsFile(string destination, YarnProjectImporter yarnProjectImporter) { // Perform a strings-only compilation to get a full strings // table, and generate the CSV. var stringTable = yarnProjectImporter.GenerateStringsTable(); // If there was an error, bail out here if (stringTable == null) { return(false); } // Convert the string tables to CSV... var outputCSV = StringTableEntry.CreateCSV(stringTable); // ...and write it to disk. File.WriteAllText(destination, outputCSV); return(true); }
internal static void AddLineTagsToFilesInYarnProject(YarnProjectImporter importer) { // First, gather all existing line tags, so that we don't // accidentally overwrite an existing one. Do this by finding // all yarn scripts in all yarn projects, and get the string // tags inside them. var allYarnFiles = // get all yarn projects across the entire project AssetDatabase.FindAssets($"t:{nameof(YarnProject)}") // Get the path for each asset's GUID .Select(guid => AssetDatabase.GUIDToAssetPath(guid)) // Get the importer for each asset at this path .Select(path => AssetImporter.GetAtPath(path)) // Ensure it's a YarnProjectImporter .OfType <YarnProjectImporter>() // Get all of their source scripts, as a single sequence .SelectMany(importerSelect => importer.sourceScripts) // Get the path for each asset .Select(sourceAsset => AssetDatabase.GetAssetPath(sourceAsset)) // get each asset importer for that path .Select(path => AssetImporter.GetAtPath(path)) // ensure that it's a YarnImporter .OfType <YarnImporter>() // get the path for each importer's asset (the compiler // will use this) .Select(importerSelect => AssetDatabase.GetAssetPath(importer)) // remove any nulls, in case any are found .Where(path => path != null); #if YARNSPINNER_DEBUG var stopwatch = System.Diagnostics.Stopwatch.StartNew(); #endif // Compile all of these, and get whatever existing string tags // they had. Do each in isolation so that we can continue even // if a file contains a parse error. var allExistingTags = allYarnFiles.SelectMany(path => { try { // Compile this script in strings-only mode to get // string entries var compilationJob = Yarn.Compiler.CompilationJob.CreateFromFiles(path); compilationJob.CompilationType = Yarn.Compiler.CompilationJob.Type.StringsOnly; var result = Yarn.Compiler.Compiler.Compile(compilationJob); return(result.StringTable.Where(i => i.Value.isImplicitTag == false).Select(i => i.Key)); } catch (Yarn.Compiler.CompilerException e) { Debug.LogWarning($"Can't check for existing line tags in {path}, because a compiler exception was thrown: {e}"); return(new string[] { }); } }).ToList(); // immediately execute this query so we can determine timing information #if YARNSPINNER_DEBUG stopwatch.Stop(); Debug.Log($"Checked {allYarnFiles.Count()} yarn files for line tags in {stopwatch.ElapsedMilliseconds}ms"); #endif var modifiedFiles = new List <string>(); try { AssetDatabase.StartAssetEditing(); foreach (var script in importer.sourceScripts) { var assetPath = AssetDatabase.GetAssetPath(script); var contents = File.ReadAllText(assetPath); // Produce a version of this file that contains line // tags added where they're needed. var taggedVersion = Yarn.Compiler.Utility.AddTagsToLines(contents, allExistingTags); // If this produced a modified version of the file, // write it out and re-import it. if (contents != taggedVersion) { modifiedFiles.Add(Path.GetFileNameWithoutExtension(assetPath)); File.WriteAllText(assetPath, taggedVersion, System.Text.Encoding.UTF8); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.Default); } } } catch (System.Exception e) { Debug.LogError($"Encountered an error when updating scripts: {e}"); return; } finally { AssetDatabase.StopAssetEditing(); } // Report on the work we did. if (modifiedFiles.Count > 0) { Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles)}"); } else { Debug.Log("No files needed updating."); } }
public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.Space(); if (sourceScriptsProperty.arraySize == 0) { EditorGUILayout.HelpBox("This Yarn Project has no content. Add Yarn Scripts to it.", MessageType.Warning); } EditorGUILayout.PropertyField(sourceScriptsProperty, true); EditorGUILayout.Space(); if (string.IsNullOrEmpty(compileErrorProperty.stringValue) == false) { EditorGUILayout.HelpBox(compileErrorProperty.stringValue, MessageType.Error); } serializedDeclarationsList.DrawLayout(); EditorGUILayout.PropertyField(defaultLanguageProperty, new GUIContent("Default Language")); CurrentProjectDefaultLanguageProperty = defaultLanguageProperty; EditorGUILayout.PropertyField(languagesToSourceAssetsProperty); CurrentProjectDefaultLanguageProperty = null; YarnProjectImporter yarnProjectImporter = serializedObject.targetObject as YarnProjectImporter; // Ask the project importer if it can generate a strings table. // This involves querying several assets, which means various // exceptions might get thrown, which we'll catch and log (if // we're in debug mode). bool canGenerateStringsTable; try { canGenerateStringsTable = yarnProjectImporter.CanGenerateStringsTable; } catch (System.Exception e) { #if YARNSPINNER_DEBUG Debug.LogWarning($"Encountered in error when checking to see if Yarn Project Importer could generate a strings table: {e}", this); #endif canGenerateStringsTable = false; } using (new EditorGUI.DisabledScope(canGenerateStringsTable == false)) { if (GUILayout.Button("Export Strings as CSV")) { var currentPath = AssetDatabase.GetAssetPath(serializedObject.targetObject); var currentFileName = Path.GetFileNameWithoutExtension(currentPath); var currentDirectory = Path.GetDirectoryName(currentPath); var destinationPath = EditorUtility.SaveFilePanel("Export Strings CSV", currentDirectory, $"{currentFileName}.csv", "csv"); if (string.IsNullOrEmpty(destinationPath) == false) { // Generate the file on disk YarnProjectUtility.WriteStringsFile(destinationPath, yarnProjectImporter); // destinationPath may have been inside our Assets // directory, so refresh the asset database AssetDatabase.Refresh(); } } if (yarnProjectImporter.languagesToSourceAssets.Count > 0) { if (GUILayout.Button("Update Existing Strings Files")) { YarnProjectUtility.UpdateLocalizationCSVs(yarnProjectImporter); } } } // Does this project's source scripts list contain any actual // assets? (It can have a count of >0 and still have no assets // when, for example, you've just clicked the + button but // haven't dragged an asset in yet.) var hasAnyTextAssets = yarnProjectImporter.sourceScripts.Where(s => s != null).Count() > 0; using (new EditorGUI.DisabledScope(canGenerateStringsTable == true || hasAnyTextAssets == false)) { if (GUILayout.Button("Add Line Tags to Scripts")) { YarnProjectUtility.AddLineTagsToFilesInYarnProject(yarnProjectImporter); } } var hadChanges = serializedObject.ApplyModifiedProperties(); if (hadChanges) { Debug.Log($"{nameof(YarnProjectImporterEditor)} had changes"); } #if UNITY_2018 // Unity 2018's ApplyRevertGUI is buggy, and doesn't // automatically detect changes to the importer's // serializedObject. This means that we'd need to track the // state of the importer, and don't have a way to present a // Revert button. // // Rather than offer a broken experience, on Unity 2018 we // immediately reimport the changes. This is slow (we're // serializing and writing the asset to disk on every property // change!) but ensures that the writes are done. if (hadChanges) { // Manually perform the same tasks as the 'Apply' button // would ApplyAndImport(); } #endif #if UNITY_2019_1_OR_NEWER // On Unity 2019 and newer, we can use an ApplyRevertGUI that // works identically to the built-in importer inspectors. ApplyRevertGUI(); #endif }