using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using CsvHelper; using UnityEditor.Localization.Plugins.CSV.Columns; using UnityEditor.Localization.Reporting; using UnityEngine; using UnityEngine.Localization.Metadata; using static UnityEngine.Localization.Tables.SharedTableData; namespace UnityEditor.Localization.Plugins.CSV { /// /// Comma Separated Values (CSV) support. /// Used to transfer localized data and carry it from one step of the localization process to the other, /// while allowing interoperability between and among tools. /// public static class Csv { /// /// Exports all in using default column mappings generated through /// . /// /// The target that will be populated with CSV data. /// The collection to export to CSV. /// An optional reporter that can be used to provide feedback during export. /// /// This example shows how to export a to a CSV file. /// /// public static void Export(TextWriter writer, StringTableCollection collection, ITaskReporter reporter = null) { Export(writer, collection, ColumnMapping.CreateDefaultMapping(), reporter); } /// /// Exports a using to control the contents of each exported column. /// . /// /// The target that will be populated with CSV data. /// The collection to export to CSV. /// Controls what will be exported. /// The can be used to export the Key, Id and shared comments whilst can be /// used to export the values and comments for a specific . /// can be used to generate the default columns for the project. /// An optional reporter that can be used to provide feedback during export. /// /// This example shows how to configure the data you wish to export in CSV through column mappings. /// /// /// /// This example shows how to export every that contains a . /// /// /// /// This example shows how to export all to multiple CSV files. /// /// public static void Export(TextWriter writer, StringTableCollection collection, IList columnMappings, ITaskReporter reporter = null) { if (writer == null) throw new ArgumentNullException(nameof(writer)); if (collection == null) throw new ArgumentNullException(nameof(collection)); VerifyColumnMappings(columnMappings); using (var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture)) { try { if (reporter?.Started != true) reporter?.Start("Importing CSV", string.Empty); reporter?.ReportProgress("Writing Headers", 0); foreach (var cell in columnMappings) { cell.WriteBegin(collection, csvWriter); } reporter?.ReportProgress("Writing Contents", 0.1f); foreach (var row in collection.GetRowEnumeratorUnsorted()) { if (row.TableEntries[0] != null && row.TableEntries[0].SharedEntry.Metadata.HasMetadata()) continue; csvWriter.NextRecord(); foreach (var cell in columnMappings) { if (string.IsNullOrEmpty(row.KeyEntry.Key)) continue; cell.WriteRow(row.KeyEntry, row.TableEntries, csvWriter); } } foreach (var cell in columnMappings) { cell.WriteEnd(collection); } reporter?.Completed("Finished Exporting"); } catch (Exception e) { reporter?.Fail("Failed Exporting.\n" + e.Message); throw; } } } /// /// Import the CSV data into using to control what data will be imported. /// See and for further details. /// /// The source of the CSV data. /// The target collection to be updated using the CSV data. /// Should an Undo operation be created so the changes can be undone? /// An optional reporter that can be used to provide feedback during import. /// After a pull has completed, any keys that exist in the but did not exist in the CSV are considered missing, /// this may be because they have been deleted from the sheet. A value of true will remove these missing entries; false will preserve them. /// /// This example show how to import a collection with default settings. /// /// public static void ImportInto(TextReader reader, StringTableCollection collection, bool createUndo = false, ITaskReporter reporter = null, bool removeMissingEntries = false) { ImportInto(reader, collection, ColumnMapping.CreateDefaultMapping(), createUndo, reporter, removeMissingEntries); } /// /// Import the CSV data into using default column mappings generated using . /// /// /// /// /// /// /// After a pull has completed, any keys that exist in the but did not exist in the CSV are considered missing, /// this may be because they have been deleted from the sheet. A value of true will remove these missing entries; false will preserve them. /// /// This example shows how to configure the data you wish to import in CSV through column mappings. /// /// /// /// This example shows how to import every that contains a . /// /// public static void ImportInto(TextReader reader, StringTableCollection collection, IList columnMappings, bool createUndo = false, ITaskReporter reporter = null, bool removeMissingEntries = false) { if (reader == null) throw new ArgumentNullException(nameof(reader)); if (collection == null) throw new ArgumentNullException(nameof(collection)); VerifyColumnMappings(columnMappings); var modifiedAssets = collection.StringTables.Select(t => t as UnityEngine.Object).ToList(); modifiedAssets.Add(collection.SharedData); if (createUndo) Undo.RegisterCompleteObjectUndo(modifiedAssets.ToArray(), "Import CSV"); try { using (var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture)) { csvReader.Read(); csvReader.ReadHeader(); if (reporter?.Started != true) reporter?.Start("Exporting CSV", string.Empty); reporter?.ReportProgress("Mapping Headers", 0); foreach (var col in columnMappings) { col.ReadBegin(collection, csvReader); } var keyCell = columnMappings.First(o => o is IKeyColumn) as IKeyColumn; reporter?.ReportProgress("Reading Contents", 0.1f); // We want to keep track of the order the entries are pulled in so we can match it var sortedEntries = new List(); var keysProcessed = new HashSet(); while (csvReader.Read()) { var keyEntry = keyCell.ReadKey(csvReader); if (keyEntry == null) continue; sortedEntries.Add(keyEntry); keysProcessed.Add(keyEntry.Id); foreach (var cell in columnMappings) { cell.ReadRow(keyEntry, csvReader); } } foreach (var cell in columnMappings) { cell.ReadEnd(collection); } var removedEntriesLog = new StringBuilder(); collection.MergeUpdatedEntries(keysProcessed, sortedEntries, removedEntriesLog, removeMissingEntries); reporter?.ReportProgress(removedEntriesLog.ToString(), 0.9f); } modifiedAssets.ForEach(EditorUtility.SetDirty); LocalizationEditorSettings.EditorEvents.RaiseCollectionModified(null, collection); // Flush changes to disk. collection.SaveChangesToDisk(); reporter?.Completed("Finished Importing"); } catch (Exception e) { reporter?.Fail("Failed Importing.\n" + e.Message); throw; } } static void VerifyColumnMappings(IList columnMappings) { if (columnMappings == null) throw new ArgumentNullException(nameof(columnMappings)); if (columnMappings.Any(c => c is null)) throw new ArgumentException("Column Mappings must not contain any null columns."); if (columnMappings.Count == 0) throw new ArgumentException("Must include at least 1 column.", nameof(columnMappings)); var keyColumnCount = columnMappings.Count(c => typeof(IKeyColumn).IsAssignableFrom(c.GetType())); if (keyColumnCount != 1) throw new ArgumentException($"Must include 1 {nameof(IKeyColumn)} however {keyColumnCount} were found.", nameof(columnMappings)); } } }