MainViewModel.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Windows;
  5. using LocalizationTool.Models;
  6. using LocalizationTool.Services;
  7. using LocalizationTool.Views;
  8. namespace LocalizationTool.ViewModels
  9. {
  10. /// <summary>主窗口 ViewModel:语言包加载、翻译、校验、搜索与 AI 工具。</summary>
  11. public partial class MainViewModel : ViewModelBase
  12. {
  13. private List<PackFile> _packs = new List<PackFile>();
  14. private List<PackFile> _targets = new List<PackFile>();
  15. private List<PackFile> _selectedLangs = new List<PackFile>();
  16. private List<RowVm> _rows = new List<RowVm>();
  17. private List<RowVm> _allRows = new List<RowVm>();
  18. private List<TranslationIssue> _lastIssues = new List<TranslationIssue>();
  19. private Config.ConfigRoot _cfg;
  20. private string _dir;
  21. private string _stats = "就绪";
  22. private bool _isTranslating;
  23. private double _progressMax = 1;
  24. private double _progressValue;
  25. public string Dir
  26. {
  27. get => _dir;
  28. set => Set(ref _dir, value);
  29. }
  30. public string Stats
  31. {
  32. get => _stats;
  33. set => Set(ref _stats, value);
  34. }
  35. public bool IsTranslating
  36. {
  37. get => _isTranslating;
  38. set
  39. {
  40. if (Set(ref _isTranslating, value)) OnPropertyChanged(nameof(TranslationEnabled));
  41. }
  42. }
  43. public double ProgressMax
  44. {
  45. get => _progressMax;
  46. set => Set(ref _progressMax, value);
  47. }
  48. public double ProgressValue
  49. {
  50. get => _progressValue;
  51. set => Set(ref _progressValue, value);
  52. }
  53. /// <summary>表格行(受搜索过滤影响)。</summary>
  54. public List<RowVm> Rows
  55. {
  56. get => _rows;
  57. set => Set(ref _rows, value);
  58. }
  59. /// <summary>全量行,过滤时基于它。</summary>
  60. public List<RowVm> AllRows
  61. {
  62. get => _allRows;
  63. set => Set(ref _allRows, value);
  64. }
  65. /// <summary>目标语言(不含源语言)。</summary>
  66. public IReadOnlyList<PackFile> Targets => _targets;
  67. /// <summary>勾选的语言(= 表格列 + 翻译目标)。</summary>
  68. public IReadOnlyList<PackFile> SelectedLangs => _selectedLangs;
  69. private List<LangOption> _langOptions = new List<LangOption>();
  70. /// <summary>语言下拉选项(CheckComboBox 的 ItemsSource)。</summary>
  71. public List<LangOption> LangOptions
  72. {
  73. get => _langOptions;
  74. set => Set(ref _langOptions, value);
  75. }
  76. /// <summary>语言勾选变化(View 据此重建表格列)。</summary>
  77. public event Action SelectedLangsChanged;
  78. /// <summary>语言下拉选项变化(View 据此刷新 CheckComboBox)。</summary>
  79. public event Action LangOptionsChanged;
  80. /// <summary>翻译开关状态。</summary>
  81. public bool TranslationEnabled => !IsTranslating;
  82. public RelayCommand LoadCommand { get; }
  83. public RelayCommand SaveCommand { get; }
  84. public RelayCommand TranslateSelectedCommand { get; }
  85. public RelayCommand TranslateAllCommand { get; }
  86. public RelayCommand TranslateMissingCommand { get; }
  87. public RelayCommand ValidateCommand { get; }
  88. public RelayCommand AddEntryCommand { get; }
  89. public RelayCommand AddLanguageCommand { get; }
  90. public RelayCommand SettingsCommand { get; }
  91. public RelayCommand AiChatCommand { get; }
  92. public RelayCommand ShowMissingCommand { get; }
  93. public RelayCommand ClearFilterCommand { get; }
  94. public RelayCommand<RowVm> TranslateRowCommand { get; }
  95. public RelayCommand<RowVm> DeleteRowCommand { get; }
  96. public RelayCommand<System.Collections.IList> DeleteRowsCommand { get; }
  97. public RelayCommand<RowVm> CopyKeyCommand { get; }
  98. public RelayCommand ExportCsvCommand { get; }
  99. public RelayCommand ImportCsvCommand { get; }
  100. public MainViewModel()
  101. {
  102. _cfg = Config.Load();
  103. Dir = string.IsNullOrWhiteSpace(_cfg.LastDirectory)
  104. ? @"D:\Program\TeamAAS\Localization"
  105. : _cfg.LastDirectory;
  106. LoadCommand = new RelayCommand(LoadPacks);
  107. SaveCommand = new RelayCommand(SaveAll);
  108. TranslateSelectedCommand = new RelayCommand(TranslateSelected);
  109. TranslateAllCommand = new RelayCommand(TranslateAll);
  110. TranslateMissingCommand = new RelayCommand(TranslateMissing);
  111. ValidateCommand = new RelayCommand(Validate);
  112. AddEntryCommand = new RelayCommand(AddEntry);
  113. AddLanguageCommand = new RelayCommand(AddLanguage);
  114. SettingsCommand = new RelayCommand(OpenSettings);
  115. AiChatCommand = new RelayCommand(OpenAiChat);
  116. ShowMissingCommand = new RelayCommand(ShowMissingOnly);
  117. ClearFilterCommand = new RelayCommand(ClearFilter);
  118. TranslateRowCommand = new RelayCommand<RowVm>(TranslateRow);
  119. DeleteRowCommand = new RelayCommand<RowVm>(DeleteRow);
  120. DeleteRowsCommand = new RelayCommand<System.Collections.IList>(DeleteRows);
  121. CopyKeyCommand = new RelayCommand<RowVm>(CopyKey);
  122. ExportCsvCommand = new RelayCommand(ExportCsv);
  123. ImportCsvCommand = new RelayCommand(ImportCsv);
  124. }
  125. /// <summary>外部(浏览对话框)设置目录后加载。</summary>
  126. public void LoadDirectory(string dir)
  127. {
  128. Dir = dir;
  129. LoadPacks();
  130. }
  131. private void LoadPacks()
  132. {
  133. var dir = Dir?.Trim();
  134. if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir))
  135. {
  136. MessageBox.Show("目录不存在:" + dir);
  137. return;
  138. }
  139. _cfg.LastDirectory = dir;
  140. Config.Save(_cfg);
  141. _packs = PackRepository.Load(dir);
  142. // 注意:不能先赋给 _rows 再赋给 Rows——Set 检测到引用未变不会通知,表格会空白。
  143. // 用局部变量走属性 setter,保证 PropertyChanged 触发。
  144. var rows = PackRepository.BuildRows(_packs, out _targets);
  145. RefreshTranslateLang();
  146. OnPropertyChanged(nameof(Targets));
  147. Rows = rows;
  148. AllRows = rows;
  149. Stats = BuildStats();
  150. }
  151. /// <summary>刷新语言下拉选项(默认全选)并通知 View 更新。</summary>
  152. private void RefreshTranslateLang()
  153. {
  154. LangOptions = _targets
  155. .Select(p => new LangOption { LanguageCode = p.LanguageCode, Display = $"{p.LanguageName} ({p.LanguageCode})" })
  156. .ToList();
  157. _selectedLangs = _targets.ToList();
  158. OnPropertyChanged(nameof(SelectedLangs));
  159. LangOptionsChanged?.Invoke();
  160. SelectedLangsChanged?.Invoke();
  161. }
  162. /// <summary>CheckComboBox 勾选变化时由 View 调用。</summary>
  163. public void SetSelectedLangs(IEnumerable<string> codes)
  164. {
  165. var set = new HashSet<string>(codes ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
  166. _selectedLangs = _targets.Where(t => set.Contains(t.LanguageCode)).ToList();
  167. OnPropertyChanged(nameof(SelectedLangs));
  168. SelectedLangsChanged?.Invoke();
  169. }
  170. private string BuildStats()
  171. {
  172. var missingRows = _rows.Count(r => r.HasMissing);
  173. var missingCells = _rows.Sum(r => r.MissingCount);
  174. var langs = string.Join(", ", _targets.Select(p => p.LanguageCode));
  175. return $"共 {_rows.Count} 条中文源文;缺译行 {missingRows},缺译单元格 {missingCells}。语言: {langs}";
  176. }
  177. private void SaveAll()
  178. {
  179. if (_targets.Count == 0) return;
  180. foreach (var lang in _targets) PackRepository.Save(lang);
  181. RefreshMissing();
  182. MessageBox.Show("已保存全部语言包。", "保存");
  183. }
  184. /// <summary>编辑后刷新缺译概览。</summary>
  185. public void RefreshMissing()
  186. {
  187. foreach (var r in _rows) r.Touch();
  188. Stats = BuildStats();
  189. }
  190. // ---------- 语言 / 词条管理 ----------
  191. private void AddLanguage()
  192. {
  193. var win = new AddLanguageWindow(_packs.Select(p => p.LanguageCode)) { Owner = Application.Current.MainWindow };
  194. if (win.ShowDialog() != true || win.SelectedLanguage == null) return;
  195. var entry = win.SelectedLanguage;
  196. if (PackRepository.CreateFromTemplate(entry.Code, entry.Name, _packs, _dir, out _))
  197. {
  198. LoadPacks();
  199. MessageBox.Show($"已创建 {entry.Code}.dat(百度语言代码: {entry.BaiduCode})。以 zh-CN 源文为 Key 集,值留空 = 缺译。");
  200. }
  201. else
  202. {
  203. MessageBox.Show("创建失败:文件已存在或参数无效。");
  204. }
  205. }
  206. private void AddEntry()
  207. {
  208. var source = _packs.FirstOrDefault(p => p.IsSource);
  209. if (source == null)
  210. {
  211. MessageBox.Show("未找到 zh-CN 源语言包,无法新增词条。请先加载包含 zh-CN.dat 的目录。");
  212. return;
  213. }
  214. var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
  215. var win = new AddEntryWindow(langs.ToList()) { Owner = Application.Current.MainWindow };
  216. if (win.ShowDialog() != true) return;
  217. var key = win.EntryKey;
  218. if (source.Translations.ContainsKey(key))
  219. {
  220. MessageBox.Show($"词条已存在:{key}");
  221. return;
  222. }
  223. source.Translations[key] = key;
  224. foreach (var lang in _targets)
  225. {
  226. if (win.EntryTranslations.TryGetValue(lang.LanguageCode, out var v) &&
  227. !string.IsNullOrWhiteSpace(v))
  228. {
  229. lang.Translations[key] = v;
  230. }
  231. }
  232. foreach (var lang in _packs) PackRepository.Save(lang);
  233. LoadPacks();
  234. Stats = $"已新增词条:{key}";
  235. }
  236. private void DeleteRow(RowVm row)
  237. {
  238. if (row == null || string.IsNullOrEmpty(row.Key)) return;
  239. DeleteByKey(row.Key);
  240. }
  241. /// <summary>批量删除(右键"删除选中行",DataGrid 多选):只弹一次确认。</summary>
  242. private void DeleteRows(System.Collections.IList items)
  243. {
  244. var rows = items?.OfType<RowVm>()
  245. .Where(r => r != null && !string.IsNullOrEmpty(r.Key))
  246. .ToList();
  247. if (rows == null || rows.Count == 0) return;
  248. if (rows.Count == 1) { DeleteByKey(rows[0].Key); return; } // 单选 = 原单条流程
  249. var preview = string.Join("、", rows.Take(5).Select(r => $"「{r.Key}」"));
  250. var more = rows.Count > 5 ? $"\n… 等共 {rows.Count} 条" : "";
  251. var confirm = MessageBox.Show(
  252. $"确认删除选中的 {rows.Count} 个词条?\n{preview}{more}\n\n该操作将从所有语言包中移除(不可撤销)。",
  253. "批量删除确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
  254. if (confirm != MessageBoxResult.Yes) return;
  255. DeleteKeysSilent(rows.Select(r => r.Key));
  256. Stats = $"已批量删除 {rows.Count} 个词条,全部语言包已保存";
  257. }
  258. private void DeleteByKey(string key)
  259. {
  260. var confirm = MessageBox.Show($"确认删除词条「{key}」?该操作将从所有语言包中移除(不可撤销)。", "删除确认",
  261. MessageBoxButton.YesNo, MessageBoxImage.Warning);
  262. if (confirm != MessageBoxResult.Yes) return;
  263. DeleteKeysSilent(new[] { key });
  264. Stats = $"已删除词条:{key}";
  265. }
  266. /// <summary>删除词条核心(不弹确认):从所有语言包移除,统一保存一次、刷新一次。</summary>
  267. private void DeleteKeysSilent(IEnumerable<string> keys)
  268. {
  269. var set = new HashSet<string>(keys.Where(k => !string.IsNullOrEmpty(k)));
  270. if (set.Count == 0) return;
  271. foreach (var p in _packs)
  272. foreach (var k in set)
  273. p.Translations.Remove(k);
  274. foreach (var p in _packs) PackRepository.Save(p);
  275. LoadPacks();
  276. }
  277. private void CopyKey(RowVm row)
  278. {
  279. if (row == null || string.IsNullOrEmpty(row.Key)) return;
  280. Clipboard.SetText(row.Key);
  281. Stats = $"✅ 已复制: {row.Key}";
  282. }
  283. private void OpenSettings()
  284. {
  285. var cfg = Config.Load();
  286. var settingsWindow = new TranslationSettingsWindow(cfg) { Owner = Application.Current.MainWindow };
  287. if (settingsWindow.ShowDialog() == true)
  288. {
  289. _cfg = Config.Load();
  290. Translation.Reset();
  291. }
  292. }
  293. private void OpenAiChat()
  294. {
  295. var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
  296. var win = new AiChatWindow(_cfg, langs.ToList(), BuildAiTools(), ExecuteAiToolAsync, ApplyRowsFromAi)
  297. {
  298. Owner = Application.Current.MainWindow
  299. };
  300. win.ShowDialog();
  301. try { LoadPacks(); } catch { }
  302. }
  303. // ---------- 校验 ----------
  304. private void Validate()
  305. {
  306. if (_rows.Count == 0) { MessageBox.Show("请先加载语言包。"); return; }
  307. foreach (var r in _rows)
  308. foreach (var c in r.Cells.Values)
  309. {
  310. c.HasIssue = false;
  311. c.IssueMessage = "";
  312. }
  313. var fileErrors = new List<string>();
  314. try
  315. {
  316. // .json 校验
  317. foreach (var f in System.IO.Directory.GetFiles(_dir, "*.json"))
  318. {
  319. try { LanguagePackIO.LoadJson(f); }
  320. catch (Exception ex) { fileErrors.Add($"{System.IO.Path.GetFileName(f)}: 无法读取({ex.Message})"); }
  321. }
  322. }
  323. catch { }
  324. var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
  325. var issues = TranslationValidator.Validate(_rows, langs);
  326. foreach (var i in issues)
  327. {
  328. i.Cell.HasIssue = true;
  329. i.Cell.IssueMessage = $"[{(i.IsError ? "错误" : "警告")}] {i.Message}";
  330. }
  331. var errorCount = issues.Count(x => x.IsError);
  332. var warnCount = issues.Count - errorCount;
  333. _lastIssues = issues;
  334. var summary = fileErrors.Count > 0
  335. ? $"⚠ 文件校验失败 {fileErrors.Count} 个:\n{string.Join("\n", fileErrors)}\n\n"
  336. : "";
  337. var body = $"{summary}译文校验完成:问题单元格 {issues.Count} 个(错误 {errorCount},警告 {warnCount})。"
  338. + "\n有问题的单元格已用红色边框标出,鼠标悬停可查看原因。";
  339. if (issues.Count > 0)
  340. {
  341. var preview = issues.Take(15)
  342. .Select(i => $"[第{i.Row.Index}行·{i.LangCode}] {i.Row.Key} → {i.Message}");
  343. var detail = "\n\n问题明细:\n" + string.Join("\n", preview);
  344. if (issues.Count > 15) detail += $"\n… 等共 {issues.Count} 条";
  345. body += detail;
  346. }
  347. MessageBox.Show(body, "校验结果");
  348. }
  349. // ---------- 搜索 / 过滤 ----------
  350. private string _filterText = "";
  351. public string FilterText
  352. {
  353. get => _filterText;
  354. set { if (Set(ref _filterText, value)) ApplyFilter(); }
  355. }
  356. private void ApplyFilter()
  357. {
  358. if (AllRows.Count == 0)
  359. {
  360. Rows = new List<RowVm>();
  361. return;
  362. }
  363. var keyword = FilterText?.Trim() ?? "";
  364. Rows = string.IsNullOrEmpty(keyword)
  365. ? AllRows
  366. : AllRows.Where(row =>
  367. (row.Key != null && row.Key.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) ||
  368. row.Cells.Values.Any(c => !string.IsNullOrEmpty(c.Value) &&
  369. c.Value.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)
  370. ).ToList();
  371. Stats = string.IsNullOrEmpty(keyword)
  372. ? $"共 {Rows.Count} 条"
  373. : $"过滤结果: {Rows.Count} / {AllRows.Count} 条";
  374. }
  375. private void ShowMissingOnly()
  376. {
  377. if (AllRows.Count == 0) return;
  378. Rows = AllRows.Where(r => r.HasMissing).ToList();
  379. Stats = $"显示 {Rows.Count} 条缺译记录(共 {AllRows.Count} 条)";
  380. }
  381. private void ClearFilter()
  382. {
  383. FilterText = "";
  384. Rows = AllRows;
  385. Stats = BuildStats();
  386. }
  387. // ---------- CSV 导入导出(逗号分隔,Excel 可直接编辑) ----------
  388. /// <summary>导出当前表格为 CSV:列 = 勾选语言(未勾选用全部目标语言),行 = 当前过滤视图。</summary>
  389. private void ExportCsv()
  390. {
  391. if (_packs.Count == 0) { MessageBox.Show("请先加载语言包。"); return; }
  392. var langs = _selectedLangs.Count > 0 ? _selectedLangs : _targets;
  393. if (langs.Count == 0) { MessageBox.Show("没有可导出的语言列。"); return; }
  394. var dlg = new Microsoft.Win32.SaveFileDialog
  395. {
  396. Filter = "CSV 表格 (*.csv)|*.csv",
  397. FileName = $"语言包导出_{DateTime.Now:yyyyMMdd_HHmm}.csv",
  398. };
  399. if (dlg.ShowDialog() != true) return;
  400. var viewRows = Rows ?? new List<RowVm>();
  401. try
  402. {
  403. CsvExchange.Export(dlg.FileName, viewRows, langs);
  404. Stats = $"已导出 {viewRows.Count} 行 → {System.IO.Path.GetFileName(dlg.FileName)}";
  405. MessageBox.Show($"导出完成:{viewRows.Count} 行,语言列 {string.Join(", ", langs.Select(l => l.LanguageCode))}。\n\n注意:CSV 中的空单元格导入时不会清空已有译文。",
  406. "导出", MessageBoxButton.OK, MessageBoxImage.Information);
  407. }
  408. catch (Exception ex)
  409. {
  410. MessageBox.Show("导出失败:" + ex.Message);
  411. }
  412. }
  413. /// <summary>从 CSV 导入:中文原文已存在 → 替换有值的译文;新中文原文 → 新增词条。写入前弹预览确认。</summary>
  414. private void ImportCsv()
  415. {
  416. if (_packs.Count == 0) { MessageBox.Show("请先加载语言包。"); return; }
  417. var dlg = new Microsoft.Win32.OpenFileDialog
  418. {
  419. Filter = "CSV 表格 (*.csv)|*.csv|所有文件 (*.*)|*.*",
  420. };
  421. if (dlg.ShowDialog() != true) return;
  422. List<string> header;
  423. List<string[]> records;
  424. try
  425. {
  426. var parsed = CsvExchange.Parse(dlg.FileName);
  427. header = parsed.Item1;
  428. records = parsed.Item2;
  429. }
  430. catch (Exception ex)
  431. {
  432. MessageBox.Show("读取失败:" + ex.Message);
  433. return;
  434. }
  435. if (header.Count == 0) { MessageBox.Show("CSV 是空的。"); return; }
  436. // 列映射:"中文原文"列 + 语言代码列(匹配当前已加载语言;其余列忽略)
  437. int keyCol = header.FindIndex(h => h != null && h.Trim() == "中文原文");
  438. if (keyCol < 0) keyCol = 0; // 无表头时默认第一列为中文原文
  439. var langCols = new List<(PackFile pack, int col)>();
  440. for (int i = 0; i < header.Count; i++)
  441. {
  442. if (i == keyCol) continue;
  443. var code = header[i]?.Trim() ?? "";
  444. var pack = _targets.FirstOrDefault(t => string.Equals(t.LanguageCode, code, StringComparison.OrdinalIgnoreCase));
  445. if (pack != null) langCols.Add((pack, i));
  446. }
  447. if (langCols.Count == 0)
  448. {
  449. MessageBox.Show($"CSV 表头没有匹配到已加载的语言列。\n表头需为语言代码(如 en-US、ja-JP),已加载:{string.Join(", ", _targets.Select(t => t.LanguageCode))}");
  450. return;
  451. }
  452. var knownKeys = new HashSet<string>(_rows.Select(r => r.Key), StringComparer.Ordinal);
  453. var source = _packs.FirstOrDefault(p => p.IsSource);
  454. int addCount = 0, updCount = 0;
  455. foreach (var rec in records)
  456. {
  457. if (rec.Length <= keyCol) continue;
  458. var key = (rec[keyCol] ?? "").Trim();
  459. if (key.Length == 0) continue;
  460. if (!knownKeys.Contains(key))
  461. {
  462. // 新词条:中文源包 key=key(key 即中文原文),目标语言写入 CSV 提供的译文
  463. if (source != null) source.Translations[key] = key;
  464. foreach (var lc in langCols)
  465. if (lc.col < rec.Length && !string.IsNullOrWhiteSpace(rec[lc.col]))
  466. lc.pack.Translations[key] = rec[lc.col].Trim();
  467. knownKeys.Add(key);
  468. addCount++;
  469. }
  470. else
  471. {
  472. // 已有词条:CSV 里非空且有变化的译文才替换(空单元格不动,防止误清空)
  473. foreach (var lc in langCols)
  474. {
  475. if (lc.col >= rec.Length) continue;
  476. var val = (rec[lc.col] ?? "").Trim();
  477. if (val.Length == 0) continue;
  478. var cur = lc.pack.Translations.TryGetValue(key, out var v) ? v : "";
  479. if (cur != val)
  480. {
  481. lc.pack.Translations[key] = val;
  482. updCount++;
  483. }
  484. }
  485. }
  486. }
  487. if (addCount == 0 && updCount == 0)
  488. {
  489. MessageBox.Show("没有需要导入的变更(词条全部已存在且译文与当前一致)。", "导入",
  490. MessageBoxButton.OK, MessageBoxImage.Information);
  491. return;
  492. }
  493. var confirm = MessageBox.Show(
  494. $"导入预览:\n\n新增词条 {addCount} 条\n更新译文 {updCount} 处\n语言列:{string.Join(", ", langCols.Select(c => c.pack.LanguageCode).Distinct())}\n\n确认写入并保存语言包?",
  495. "确认导入", MessageBoxButton.YesNo, MessageBoxImage.Question);
  496. if (confirm != MessageBoxResult.Yes) return;
  497. try
  498. {
  499. foreach (var p in _packs) PackRepository.Save(p);
  500. LoadPacks();
  501. Stats = $"导入完成:新增 {addCount} 条,更新 {updCount} 处译文";
  502. MessageBox.Show($"导入完成:新增 {addCount} 条,更新 {updCount} 处译文。", "导入",
  503. MessageBoxButton.OK, MessageBoxImage.Information);
  504. }
  505. catch (Exception ex)
  506. {
  507. MessageBox.Show("写入失败:" + ex.Message);
  508. }
  509. }
  510. }
  511. }