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