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