From 390392bb540bf5964d05848bd748b910b09258bd Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 16 Nov 2025 14:13:02 -0500 Subject: [PATCH 1/8] handle IO errors when exporting CSVs from language editor --- LangEditor/LangEdit.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 3f61030..585af26 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -231,12 +231,20 @@ private void exportToolStripMenuItem_Click(object sender, EventArgs e) languageContent += tempValue + "\r\n"; } - FileStream file = (FileStream)saveDialog.OpenFile(); - file.Write(Encoding.UTF8.GetBytes(languageContent)); - file.Flush(); - file.Close(); + try + { + FileStream file = (FileStream)saveDialog.OpenFile(); + file.Write(Encoding.UTF8.GetBytes(languageContent)); + file.Flush(); + file.Close(); - MessageBox.Show("Export successful.", "Information", MessageBoxButtons.OK); + MessageBox.Show("Export successful.", "Information", MessageBoxButtons.OK); + } + catch (IOException ex) + { + MessageBox.Show($"There was an issue exporting: {ex.Message}", "Error", MessageBoxButtons.OK); + + } } private void applyChangesToolStripMenuItem_Click(object sender, EventArgs e) From 672a64ea9014d316aea40b28fbbca8030abf0a9f Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 16 Nov 2025 14:20:45 -0500 Subject: [PATCH 2/8] minor language editor UI layout improvements --- LangEditor/LangEdit.Designer.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/LangEditor/LangEdit.Designer.cs b/LangEditor/LangEdit.Designer.cs index d87be21..bacd4ab 100644 --- a/LangEditor/LangEdit.Designer.cs +++ b/LangEditor/LangEdit.Designer.cs @@ -50,14 +50,17 @@ private void InitializeComponent() // dgvMain // dgvMain.AllowUserToOrderColumns = true; + dgvMain.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; dgvMain.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; dgvMain.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { colID, colString }); dgvMain.Dock = System.Windows.Forms.DockStyle.Fill; dgvMain.Location = new System.Drawing.Point(0, 24); dgvMain.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + dgvMain.MinimumSize = new System.Drawing.Size(70, 0); dgvMain.MultiSelect = false; dgvMain.Name = "dgvMain"; - dgvMain.Size = new System.Drawing.Size(484, 382); + dgvMain.RowHeadersWidth = 30; + dgvMain.Size = new System.Drawing.Size(648, 384); dgvMain.TabIndex = 0; dgvMain.RowsAdded += dgvMain_RowsAdded; // @@ -73,10 +76,10 @@ private void InitializeComponent() // // statusStrip1 // - statusStrip1.Location = new System.Drawing.Point(0, 406); + statusStrip1.Location = new System.Drawing.Point(0, 408); statusStrip1.Name = "statusStrip1"; statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0); - statusStrip1.Size = new System.Drawing.Size(484, 22); + statusStrip1.Size = new System.Drawing.Size(648, 22); statusStrip1.TabIndex = 1; statusStrip1.Text = "statusStrip1"; // @@ -86,7 +89,7 @@ private void InitializeComponent() menuStrip1.Location = new System.Drawing.Point(0, 0); menuStrip1.Name = "menuStrip1"; menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2); - menuStrip1.Size = new System.Drawing.Size(484, 24); + menuStrip1.Size = new System.Drawing.Size(648, 24); menuStrip1.TabIndex = 2; menuStrip1.Text = "menuStrip1"; // @@ -165,7 +168,7 @@ private void InitializeComponent() // AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - ClientSize = new System.Drawing.Size(484, 428); + ClientSize = new System.Drawing.Size(648, 430); Controls.Add(dgvMain); Controls.Add(statusStrip1); Controls.Add(menuStrip1); From 90ae55b2ffb8abe16abf456cb171bb62046db215 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 16 Nov 2025 14:38:23 -0500 Subject: [PATCH 3/8] add explicit ID column to language editor This should make it easier to cross-reference hashed string IDs from other resources. --- LangEditor/LangEdit.Designer.cs | 9 ++++- LangEditor/LangEdit.cs | 32 +++++++++------- LangEditor/LangEdit.resx | 65 ++++++++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/LangEditor/LangEdit.Designer.cs b/LangEditor/LangEdit.Designer.cs index bacd4ab..385e288 100644 --- a/LangEditor/LangEdit.Designer.cs +++ b/LangEditor/LangEdit.Designer.cs @@ -30,6 +30,7 @@ private void InitializeComponent() { dgvMain = new System.Windows.Forms.DataGridView(); colID = new System.Windows.Forms.DataGridViewTextBoxColumn(); + colName = new System.Windows.Forms.DataGridViewTextBoxColumn(); colString = new System.Windows.Forms.DataGridViewTextBoxColumn(); statusStrip1 = new System.Windows.Forms.StatusStrip(); menuStrip1 = new System.Windows.Forms.MenuStrip(); @@ -52,7 +53,7 @@ private void InitializeComponent() dgvMain.AllowUserToOrderColumns = true; dgvMain.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; dgvMain.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - dgvMain.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { colID, colString }); + dgvMain.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { colID, colName, colString }); dgvMain.Dock = System.Windows.Forms.DockStyle.Fill; dgvMain.Location = new System.Drawing.Point(0, 24); dgvMain.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); @@ -69,6 +70,11 @@ private void InitializeComponent() colID.HeaderText = "ID"; colID.Name = "colID"; // + // colName + // + colName.HeaderText = "Name"; + colName.Name = "colName"; + // // colString // colString.HeaderText = "String"; @@ -198,6 +204,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1; private System.Windows.Forms.ToolStripMenuItem applyChangesToolStripMenuItem; private System.Windows.Forms.DataGridViewTextBoxColumn colID; + private System.Windows.Forms.DataGridViewTextBoxColumn colName; private System.Windows.Forms.DataGridViewTextBoxColumn colString; private System.Windows.Forms.ToolStripMenuItem findNextToolStripMenuItem; } diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 585af26..49040ae 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -99,13 +99,13 @@ public void UpdateDisplay() { if (dict.ContainsKey(entry.Key)) { - dgvMain.Rows.Add(dict[entry.Key], entry.Value); + dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), dict[entry.Key], entry.Value); total++; resolved++; } else { - dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), entry.Value); + dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), "", entry.Value); total++; } } @@ -146,7 +146,7 @@ public int RebuildLanguage() id = Language.HashID(idString); } - string value = (string)dgvMain.Rows[i].Cells[1].Value; + string value = (string)dgvMain.Rows[i].Cells[2].Value; if (data.ContainsKey(id)) { MessageBox.Show(this, "ID " + idString + " already in use", "Warning", @@ -175,7 +175,7 @@ private void importToolStripMenuItem_Click(object sender, EventArgs e) string line; while ((line = file.ReadLine()) != null) { - string[] pair = line.Split(',', 2); + string[] pair = line.Split(',', 3); pair[0] = pair[0].Remove(0, 1).Remove(pair[0].Length - 2, 1); // Remove quotes uint key; if (pair[0].StartsWith("0x") && pair[0].Length == 10) @@ -189,14 +189,14 @@ private void importToolStripMenuItem_Click(object sender, EventArgs e) else key = Language.HashID(pair[0]); - if (pair[1].Length != 0) // Non-empty value + if (pair[2].Length != 0) // Non-empty value { - pair[1] = pair[1].Substring(1, pair[1].Length - 2); // Remove enclosing double quotes - pair[1] = pair[1].Replace("\"\"", "\""); // Convert double quotes - pair[1] = pair[1].Replace("\\r", "\xD").Replace("\\n", "\xA"); // Convert newlines + pair[2] = pair[2].Substring(1, pair[2].Length - 2); // Remove enclosing double quotes + pair[2] = pair[2].Replace("\"\"", "\""); // Convert double quotes + pair[2] = pair[2].Replace("\\r", "\xD").Replace("\\n", "\xA"); // Convert newlines } - data.Add(key, pair[1]); + data.Add(key, pair[2]); } _lang.mpEntries = data; @@ -219,8 +219,14 @@ private void exportToolStripMenuItem_Click(object sender, EventArgs e) string languageContent = ""; for (int i = 0; i < dgvMain.Rows.Count - 1; ++i) { - languageContent += "\"" + (string)(dgvMain.Rows[i].Cells[0].Value) + "\","; - string tempValue = (string)dgvMain.Rows[i].Cells[1].Value; + languageContent + += "\"" + + (string)(dgvMain.Rows[i].Cells[0].Value) + + "\",\"" + + ((string)dgvMain.Rows[i].Cells[1].Value != null ? (string)dgvMain.Rows[i].Cells[1].Value : "") + + "\","; + + string tempValue = (string)dgvMain.Rows[i].Cells[2].Value; tempValue = tempValue.Replace("\xD", "\\r").Replace("\xA", "\\n"); // Convert newlines tempValue = tempValue.Replace("\"", "\"\""); // Convert double quotes if (tempValue.Length != 0) // Enclose non-empty strings in double quotes @@ -266,7 +272,7 @@ private void findToolStripMenuItem_Click(object sender, EventArgs e) foreach (DataGridViewRow row in dgvMain.Rows) { if ((row.Cells[0].Value != null && ((string)row.Cells[0].Value).Contains(value, StringComparison.CurrentCultureIgnoreCase)) - || (row.Cells[1].Value != null && ((string)row.Cells[1].Value).Contains(value, StringComparison.CurrentCultureIgnoreCase))) + || (row.Cells[2].Value != null && ((string)row.Cells[2].Value).Contains(value, StringComparison.CurrentCultureIgnoreCase))) { dgvMain.ClearSelection(); dgvMain.CurrentCell = row.Cells[0]; @@ -291,7 +297,7 @@ private void findNextToolStripMenuItem_Click(object sender, EventArgs e) for (int i = start; i < dgvMain.Rows.Count - 1; ++i) { if (((string)dgvMain.Rows[i].Cells[0].Value).Contains(searchVal, StringComparison.CurrentCultureIgnoreCase) - || ((string)dgvMain.Rows[i].Cells[1].Value).Contains(searchVal, StringComparison.CurrentCultureIgnoreCase)) + || ((string)dgvMain.Rows[i].Cells[2].Value).Contains(searchVal, StringComparison.CurrentCultureIgnoreCase)) { dgvMain.ClearSelection(); dgvMain.CurrentCell = dgvMain.Rows[i].Cells[0]; diff --git a/LangEditor/LangEdit.resx b/LangEditor/LangEdit.resx index 9dec3c0..35811d6 100644 --- a/LangEditor/LangEdit.resx +++ b/LangEditor/LangEdit.resx @@ -1,4 +1,64 @@ - + + + @@ -60,6 +120,9 @@ True + + True + True From b690819ab92bcd8259af7324c2f472a897e59d68 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 16 Nov 2025 15:13:18 -0500 Subject: [PATCH 4/8] add auto ID/name recalculation on edit w/ confirmation dialog --- LangEditor/LangEdit.cs | 108 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 49040ae..940bc98 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -14,6 +14,8 @@ public partial class LangEdit : Form, IEntryEditor public event OnChanged Changed; private bool _ignoreChanges; + private bool _suppressHandlers; + private object _originalCellValue; private string searchVal; private Dictionary dict; @@ -24,8 +26,12 @@ public Language Lang set { _ignoreChanges = true; + _suppressHandlers = true; + _lang = value; UpdateDisplay(); + + _suppressHandlers = false; _ignoreChanges = false; } } @@ -33,11 +39,113 @@ public Language Lang public LangEdit() { _ignoreChanges = true; + _suppressHandlers = true; + InitializeComponent(); GenerateDictionary(); + + dgvMain.CellBeginEdit += dgvMain_CellBeginEdit; + dgvMain.CellEndEdit += dgvMain_CellEndEdit; + + _suppressHandlers = false; _ignoreChanges = false; } + private void dgvMain_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e) + { + var grid = (DataGridView)sender; + _originalCellValue = grid[e.ColumnIndex, e.RowIndex].Value; + } + + private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) + { + if (_suppressHandlers) + return; + + var grid = (DataGridView)sender; + var column = grid.Columns[e.ColumnIndex]; + + if (column.Name != "colID" && column.Name != "colName") + return; + + DataGridViewCell cell = grid[e.ColumnIndex, e.RowIndex]; + var newValue = cell.Value; + + if (Equals(_originalCellValue, newValue)) + return; + + string associated = column.Name == "colName" ? "ID" : "Name"; + var result = MessageBox.Show( + this, + $"Are you sure you want to apply this change to this {column.HeaderText}?" + + $"\nThis will update the associated {associated} field!" + + $"\n\nNew: {newValue}\nOriginal: {_originalCellValue}", + "Confirm change", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + + if (result == DialogResult.No) + { + _suppressHandlers = true; + cell.Value = _originalCellValue; + _suppressHandlers = false; + _originalCellValue = null; + return; + } + + _suppressHandlers = true; + try + { + var row = grid.Rows[e.RowIndex]; + + if (column.Name == "colName") + { + var name = Convert.ToString(newValue) ?? string.Empty; + if (name.Length > 0) + { + var id = Language.HashID(name); + row.Cells["colID"].Value = "0x" + id.ToString("X8"); + } + } + else if (column.Name == "colID") + { + var idText = Convert.ToString(newValue) ?? string.Empty; + idText = idText.Trim(); + + if (idText.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + idText = idText.Substring(2); + + if (!uint.TryParse(idText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var id)) + { + MessageBox.Show( + this, + "ID must be a valid 32-bit hexadecimal value, for example 0x00000001.", + "Invalid ID", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + + cell.Value = _originalCellValue; + _originalCellValue = null; + return; + } + + row.Cells["colID"].Value = "0x" + id.ToString("X8"); + + if (dict.TryGetValue(id, out var resolvedName)) + row.Cells["colName"].Value = resolvedName; + else + row.Cells["colName"].Value = string.Empty; + } + } + finally + { + _suppressHandlers = false; + _originalCellValue = null; + } + } + public void GenerateDictionary() { dict = new Dictionary(); From 354ed163a62778238d25bbeb2c08d287550a55e1 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sat, 2 May 2026 21:42:34 -0400 Subject: [PATCH 5/8] format cell dimensions in lang editor --- LangEditor/LangEdit.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 8b8b3a1..7ce021b 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -44,6 +44,7 @@ public LangEdit() _suppressHandlers = true; InitializeComponent(); + FormatCellDimensions(); GenerateDictionary(); dgvMain.CellBeginEdit += dgvMain_CellBeginEdit; @@ -53,6 +54,16 @@ public LangEdit() _ignoreChanges = false; } + private void FormatCellDimensions() + { + dgvMain.RowTemplate.Height = 20; + + int width = TextRenderer.MeasureText("0x00000000", dgvMain.Font).Width + 16; + colID.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; + colID.MinimumWidth = width; + colID.Width = width; + } + private void dgvMain_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e) { var grid = (DataGridView)sender; From 684eb1ddedbd2db13a265c4cc87512a2205f0083 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 18 Oct 2026 14:37:20 -0400 Subject: [PATCH 6/8] finish up language editor improvements --- LangEditor/LangEdit.Designer.cs | 26 ++- LangEditor/LangEdit.cs | 362 ++++++++++++++++++++------------ LangEditor/Language.cs | 2 + 3 files changed, 250 insertions(+), 140 deletions(-) diff --git a/LangEditor/LangEdit.Designer.cs b/LangEditor/LangEdit.Designer.cs index 385e288..fcbabb7 100644 --- a/LangEditor/LangEdit.Designer.cs +++ b/LangEditor/LangEdit.Designer.cs @@ -33,7 +33,8 @@ private void InitializeComponent() colName = new System.Windows.Forms.DataGridViewTextBoxColumn(); colString = new System.Windows.Forms.DataGridViewTextBoxColumn(); statusStrip1 = new System.Windows.Forms.StatusStrip(); - menuStrip1 = new System.Windows.Forms.MenuStrip(); + lblStatus = new System.Windows.Forms.ToolStripStatusLabel(); +menuStrip1 = new System.Windows.Forms.MenuStrip(); fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -45,6 +46,7 @@ private void InitializeComponent() toolsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); hashToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)dgvMain).BeginInit(); + statusStrip1.SuspendLayout(); menuStrip1.SuspendLayout(); SuspendLayout(); // @@ -81,14 +83,20 @@ private void InitializeComponent() colString.Name = "colString"; // // statusStrip1 - // - statusStrip1.Location = new System.Drawing.Point(0, 408); + // + statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { lblStatus }); + statusStrip1.Location= new System.Drawing.Point(0, 408); statusStrip1.Name = "statusStrip1"; statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0); statusStrip1.Size = new System.Drawing.Size(648, 22); statusStrip1.TabIndex = 1; statusStrip1.Text = "statusStrip1"; - // + // + // lblStatus + // + lblStatus.Name = "lblStatus"; + lblStatus.Size = new System.Drawing.Size(0, 17); +// // menuStrip1 // menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { fileToolStripMenuItem, editToolStripMenuItem, toolsToolStripMenuItem }); @@ -128,7 +136,8 @@ private void InitializeComponent() // applyChangesToolStripMenuItem // applyChangesToolStripMenuItem.Name = "applyChangesToolStripMenuItem"; - applyChangesToolStripMenuItem.Size = new System.Drawing.Size(163, 22); + applyChangesToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S; +applyChangesToolStripMenuItem.Size = new System.Drawing.Size(163, 22); applyChangesToolStripMenuItem.Text = "Apply Changes"; applyChangesToolStripMenuItem.Click += applyChangesToolStripMenuItem_Click; // @@ -151,7 +160,7 @@ private void InitializeComponent() // findNextToolStripMenuItem.Enabled = false; findNextToolStripMenuItem.Name = "findNextToolStripMenuItem"; - findNextToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Shift | System.Windows.Forms.Keys.F; + findNextToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F3; findNextToolStripMenuItem.Size = new System.Drawing.Size(195, 22); findNextToolStripMenuItem.Text = "Find next"; findNextToolStripMenuItem.Click += findNextToolStripMenuItem_Click; @@ -183,6 +192,8 @@ private void InitializeComponent() Name = "LangEdit"; Text = "Language Editor"; ((System.ComponentModel.ISupportInitialize)dgvMain).EndInit(); + statusStrip1.ResumeLayout(false); + statusStrip1.PerformLayout(); menuStrip1.ResumeLayout(false); menuStrip1.PerformLayout(); ResumeLayout(false); @@ -193,7 +204,8 @@ private void InitializeComponent() private System.Windows.Forms.DataGridView dgvMain; private System.Windows.Forms.StatusStrip statusStrip1; - private System.Windows.Forms.MenuStrip menuStrip1; + private System.Windows.Forms.ToolStripStatusLabel lblStatus; +private System.Windows.Forms.MenuStrip menuStrip1; private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem; diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 7ce021b..3f2f6f6 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Windows.Forms; @@ -16,6 +17,7 @@ public partial class LangEdit : Form, IEntryEditor private bool _ignoreChanges; private bool _suppressHandlers; + private bool _dirty; // Grid has edits that haven't been applied to the resource private object _originalCellValue; private string searchVal; @@ -49,6 +51,9 @@ public LangEdit() dgvMain.CellBeginEdit += dgvMain_CellBeginEdit; dgvMain.CellEndEdit += dgvMain_CellEndEdit; + dgvMain.CellValueChanged += dgvMain_CellValueChanged; + dgvMain.UserDeletedRow += (sender, e) => _dirty = true; + FormClosing += LangEdit_FormClosing; _suppressHandlers = false; _ignoreChanges = false; @@ -62,6 +67,27 @@ private void FormatCellDimensions() colID.AutoSizeMode = DataGridViewAutoSizeColumnMode.None; colID.MinimumWidth = width; colID.Width = width; + + // Strings can contain line breaks, so wrapping makes the cell editor multiline + colString.DefaultCellStyle.WrapMode = DataGridViewTriState.True; + } + + private void dgvMain_CellValueChanged(object sender, DataGridViewCellEventArgs e) + { + if (!_ignoreChanges && !_suppressHandlers) + _dirty = true; + } + + private void LangEdit_FormClosing(object sender, FormClosingEventArgs e) + { + dgvMain.EndEdit(); + if (!_dirty) + return; + + DialogResult result = MessageBox.Show(this, "Apply changes before closing?", "Unapplied changes", + MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); + if (result == DialogResult.Cancel || (result == DialogResult.Yes && RebuildLanguage() != 0)) + e.Cancel = true; } private void dgvMain_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e) @@ -87,24 +113,28 @@ private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) if (Equals(_originalCellValue, newValue)) return; - string associated = column.Name == "colName" ? "ID" : "Name"; - var result = MessageBox.Show( - this, - $"Are you sure you want to apply this change to this {column.HeaderText}?" + - $"\nThis will update the associated {associated} field!" + - $"\n\nNew: {newValue}\nOriginal: {_originalCellValue}", - "Confirm change", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question - ); - - if (result == DialogResult.No) + // A new or empty cell has nothing to overwrite, so only confirm when replacing a value + if (!string.IsNullOrEmpty(Convert.ToString(_originalCellValue))) { - _suppressHandlers = true; - cell.Value = _originalCellValue; - _suppressHandlers = false; - _originalCellValue = null; - return; + string associated = column.Name == "colName" ? "ID" : "Name"; + var result = MessageBox.Show( + this, + $"Are you sure you want to apply this change to this {column.HeaderText}?" + + $"\nThis will update the associated {associated} field!" + + $"\n\nNew: {newValue}\nOriginal: {_originalCellValue}", + "Confirm change", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + + if (result == DialogResult.No) + { + _suppressHandlers = true; + cell.Value = _originalCellValue; + _suppressHandlers = false; + _originalCellValue = null; + return; + } } _suppressHandlers = true; @@ -162,34 +192,27 @@ private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) public void GenerateDictionary() { dict = new Dictionary(); - StreamReader keys = new StreamReader(AppContext.BaseDirectory + "keys/keys.txt", Encoding.UTF8); - string key; - while ((key = keys.ReadLine()) != null) + string keysDir = AppContext.BaseDirectory + "keys/"; + if (!File.Exists(keysDir + "keys.txt")) + return; + + // Names only label rows, so a duplicate key or hash collision keeps the first name + foreach (string key in File.ReadLines(keysDir + "keys.txt", Encoding.UTF8)) { if (key.Contains("") - subKeyFilePos); - StreamReader subKeys = new StreamReader(AppContext.BaseDirectory + "keys/" + subKeyFile + ".txt", Encoding.UTF8); - string subKey; - while ((subKey = subKeys.ReadLine()) != null) + if (!File.Exists(keysDir + subKeyFile + ".txt")) + continue; + foreach (string subKey in File.ReadLines(keysDir + subKeyFile + ".txt", Encoding.UTF8)) { string fullSubKey = key.Replace(subKeyFile, subKey); // Replace file name with subkey fullSubKey = fullSubKey.Remove(subKeyFilePos - 2, 2); // Remove - try - { - dict.Add(Language.HashID(fullSubKey), fullSubKey); - } - catch - { - MessageBox.Show("ID " + fullSubKey + " (0x" + Language.HashID(fullSubKey).ToString("X8") + ") already exists.", "Error", MessageBoxButtons.OK); - continue; - } + dict.TryAdd(Language.HashID(fullSubKey), fullSubKey); } - subKeys.Close(); - continue; } else if (key.Contains(" - dict.Add(Language.HashID(fullSubKey), fullSubKey); + dict.TryAdd(Language.HashID(fullSubKey), fullSubKey); } } - dict.Add(Language.HashID(key), key); + dict.TryAdd(Language.HashID(key), key); } - - keys.Close(); } public void UpdateDisplay() { - int resolved = 0, total = 0; + bool ignoreChanges = _ignoreChanges; + _ignoreChanges = true; + + int resolved = 0; foreach (KeyValuePair entry in _lang.mpEntries) { - if (dict.ContainsKey(entry.Key)) - { - dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), dict[entry.Key], entry.Value); - total++; + if (dict.TryGetValue(entry.Key, out string name)) resolved++; - } - else - { - dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), "", entry.Value); - total++; - } + dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), name ?? "", entry.Value); } - MessageBox.Show("Resolved " + resolved + "/" + total + " keys.", "Information", - MessageBoxButtons.OK); + + _ignoreChanges = ignoreChanges; + lblStatus.Text = $"{_lang.LanguageName}: {_lang.mpEntries.Count} strings, {resolved} names resolved"; + } + + private void SelectRow(DataGridViewRow row) + { + dgvMain.ClearSelection(); + dgvMain.CurrentCell = row.Cells[0]; + row.Selected = true; } public int RebuildLanguage() @@ -239,14 +263,19 @@ public int RebuildLanguage() if (_ignoreChanges) return 1; + dgvMain.EndEdit(); // Include the cell being edited Dictionary data = new Dictionary(); - for (int i = 0; i < dgvMain.Rows.Count - 1; ++i) + foreach (DataGridViewRow row in dgvMain.Rows) { - string idString = (string)dgvMain.Rows[i].Cells[0].Value; + if (row.IsNewRow) + continue; + + string idString = (string)row.Cells[0].Value; if (idString == null) { + SelectRow(row); MessageBox.Show(this, "ID cannot be left blank", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return 2; @@ -257,7 +286,8 @@ public int RebuildLanguage() { if (!uint.TryParse(idString[2..], NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out id)) { - MessageBox.Show(this, "Failed to parse ID \"" + idString + "\"", "Warning", + SelectRow(row); + MessageBox.Show(this, "Failed to parse ID \"" + idString+ "\"", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return 3; } @@ -267,9 +297,10 @@ public int RebuildLanguage() id = Language.HashID(idString); } - string value = (string)dgvMain.Rows[i].Cells[2].Value; + string value = (string)row.Cells[2].Value; if (data.ContainsKey(id)) { + SelectRow(row); MessageBox.Show(this, "ID " + idString + " already in use", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return 4; @@ -278,6 +309,7 @@ public int RebuildLanguage() } _lang.mpEntries = data; + _dirty = false; Changed?.Invoke(); @@ -291,39 +323,112 @@ private void importToolStripMenuItem_Click(object sender, EventArgs e) if (openDialog.ShowDialog() == DialogResult.Cancel) return; + string error = ImportCSV(openDialog.FileName); + if (error != null) + MessageBox.Show(this, error + " Import cancelled.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + internal string ImportCSV(string path) + { Dictionary data = new Dictionary(); - StreamReader file = new StreamReader(openDialog.FileName, Encoding.UTF8); - string line; - while ((line = file.ReadLine()) != null) + int row = 0; + try { - string[] pair = line.Split(',', 3); - pair[0] = pair[0].Remove(0, 1).Remove(pair[0].Length - 2, 1); // Remove quotes - uint key; - if (pair[0].StartsWith("0x") && pair[0].Length == 10) + using StreamReader reader = new StreamReader(path, Encoding.UTF8); + foreach (List fields in ReadCSV(reader)) { - if (!uint.TryParse(pair[0][2..], NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out key)) + row++; + string idText = fields[0].Trim(); + string name = fields.Count > 1 ? fields[1].Trim() : ""; + + uint key; + if (idText.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { - MessageBox.Show("Could not parse ID " + pair[0] + ". Import cancelled.", "Error", MessageBoxButtons.OK); - return; + if (!uint.TryParse(idText[2..], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out key)) + return $"Could not parse ID {idText} on row {row}."; } - } - else - key = Language.HashID(pair[0]); + else if (idText.Length != 0) + key = Language.HashID(idText); + else if (name.Length != 0) + key = Language.HashID(name); + else + return $"Row {row} has no ID or name."; - if (pair[2].Length != 0) // Non-empty value - { - pair[2] = pair[2].Substring(1, pair[2].Length - 2); // Remove enclosing double quotes - pair[2] = pair[2].Replace("\"\"", "\""); // Convert double quotes - pair[2] = pair[2].Replace("\\r", "\xD").Replace("\\n", "\xA"); // Convert newlines - } + string value = string.Join(",", fields.Skip(2)); // An unquoted string may contain commas + value = value.Replace("\\r", "\xD").Replace("\\n", "\xA"); // Convert newlines - data.Add(key, pair[2]); + if (!data.TryAdd(key, value)) + return $"ID {idText} on row {row} is a duplicate."; + } + } + catch (InvalidDataException) + { + return $"Row {row + 1} has a quote that is never closed."; + } + catch (IOException ex) + { + return ex.Message; } _lang.mpEntries = data; dgvMain.Rows.Clear(); UpdateDisplay(); + _dirty = true; + return null; + } + + private static IEnumerable> ReadCSV(TextReader reader) + { + List fields = new List(); + StringBuilder field = new StringBuilder(); + bool quoted = false; + bool rowStarted = false; + int c; + while ((c = reader.Read()) != -1) + { + if (quoted) + { + if (c != '"') + field.Append((char)c); + else if (reader.Peek() == '"') + field.Append((char)reader.Read()); + else + quoted = false; + } + else if (c == '\r' || c == '\n') + { + if (c == '\r' && reader.Peek() == '\n') + reader.Read(); + if (rowStarted) + { + fields.Add(field.ToString()); + yield return fields; + } + fields = new List(); + field.Clear(); + rowStarted = false; + continue; + } + else if (c == '"') + quoted = true; + else if (c == ',') + { + fields.Add(field.ToString()); + field.Clear(); + } + else + field.Append((char)c); + rowStarted = true; + } + + if (quoted) + throw new InvalidDataException("Unterminated quote"); + if (rowStarted) + { + fields.Add(field.ToString()); + yield return fields; + } } private void exportToolStripMenuItem_Click(object sender, EventArgs e) @@ -333,45 +438,45 @@ private void exportToolStripMenuItem_Click(object sender, EventArgs e) if (saveDialog.ShowDialog() == DialogResult.Cancel) return; - // CSV content as a single contiguous string. - // Keys are prepended with 0x and padded to 8 characters. - // In values, incompatible characters are escaped. - // Both keys and values are enclosed in double quotes. - string languageContent = ""; - for (int i = 0; i < dgvMain.Rows.Count - 1; ++i) + string error = ExportCSV(saveDialog.FileName); + if (error == null) + MessageBox.Show("Export successful.", "Information", MessageBoxButtons.OK); + else + MessageBox.Show($"There was an issue exporting: {error}", "Error", MessageBoxButtons.OK); + } + + // Keys are prepended with 0x and padded to 8 characters. + // In values, incompatible characters are escaped. + // Keys and names are enclosed in double quotes, as are non-empty values. + internal string ExportCSV(string path) + { + dgvMain.EndEdit(); // Include the cell being edited + StringBuilder languageContent = new StringBuilder(); + foreach (DataGridViewRow row in dgvMain.Rows) { - languageContent - += "\"" - + (string)(dgvMain.Rows[i].Cells[0].Value) - + "\",\"" - + ((string)dgvMain.Rows[i].Cells[1].Value != null ? (string)dgvMain.Rows[i].Cells[1].Value : "") - + "\","; - - string tempValue = (string)dgvMain.Rows[i].Cells[2].Value; + if (row.IsNewRow) + continue; + + languageContent.Append('"').Append(row.Cells[0].Value).Append("\",\"").Append(row.Cells[1].Value).Append("\","); + + string tempValue = (string)row.Cells[2].Value ?? ""; tempValue = tempValue.Replace("\xD", "\\r").Replace("\xA", "\\n"); // Convert newlines tempValue = tempValue.Replace("\"", "\"\""); // Convert double quotes if (tempValue.Length != 0) // Enclose non-empty strings in double quotes - { - tempValue = "\"" + tempValue; - tempValue += "\""; - } - languageContent += tempValue + "\r\n"; + languageContent.Append('"').Append(tempValue).Append('"'); + languageContent.Append("\r\n"); } try { - FileStream file = (FileStream)saveDialog.OpenFile(); - file.Write(Encoding.UTF8.GetBytes(languageContent)); - file.Flush(); - file.Close(); - - MessageBox.Show("Export successful.", "Information", MessageBoxButtons.OK); + // The BOM lets spreadsheet apps detect UTF-8 + File.WriteAllText(path, languageContent.ToString(), new UTF8Encoding(true)); } - catch (IOException ex) + catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { - MessageBox.Show($"There was an issue exporting: {ex.Message}", "Error", MessageBoxButtons.OK); - + return ex.Message; } + return null; } private void applyChangesToolStripMenuItem_Click(object sender, EventArgs e) @@ -379,7 +484,7 @@ private void applyChangesToolStripMenuItem_Click(object sender, EventArgs e) int result = RebuildLanguage(); if (result == 0) - MessageBox.Show(this, "Done!", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); + lblStatus.Text = $"{_lang.LanguageName}: {_lang.mpEntries.Count} strings, changes applied"; } private void findToolStripMenuItem_Click(object sender, EventArgs e) @@ -389,45 +494,36 @@ private void findToolStripMenuItem_Click(object sender, EventArgs e) return; searchVal = value; findNextToolStripMenuItem.Enabled = true; - - foreach (DataGridViewRow row in dgvMain.Rows) - { - if ((row.Cells[0].Value != null && ((string)row.Cells[0].Value).Contains(value, StringComparison.CurrentCultureIgnoreCase)) - || (row.Cells[2].Value != null && ((string)row.Cells[2].Value).Contains(value, StringComparison.CurrentCultureIgnoreCase))) - { - dgvMain.ClearSelection(); - dgvMain.CurrentCell = row.Cells[0]; - row.Selected = true; - return; - } - } - - MessageBox.Show(this, "Hash not found!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + FindFrom(0); } private void findNextToolStripMenuItem_Click(object sender, EventArgs e) { if (searchVal == null) return; - int start = dgvMain.SelectedCells[0].RowIndex + 1; - if (start > dgvMain.Rows.Count - 1) - { - MessageBox.Show(this, "Hash not found!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - for (int i = start; i < dgvMain.Rows.Count - 1; ++i) + FindFrom(dgvMain.CurrentCell == null ? 0 : dgvMain.CurrentCell.RowIndex + 1); + } + + private void FindFrom(int start) + { + int count = dgvMain.Rows.Count; + for (int i = 0; i < count; ++i) { - if (((string)dgvMain.Rows[i].Cells[0].Value).Contains(searchVal, StringComparison.CurrentCultureIgnoreCase) - || ((string)dgvMain.Rows[i].Cells[2].Value).Contains(searchVal, StringComparison.CurrentCultureIgnoreCase)) + DataGridViewRow row = dgvMain.Rows[(start + i) % count]; + if (row.IsNewRow) + continue; + + foreach (DataGridViewCell cell in row.Cells) { - dgvMain.ClearSelection(); - dgvMain.CurrentCell = dgvMain.Rows[i].Cells[0]; - dgvMain.Rows[i].Selected = true; - return; + if (cell.Value is string text && text.Contains(searchVal, StringComparison.CurrentCultureIgnoreCase)) + { + SelectRow(row); + return; + } } } - MessageBox.Show(this, "Hash not found!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show(this, "\"" + searchVal + "\" not found.", "Find", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void hashToolStripMenuItem_Click(object sender, EventArgs e) @@ -437,7 +533,7 @@ private void hashToolStripMenuItem_Click(object sender, EventArgs e) return; uint result = Language.HashID(value); - MessageBox.Show(this, "Hashed value is: " + result.ToString("X8"), "Information", + MessageBox.Show(this, "Hashed value is: 0x" + result.ToString("X8"), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); } diff --git a/LangEditor/Language.cs b/LangEditor/Language.cs index e77ce72..b0a959d 100644 --- a/LangEditor/Language.cs +++ b/LangEditor/Language.cs @@ -43,6 +43,8 @@ private enum LanguageId : uint private uint muSize; // Number of entries internal Dictionary mpEntries; + internal string LanguageName => meLanguageID.ToString().Replace("E_LANGUAGE_", ""); + public Language() { mpEntries = new Dictionary(); From 80c953d45fa8743a277f74703125e9a2949ac898 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 18 Oct 2026 14:57:12 -0400 Subject: [PATCH 7/8] modernize lang editor updates --- LangEditor/LangEdit.cs | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 3f2f6f6..18d6833 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -92,7 +92,7 @@ private void LangEdit_FormClosing(object sender, FormClosingEventArgs e) private void dgvMain_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e) { - var grid = (DataGridView)sender; + DataGridView grid = (DataGridView)sender; _originalCellValue = grid[e.ColumnIndex, e.RowIndex].Value; } @@ -101,14 +101,14 @@ private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) if (_suppressHandlers) return; - var grid = (DataGridView)sender; - var column = grid.Columns[e.ColumnIndex]; + DataGridView grid = (DataGridView)sender; + DataGridViewColumn column = grid.Columns[e.ColumnIndex]; if (column.Name != "colID" && column.Name != "colName") return; DataGridViewCell cell = grid[e.ColumnIndex, e.RowIndex]; - var newValue = cell.Value; + object newValue = cell.Value; if (Equals(_originalCellValue, newValue)) return; @@ -117,7 +117,7 @@ private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) if (!string.IsNullOrEmpty(Convert.ToString(_originalCellValue))) { string associated = column.Name == "colName" ? "ID" : "Name"; - var result = MessageBox.Show( + DialogResult result = MessageBox.Show( this, $"Are you sure you want to apply this change to this {column.HeaderText}?" + $"\nThis will update the associated {associated} field!" + @@ -140,26 +140,26 @@ private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) _suppressHandlers = true; try { - var row = grid.Rows[e.RowIndex]; + DataGridViewRow row = grid.Rows[e.RowIndex]; if (column.Name == "colName") { - var name = Convert.ToString(newValue) ?? string.Empty; + string name = Convert.ToString(newValue) ?? string.Empty; if (name.Length > 0) { - var id = Language.HashID(name); - row.Cells["colID"].Value = "0x" + id.ToString("X8"); + uint id = Language.HashID(name); + row.Cells["colID"].Value = $"0x{id:X8}"; } } else if (column.Name == "colID") { - var idText = Convert.ToString(newValue) ?? string.Empty; + string idText = Convert.ToString(newValue) ?? string.Empty; idText = idText.Trim(); if (idText.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - idText = idText.Substring(2); + idText = idText[2..]; - if (!uint.TryParse(idText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var id)) + if (!uint.TryParse(idText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint id)) { MessageBox.Show( this, @@ -174,9 +174,9 @@ private void dgvMain_CellEndEdit(object sender, DataGridViewCellEventArgs e) return; } - row.Cells["colID"].Value = "0x" + id.ToString("X8"); + row.Cells["colID"].Value = $"0x{id:X8}"; - if (dict.TryGetValue(id, out var resolvedName)) + if (dict.TryGetValue(id, out string resolvedName)) row.Cells["colName"].Value = resolvedName; else row.Cells["colName"].Value = string.Empty; @@ -244,7 +244,7 @@ public void UpdateDisplay() { if (dict.TryGetValue(entry.Key, out string name)) resolved++; - dgvMain.Rows.Add("0x" + entry.Key.ToString("X8"), name ?? "", entry.Value); + dgvMain.Rows.Add($"0x{entry.Key:X8}", name ?? "", entry.Value); } _ignoreChanges = ignoreChanges; @@ -330,11 +330,11 @@ private void importToolStripMenuItem_Click(object sender, EventArgs e) internal string ImportCSV(string path) { - Dictionary data = new Dictionary(); + Dictionary data = []; int row = 0; try { - using StreamReader reader = new StreamReader(path, Encoding.UTF8); + using StreamReader reader = new(path, Encoding.UTF8); foreach (List fields in ReadCSV(reader)) { row++; @@ -380,8 +380,8 @@ internal string ImportCSV(string path) private static IEnumerable> ReadCSV(TextReader reader) { - List fields = new List(); - StringBuilder field = new StringBuilder(); + List fields = []; + StringBuilder field = new(); bool quoted = false; bool rowStarted = false; int c; @@ -405,7 +405,7 @@ private static IEnumerable> ReadCSV(TextReader reader) fields.Add(field.ToString()); yield return fields; } - fields = new List(); + fields = []; field.Clear(); rowStarted = false; continue; @@ -451,7 +451,7 @@ private void exportToolStripMenuItem_Click(object sender, EventArgs e) internal string ExportCSV(string path) { dgvMain.EndEdit(); // Include the cell being edited - StringBuilder languageContent = new StringBuilder(); + StringBuilder languageContent = new(); foreach (DataGridViewRow row in dgvMain.Rows) { if (row.IsNewRow) @@ -472,7 +472,7 @@ internal string ExportCSV(string path) // The BOM lets spreadsheet apps detect UTF-8 File.WriteAllText(path, languageContent.ToString(), new UTF8Encoding(true)); } - catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return ex.Message; } @@ -523,7 +523,7 @@ private void FindFrom(int start) } } - MessageBox.Show(this, "\"" + searchVal + "\" not found.", "Find", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show(this, $"\"{searchVal}\" not found.", "Find", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void hashToolStripMenuItem_Click(object sender, EventArgs e) @@ -533,7 +533,7 @@ private void hashToolStripMenuItem_Click(object sender, EventArgs e) return; uint result = Language.HashID(value); - MessageBox.Show(this, "Hashed value is: 0x" + result.ToString("X8"), "Information", + MessageBox.Show(this, $"Hashed value is: 0x{result:X8}", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); } From b17c2c2d1f43656bd95b9c42126b4cdf6eeedbba Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sun, 18 Oct 2026 15:08:19 -0400 Subject: [PATCH 8/8] Address "Support legacy two-column CSV imports" --- LangEditor/LangEdit.cs | 61 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/LangEditor/LangEdit.cs b/LangEditor/LangEdit.cs index 18d6833..42d0e88 100644 --- a/LangEditor/LangEdit.cs +++ b/LangEditor/LangEdit.cs @@ -330,46 +330,53 @@ private void importToolStripMenuItem_Click(object sender, EventArgs e) internal string ImportCSV(string path) { - Dictionary data = []; - int row = 0; + List> rows = []; try { using StreamReader reader = new(path, Encoding.UTF8); foreach (List fields in ReadCSV(reader)) - { - row++; - string idText = fields[0].Trim(); - string name = fields.Count > 1 ? fields[1].Trim() : ""; - - uint key; - if (idText.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - { - if (!uint.TryParse(idText[2..], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out key)) - return $"Could not parse ID {idText} on row {row}."; - } - else if (idText.Length != 0) - key = Language.HashID(idText); - else if (name.Length != 0) - key = Language.HashID(name); - else - return $"Row {row} has no ID or name."; - - string value = string.Join(",", fields.Skip(2)); // An unquoted string may contain commas - value = value.Replace("\\r", "\xD").Replace("\\n", "\xA"); // Convert newlines - - if (!data.TryAdd(key, value)) - return $"ID {idText} on row {row} is a duplicate."; - } + rows.Add(fields); } catch (InvalidDataException) { - return $"Row {row + 1} has a quote that is never closed."; + return $"Row {rows.Count + 1} has a quote that is never closed."; } catch (IOException ex) { return ex.Message; } + // Exports from before the Name column have two columns, ID (or name) and string. + // This is decided per file, since a current-format row missing its string looks the same. + bool legacy = rows.All(fields => fields.Count <= 2); + + Dictionary data = []; + for (int row = 1; row <= rows.Count; row++) + { + List fields = rows[row - 1]; + string idText = fields[0].Trim(); + string name = !legacy && fields.Count > 1 ? fields[1].Trim() : ""; + + uint key; + if (idText.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + if (!uint.TryParse(idText[2..], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out key)) + return $"Could not parse ID {idText} on row {row}."; + } + else if (idText.Length != 0) + key = Language.HashID(idText); + else if (name.Length != 0) + key = Language.HashID(name); + else + return $"Row {row} has no ID or name."; + + string value = string.Join(",", fields.Skip(legacy ? 1 : 2)); // An unquoted string may contain commas + value = value.Replace("\\r", "\xD").Replace("\\n", "\xA"); // Convert newlines + + if (!data.TryAdd(key, value)) + return $"ID {idText} on row {row} is a duplicate."; + } + _lang.mpEntries = data; dgvMain.Rows.Clear();