using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using LocalizationTool.Models;
using LocalizationTool.Services;
using LocalizationTool.Views;
namespace LocalizationTool.ViewModels
{
/// 主窗口 ViewModel:语言包加载、翻译、校验、搜索与 AI 工具。
public partial class MainViewModel : ViewModelBase
{
private List _packs = new List();
private List _targets = new List();
private List _selectedLangs = new List();
private List _rows = new List();
private List _allRows = new List();
private List _lastIssues = new List();
private Config.ConfigRoot _cfg;
private string _dir;
private string _stats = "就绪";
private bool _isTranslating;
private double _progressMax = 1;
private double _progressValue;
public string Dir
{
get => _dir;
set => Set(ref _dir, value);
}
public string Stats
{
get => _stats;
set => Set(ref _stats, value);
}
public bool IsTranslating
{
get => _isTranslating;
set
{
if (Set(ref _isTranslating, value)) OnPropertyChanged(nameof(TranslationEnabled));
}
}
public double ProgressMax
{
get => _progressMax;
set => Set(ref _progressMax, value);
}
public double ProgressValue
{
get => _progressValue;
set => Set(ref _progressValue, value);
}
/// 表格行(受搜索过滤影响)。
public List Rows
{
get => _rows;
set => Set(ref _rows, value);
}
/// 全量行,过滤时基于它。
public List AllRows
{
get => _allRows;
set => Set(ref _allRows, value);
}
/// 目标语言(不含源语言)。
public IReadOnlyList Targets => _targets;
/// 勾选的语言(= 表格列 + 翻译目标)。
public IReadOnlyList SelectedLangs => _selectedLangs;
private List _langOptions = new List();
/// 语言下拉选项(CheckComboBox 的 ItemsSource)。
public List LangOptions
{
get => _langOptions;
set => Set(ref _langOptions, value);
}
/// 语言勾选变化(View 据此重建表格列)。
public event Action SelectedLangsChanged;
/// 语言下拉选项变化(View 据此刷新 CheckComboBox)。
public event Action LangOptionsChanged;
/// 翻译开关状态。
public bool TranslationEnabled => !IsTranslating;
public RelayCommand LoadCommand { get; }
public RelayCommand SaveCommand { get; }
public RelayCommand TranslateSelectedCommand { get; }
public RelayCommand TranslateAllCommand { get; }
public RelayCommand TranslateMissingCommand { get; }
public RelayCommand ValidateCommand { get; }
public RelayCommand AddEntryCommand { get; }
public RelayCommand AddLanguageCommand { get; }
public RelayCommand SettingsCommand { get; }
public RelayCommand AiChatCommand { get; }
public RelayCommand ShowMissingCommand { get; }
public RelayCommand ClearFilterCommand { get; }
public RelayCommand TranslateRowCommand { get; }
public RelayCommand DeleteRowCommand { get; }
public RelayCommand DeleteRowsCommand { get; }
public RelayCommand CopyKeyCommand { get; }
public RelayCommand ExportCsvCommand { get; }
public RelayCommand ImportCsvCommand { get; }
public MainViewModel()
{
_cfg = Config.Load();
Dir = string.IsNullOrWhiteSpace(_cfg.LastDirectory)
? @"D:\Program\TeamAAS\Localization"
: _cfg.LastDirectory;
LoadCommand = new RelayCommand(LoadPacks);
SaveCommand = new RelayCommand(SaveAll);
TranslateSelectedCommand = new RelayCommand(TranslateSelected);
TranslateAllCommand = new RelayCommand(TranslateAll);
TranslateMissingCommand = new RelayCommand(TranslateMissing);
ValidateCommand = new RelayCommand(Validate);
AddEntryCommand = new RelayCommand(AddEntry);
AddLanguageCommand = new RelayCommand(AddLanguage);
SettingsCommand = new RelayCommand(OpenSettings);
AiChatCommand = new RelayCommand(OpenAiChat);
ShowMissingCommand = new RelayCommand(ShowMissingOnly);
ClearFilterCommand = new RelayCommand(ClearFilter);
TranslateRowCommand = new RelayCommand(TranslateRow);
DeleteRowCommand = new RelayCommand(DeleteRow);
DeleteRowsCommand = new RelayCommand(DeleteRows);
CopyKeyCommand = new RelayCommand(CopyKey);
ExportCsvCommand = new RelayCommand(ExportCsv);
ImportCsvCommand = new RelayCommand(ImportCsv);
}
/// 外部(浏览对话框)设置目录后加载。
public void LoadDirectory(string dir)
{
Dir = dir;
LoadPacks();
}
private void LoadPacks()
{
var dir = Dir?.Trim();
if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir))
{
MessageBox.Show("目录不存在:" + dir);
return;
}
_cfg.LastDirectory = dir;
Config.Save(_cfg);
_packs = PackRepository.Load(dir);
// 注意:不能先赋给 _rows 再赋给 Rows——Set 检测到引用未变不会通知,表格会空白。
// 用局部变量走属性 setter,保证 PropertyChanged 触发。
var rows = PackRepository.BuildRows(_packs, out _targets);
RefreshTranslateLang();
OnPropertyChanged(nameof(Targets));
Rows = rows;
AllRows = rows;
Stats = BuildStats();
}
/// 刷新语言下拉选项(默认全选)并通知 View 更新。
private void RefreshTranslateLang()
{
LangOptions = _targets
.Select(p => new LangOption { LanguageCode = p.LanguageCode, Display = $"{p.LanguageName} ({p.LanguageCode})" })
.ToList();
_selectedLangs = _targets.ToList();
OnPropertyChanged(nameof(SelectedLangs));
LangOptionsChanged?.Invoke();
SelectedLangsChanged?.Invoke();
}
/// CheckComboBox 勾选变化时由 View 调用。
public void SetSelectedLangs(IEnumerable codes)
{
var set = new HashSet(codes ?? Enumerable.Empty(), StringComparer.Ordinal);
_selectedLangs = _targets.Where(t => set.Contains(t.LanguageCode)).ToList();
OnPropertyChanged(nameof(SelectedLangs));
SelectedLangsChanged?.Invoke();
}
private string BuildStats()
{
var missingRows = _rows.Count(r => r.HasMissing);
var missingCells = _rows.Sum(r => r.MissingCount);
var langs = string.Join(", ", _targets.Select(p => p.LanguageCode));
return $"共 {_rows.Count} 条中文源文;缺译行 {missingRows},缺译单元格 {missingCells}。语言: {langs}";
}
private void SaveAll()
{
if (_targets.Count == 0) return;
foreach (var lang in _targets) PackRepository.Save(lang);
RefreshMissing();
MessageBox.Show("已保存全部语言包。", "保存");
}
/// 编辑后刷新缺译概览。
public void RefreshMissing()
{
foreach (var r in _rows) r.Touch();
Stats = BuildStats();
}
// ---------- 语言 / 词条管理 ----------
private void AddLanguage()
{
var win = new AddLanguageWindow(_packs.Select(p => p.LanguageCode)) { Owner = Application.Current.MainWindow };
if (win.ShowDialog() != true || win.SelectedLanguage == null) return;
var entry = win.SelectedLanguage;
if (PackRepository.CreateFromTemplate(entry.Code, entry.Name, _packs, _dir, out _))
{
LoadPacks();
MessageBox.Show($"已创建 {entry.Code}.dat(百度语言代码: {entry.BaiduCode})。以 zh-CN 源文为 Key 集,值留空 = 缺译。");
}
else
{
MessageBox.Show("创建失败:文件已存在或参数无效。");
}
}
private void AddEntry()
{
var source = _packs.FirstOrDefault(p => p.IsSource);
if (source == null)
{
MessageBox.Show("未找到 zh-CN 源语言包,无法新增词条。请先加载包含 zh-CN.dat 的目录。");
return;
}
var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
var win = new AddEntryWindow(langs.ToList()) { Owner = Application.Current.MainWindow };
if (win.ShowDialog() != true) return;
var key = win.EntryKey;
if (source.Translations.ContainsKey(key))
{
MessageBox.Show($"词条已存在:{key}");
return;
}
source.Translations[key] = key;
foreach (var lang in _targets)
{
if (win.EntryTranslations.TryGetValue(lang.LanguageCode, out var v) &&
!string.IsNullOrWhiteSpace(v))
{
lang.Translations[key] = v;
}
}
foreach (var lang in _packs) PackRepository.Save(lang);
LoadPacks();
Stats = $"已新增词条:{key}";
}
private void DeleteRow(RowVm row)
{
if (row == null || string.IsNullOrEmpty(row.Key)) return;
DeleteByKey(row.Key);
}
/// 批量删除(右键"删除选中行",DataGrid 多选):只弹一次确认。
private void DeleteRows(System.Collections.IList items)
{
var rows = items?.OfType()
.Where(r => r != null && !string.IsNullOrEmpty(r.Key))
.ToList();
if (rows == null || rows.Count == 0) return;
if (rows.Count == 1) { DeleteByKey(rows[0].Key); return; } // 单选 = 原单条流程
var preview = string.Join("、", rows.Take(5).Select(r => $"「{r.Key}」"));
var more = rows.Count > 5 ? $"\n… 等共 {rows.Count} 条" : "";
var confirm = MessageBox.Show(
$"确认删除选中的 {rows.Count} 个词条?\n{preview}{more}\n\n该操作将从所有语言包中移除(不可撤销)。",
"批量删除确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;
DeleteKeysSilent(rows.Select(r => r.Key));
Stats = $"已批量删除 {rows.Count} 个词条,全部语言包已保存";
}
private void DeleteByKey(string key)
{
var confirm = MessageBox.Show($"确认删除词条「{key}」?该操作将从所有语言包中移除(不可撤销)。", "删除确认",
MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;
DeleteKeysSilent(new[] { key });
Stats = $"已删除词条:{key}";
}
/// 删除词条核心(不弹确认):从所有语言包移除,统一保存一次、刷新一次。
private void DeleteKeysSilent(IEnumerable keys)
{
var set = new HashSet(keys.Where(k => !string.IsNullOrEmpty(k)));
if (set.Count == 0) return;
foreach (var p in _packs)
foreach (var k in set)
p.Translations.Remove(k);
foreach (var p in _packs) PackRepository.Save(p);
LoadPacks();
}
private void CopyKey(RowVm row)
{
if (row == null || string.IsNullOrEmpty(row.Key)) return;
Clipboard.SetText(row.Key);
Stats = $"✅ 已复制: {row.Key}";
}
private void OpenSettings()
{
var cfg = Config.Load();
var settingsWindow = new TranslationSettingsWindow(cfg) { Owner = Application.Current.MainWindow };
if (settingsWindow.ShowDialog() == true)
{
_cfg = Config.Load();
Translation.Reset();
}
}
private void OpenAiChat()
{
var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
var win = new AiChatWindow(_cfg, langs.ToList(), BuildAiTools(), ExecuteAiToolAsync, ApplyRowsFromAi)
{
Owner = Application.Current.MainWindow
};
win.ShowDialog();
try { LoadPacks(); } catch { }
}
// ---------- 校验 ----------
private void Validate()
{
if (_rows.Count == 0) { MessageBox.Show("请先加载语言包。"); return; }
foreach (var r in _rows)
foreach (var c in r.Cells.Values)
{
c.HasIssue = false;
c.IssueMessage = "";
}
var fileErrors = new List();
try
{
// .json 校验
foreach (var f in System.IO.Directory.GetFiles(_dir, "*.json"))
{
try { LanguagePackIO.LoadJson(f); }
catch (Exception ex) { fileErrors.Add($"{System.IO.Path.GetFileName(f)}: 无法读取({ex.Message})"); }
}
}
catch { }
var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
var issues = TranslationValidator.Validate(_rows, langs);
foreach (var i in issues)
{
i.Cell.HasIssue = true;
i.Cell.IssueMessage = $"[{(i.IsError ? "错误" : "警告")}] {i.Message}";
}
var errorCount = issues.Count(x => x.IsError);
var warnCount = issues.Count - errorCount;
_lastIssues = issues;
var summary = fileErrors.Count > 0
? $"⚠ 文件校验失败 {fileErrors.Count} 个:\n{string.Join("\n", fileErrors)}\n\n"
: "";
var body = $"{summary}译文校验完成:问题单元格 {issues.Count} 个(错误 {errorCount},警告 {warnCount})。"
+ "\n有问题的单元格已用红色边框标出,鼠标悬停可查看原因。";
if (issues.Count > 0)
{
var preview = issues.Take(15)
.Select(i => $"[第{i.Row.Index}行·{i.LangCode}] {i.Row.Key} → {i.Message}");
var detail = "\n\n问题明细:\n" + string.Join("\n", preview);
if (issues.Count > 15) detail += $"\n… 等共 {issues.Count} 条";
body += detail;
}
MessageBox.Show(body, "校验结果");
}
// ---------- 搜索 / 过滤 ----------
private string _filterText = "";
public string FilterText
{
get => _filterText;
set { if (Set(ref _filterText, value)) ApplyFilter(); }
}
private void ApplyFilter()
{
if (AllRows.Count == 0)
{
Rows = new List();
return;
}
var keyword = FilterText?.Trim() ?? "";
Rows = string.IsNullOrEmpty(keyword)
? AllRows
: AllRows.Where(row =>
(row.Key != null && row.Key.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) ||
row.Cells.Values.Any(c => !string.IsNullOrEmpty(c.Value) &&
c.Value.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)
).ToList();
Stats = string.IsNullOrEmpty(keyword)
? $"共 {Rows.Count} 条"
: $"过滤结果: {Rows.Count} / {AllRows.Count} 条";
}
private void ShowMissingOnly()
{
if (AllRows.Count == 0) return;
Rows = AllRows.Where(r => r.HasMissing).ToList();
Stats = $"显示 {Rows.Count} 条缺译记录(共 {AllRows.Count} 条)";
}
private void ClearFilter()
{
FilterText = "";
Rows = AllRows;
Stats = BuildStats();
}
// ---------- CSV 导入导出(逗号分隔,Excel 可直接编辑) ----------
/// 导出当前表格为 CSV:列 = 勾选语言(未勾选用全部目标语言),行 = 当前过滤视图。
private void ExportCsv()
{
if (_packs.Count == 0) { MessageBox.Show("请先加载语言包。"); return; }
var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
if (langs.Count == 0) { MessageBox.Show("没有可导出的语言列。"); return; }
var dlg = new Microsoft.Win32.SaveFileDialog
{
Filter = "CSV 表格 (*.csv)|*.csv",
FileName = $"语言包导出_{DateTime.Now:yyyyMMdd_HHmm}.csv",
};
if (dlg.ShowDialog() != true) return;
var viewRows = Rows ?? new List();
try
{
CsvExchange.Export(dlg.FileName, viewRows, langs);
Stats = $"已导出 {viewRows.Count} 行 → {System.IO.Path.GetFileName(dlg.FileName)}";
MessageBox.Show($"导出完成:{viewRows.Count} 行,语言列 {string.Join(", ", langs.Select(l => l.LanguageCode))}。\n\n注意:CSV 中的空单元格导入时不会清空已有译文。",
"导出", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show("导出失败:" + ex.Message);
}
}
/// 从 CSV 导入:中文原文已存在 → 替换有值的译文;新中文原文 → 新增词条。写入前弹预览确认。
private void ImportCsv()
{
if (_packs.Count == 0) { MessageBox.Show("请先加载语言包。"); return; }
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "CSV 表格 (*.csv)|*.csv|所有文件 (*.*)|*.*",
};
if (dlg.ShowDialog() != true) return;
List header;
List records;
try
{
var parsed = CsvExchange.Parse(dlg.FileName);
header = parsed.Item1;
records = parsed.Item2;
}
catch (Exception ex)
{
MessageBox.Show("读取失败:" + ex.Message);
return;
}
if (header.Count == 0) { MessageBox.Show("CSV 是空的。"); return; }
// 列映射:"中文原文"列 + 语言代码列(匹配当前已加载语言;其余列忽略)
int keyCol = header.FindIndex(h => h != null && h.Trim() == "中文原文");
if (keyCol < 0) keyCol = 0; // 无表头时默认第一列为中文原文
var langCols = new List<(PackFile pack, int col)>();
for (int i = 0; i < header.Count; i++)
{
if (i == keyCol) continue;
var code = header[i]?.Trim() ?? "";
var pack = _targets.FirstOrDefault(t => string.Equals(t.LanguageCode, code, StringComparison.OrdinalIgnoreCase));
if (pack != null) langCols.Add((pack, i));
}
if (langCols.Count == 0)
{
MessageBox.Show($"CSV 表头没有匹配到已加载的语言列。\n表头需为语言代码(如 en-US、ja-JP),已加载:{string.Join(", ", _targets.Select(t => t.LanguageCode))}");
return;
}
var knownKeys = new HashSet(_rows.Select(r => r.Key), StringComparer.Ordinal);
var source = _packs.FirstOrDefault(p => p.IsSource);
int addCount = 0, updCount = 0;
foreach (var rec in records)
{
if (rec.Length <= keyCol) continue;
var key = (rec[keyCol] ?? "").Trim();
if (key.Length == 0) continue;
if (!knownKeys.Contains(key))
{
// 新词条:中文源包 key=key(key 即中文原文),目标语言写入 CSV 提供的译文
if (source != null) source.Translations[key] = key;
foreach (var lc in langCols)
if (lc.col < rec.Length && !string.IsNullOrWhiteSpace(rec[lc.col]))
lc.pack.Translations[key] = rec[lc.col].Trim();
knownKeys.Add(key);
addCount++;
}
else
{
// 已有词条:CSV 里非空且有变化的译文才替换(空单元格不动,防止误清空)
foreach (var lc in langCols)
{
if (lc.col >= rec.Length) continue;
var val = (rec[lc.col] ?? "").Trim();
if (val.Length == 0) continue;
var cur = lc.pack.Translations.TryGetValue(key, out var v) ? v : "";
if (cur != val)
{
lc.pack.Translations[key] = val;
updCount++;
}
}
}
}
if (addCount == 0 && updCount == 0)
{
MessageBox.Show("没有需要导入的变更(词条全部已存在且译文与当前一致)。", "导入",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var confirm = MessageBox.Show(
$"导入预览:\n\n新增词条 {addCount} 条\n更新译文 {updCount} 处\n语言列:{string.Join(", ", langCols.Select(c => c.pack.LanguageCode).Distinct())}\n\n确认写入并保存语言包?",
"确认导入", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (confirm != MessageBoxResult.Yes) return;
try
{
foreach (var p in _packs) PackRepository.Save(p);
LoadPacks();
Stats = $"导入完成:新增 {addCount} 条,更新 {updCount} 处译文";
MessageBox.Show($"导入完成:新增 {addCount} 条,更新 {updCount} 处译文。", "导入",
MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show("写入失败:" + ex.Message);
}
}
}
}