| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Windows;
- using LocalizationTool.Models;
- using LocalizationTool.Services;
- using LocalizationTool.Views;
- namespace LocalizationTool.ViewModels
- {
- /// <summary>主窗口 ViewModel:语言包加载、翻译、校验、搜索与 AI 工具。</summary>
- public partial class MainViewModel : ViewModelBase
- {
- private List<PackFile> _packs = new List<PackFile>();
- private List<PackFile> _targets = new List<PackFile>();
- private List<PackFile> _selectedLangs = new List<PackFile>();
- private List<RowVm> _rows = new List<RowVm>();
- private List<RowVm> _allRows = new List<RowVm>();
- private List<TranslationIssue> _lastIssues = new List<TranslationIssue>();
- 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);
- }
- /// <summary>表格行(受搜索过滤影响)。</summary>
- public List<RowVm> Rows
- {
- get => _rows;
- set => Set(ref _rows, value);
- }
- /// <summary>全量行,过滤时基于它。</summary>
- public List<RowVm> AllRows
- {
- get => _allRows;
- set => Set(ref _allRows, value);
- }
- /// <summary>目标语言(不含源语言)。</summary>
- public IReadOnlyList<PackFile> Targets => _targets;
- /// <summary>勾选的语言(= 表格列 + 翻译目标)。</summary>
- public IReadOnlyList<PackFile> SelectedLangs => _selectedLangs;
- private List<LangOption> _langOptions = new List<LangOption>();
- /// <summary>语言下拉选项(CheckComboBox 的 ItemsSource)。</summary>
- public List<LangOption> LangOptions
- {
- get => _langOptions;
- set => Set(ref _langOptions, value);
- }
- /// <summary>语言勾选变化(View 据此重建表格列)。</summary>
- public event Action SelectedLangsChanged;
- /// <summary>语言下拉选项变化(View 据此刷新 CheckComboBox)。</summary>
- public event Action LangOptionsChanged;
- /// <summary>翻译开关状态。</summary>
- 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<RowVm> TranslateRowCommand { get; }
- public RelayCommand<RowVm> DeleteRowCommand { get; }
- public RelayCommand<System.Collections.IList> DeleteRowsCommand { get; }
- public RelayCommand<RowVm> 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<RowVm>(TranslateRow);
- DeleteRowCommand = new RelayCommand<RowVm>(DeleteRow);
- DeleteRowsCommand = new RelayCommand<System.Collections.IList>(DeleteRows);
- CopyKeyCommand = new RelayCommand<RowVm>(CopyKey);
- ExportCsvCommand = new RelayCommand(ExportCsv);
- ImportCsvCommand = new RelayCommand(ImportCsv);
- }
- /// <summary>外部(浏览对话框)设置目录后加载。</summary>
- 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();
- }
- /// <summary>刷新语言下拉选项(默认全选)并通知 View 更新。</summary>
- 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();
- }
- /// <summary>CheckComboBox 勾选变化时由 View 调用。</summary>
- public void SetSelectedLangs(IEnumerable<string> codes)
- {
- var set = new HashSet<string>(codes ?? Enumerable.Empty<string>(), 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("已保存全部语言包。", "保存");
- }
- /// <summary>编辑后刷新缺译概览。</summary>
- 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);
- }
- /// <summary>批量删除(右键"删除选中行",DataGrid 多选):只弹一次确认。</summary>
- private void DeleteRows(System.Collections.IList items)
- {
- var rows = items?.OfType<RowVm>()
- .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}";
- }
- /// <summary>删除词条核心(不弹确认):从所有语言包移除,统一保存一次、刷新一次。</summary>
- private void DeleteKeysSilent(IEnumerable<string> keys)
- {
- var set = new HashSet<string>(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<string>();
- 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<RowVm>();
- 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 可直接编辑) ----------
- /// <summary>导出当前表格为 CSV:列 = 勾选语言(未勾选用全部目标语言),行 = 当前过滤视图。</summary>
- 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<RowVm>();
- 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);
- }
- }
- /// <summary>从 CSV 导入:中文原文已存在 → 替换有值的译文;新中文原文 → 新增词条。写入前弹预览确认。</summary>
- 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<string> header;
- List<string[]> 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<string>(_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);
- }
- }
- }
- }
|