From 36c9004bfcd023fbd6d9952e541659c9bb8ffdf0 Mon Sep 17 00:00:00 2001 From: qwqcode Date: Tue, 24 Sep 2024 20:48:41 +0800 Subject: [PATCH 1/3] feat: add i18n support - Load language settings from config - Set default language based on system culture - Implement language translation helper - Add language resource loading mechanism --- SubRenamer/App.axaml | 13 +- SubRenamer/App.axaml.cs | 17 ++- SubRenamer/Assets/Lang/en-US.axaml | 126 +++++++++++++++++++ SubRenamer/Assets/Lang/zh-Hans.axaml | 126 +++++++++++++++++++ SubRenamer/Config.cs | 1 + SubRenamer/Helper/I18NHelper.cs | 37 ++++++ SubRenamer/Helper/IssueReporter.cs | 14 ++- SubRenamer/Helper/MatchItemHelper.cs | 8 +- SubRenamer/Helper/ResourceHelper.cs | 29 +++++ SubRenamer/Model/MatchItem.cs | 39 +++++- SubRenamer/Model/MatchItemStatus.cs | 10 ++ SubRenamer/Model/RenameTask.cs | 41 +++++- SubRenamer/Model/RenameTaskStatus.cs | 9 ++ SubRenamer/Services/DialogService.cs | 8 +- SubRenamer/Services/FilesService.cs | 11 +- SubRenamer/Services/ImportService.cs | 4 +- SubRenamer/Services/RenameService.cs | 37 +++--- SubRenamer/SubRenamer.csproj | 11 -- SubRenamer/ViewModels/MainViewModel.cs | 7 +- SubRenamer/ViewModels/ManualRuleViewModel.cs | 7 +- SubRenamer/ViewModels/RegexRuleViewModel.cs | 5 +- SubRenamer/ViewModels/SettingsViewModel.cs | 38 ++++-- SubRenamer/Views/ConflictWindow.axaml | 6 +- SubRenamer/Views/ItemEditWindow.axaml | 8 +- SubRenamer/Views/MainWindow.axaml | 83 ++++++------ SubRenamer/Views/ManualRuleWindow.axaml | 28 ++--- SubRenamer/Views/RegexRuleWindow.axaml | 32 ++--- SubRenamer/Views/RulesWindow.axaml | 18 +-- SubRenamer/Views/SettingsWindow.axaml | 60 +++++---- 29 files changed, 640 insertions(+), 193 deletions(-) create mode 100644 SubRenamer/Assets/Lang/en-US.axaml create mode 100644 SubRenamer/Assets/Lang/zh-Hans.axaml create mode 100644 SubRenamer/Helper/I18NHelper.cs create mode 100644 SubRenamer/Helper/ResourceHelper.cs create mode 100644 SubRenamer/Model/MatchItemStatus.cs create mode 100644 SubRenamer/Model/RenameTaskStatus.cs diff --git a/SubRenamer/App.axaml b/SubRenamer/App.axaml index cbc28e5..8d951a9 100644 --- a/SubRenamer/App.axaml +++ b/SubRenamer/App.axaml @@ -9,6 +9,15 @@ + + + + + + + + + @@ -16,9 +25,9 @@ - + - + diff --git a/SubRenamer/App.axaml.cs b/SubRenamer/App.axaml.cs index b16e6cb..2e8ef21 100644 --- a/SubRenamer/App.axaml.cs +++ b/SubRenamer/App.axaml.cs @@ -1,22 +1,15 @@ using System; -using System.Collections.Generic; +using System.Globalization; using System.IO; -using System.Linq; using System.Threading.Tasks; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; -using Avalonia.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using SubRenamer.ViewModels; using SubRenamer.Views; using Microsoft.Extensions.DependencyInjection; -using MsBox.Avalonia; -using MsBox.Avalonia.Dto; -using MsBox.Avalonia.Enums; -using MsBox.Avalonia.Models; using SubRenamer.Helper; using SubRenamer.Model; using SubRenamer.Services; @@ -47,6 +40,12 @@ public override void OnFrameworkInitializationCompleted() // load theme Config.ApplyThemeMode(Config.Get().ThemeMode); + + // load i18n + if (string.IsNullOrWhiteSpace(Config.Get().Language)) + Config.Get().Language = CultureInfo.CurrentCulture.Name.Contains("en") ? "en-US" : "zh-Hans"; + if (Config.Get().Language != I18NHelper.DefaultLanguage) + Application.Current.Translate(Config.Get().Language); var mainWindowStore = new MainViewModel(); desktop.MainWindow = new MainWindow @@ -142,7 +141,7 @@ private static void _afterInitTasks(MainViewModel? mainWindowStore) var updateSrc = await Updater.GetUpdatesAsync(); if (updateSrc != null && mainWindowStore != null) { - mainWindowStore.CurrVersionText += " (有更新)"; + mainWindowStore.CurrVersionText += " " + Application.Current.GetResource("App.Strings.MenuUpdateAlert"); mainWindowStore.CurrVersionBtnLink = updateSrc; } } diff --git a/SubRenamer/Assets/Lang/en-US.axaml b/SubRenamer/Assets/Lang/en-US.axaml new file mode 100644 index 0000000..b6d3b41 --- /dev/null +++ b/SubRenamer/Assets/Lang/en-US.axaml @@ -0,0 +1,126 @@ + + SubRenamer + Import Files + Import Folder + Edit + Delete + Rematch + Clear List + Rules + Settings + Preview + One-Click Rename + Settings... + Quit SubRenamer + File + Import Files... + Import Folder... + Rematch + Clear List + Rules + Settings + Pinned + Pin + (Update Available) + Edit + Delete + Drop Video + Drop Subtitle + Select All + Reveal Video in Folder + Reveal Subtitle in Folder + Copy Rename Commands to Clipboard + Match + Video + Subtitle + Status + Original File Name + Modified + Modified + Ready + Failed + No Modify + Matched + No Video + No Subtitle + Unmatched + Modified + Subtitle Filter + Multiple languages detected in subtitles, please select: + Import Files + Keep All + Edit + Match + Video File + Subtitle File + Manual Match + Video Filename Rule + Select Video File + Select any file and manually change the episode number on the left + No Match + Subtitle Filename Rule + Select Subtitle File + Wildcard * The asterisk can indicate the omission of any text. For example: +For the file name: "[Steins;Gate][11][1080P] The Doctrine of the Boundary of Space and Time.sc.ass" +It can be modified to: "[Steins;Gate][$$]*.sc.ass" + Save + Cancel + Error + Regex Match + Video File Regex + e.g., Breaking\.Bad\.S03E(\d+).*\.mp4 + Subtitle File Regex + e.g., Breaking\.Bad\.S03E(\d+).*\.ass + Match Test + Video + Subtitle + Input content or open file... + No Match + Learn More + Test Tool + Save + Cancel + No Match + Error + Match Rules + Auto Match + Match by comparing differences in filenames + Manual Match + Manually input text before and after episode number in filenames + Regex Match + Manually input regex to match episode numbers in filenames + Edit + Settings + Language + Backup Original Subtitles + If subtitles and video files are in the same directory, back up to SubBackup + Language Filter Dialog + Show language filter dialog when imported subtitles contain multiple languages + Keep Subtitle Language Suffix + For example, keep ".sc" in "subtitle.sc.srt" + Add Custom Suffix + Add a custom suffix before the subtitle file extension + Enter suffix, e.g., zh-Hans + File Format Extension + Extend the suffixes for recognizing subtitle and video file formats (separated by commas) + Subtitle Format Extension + e.g., srt + Video Format Extension + e.g., mkv + Check for Program Updates + |´・ω・)ノ Hi! This is an open-source program + You can find the source code on + GitHub + , please consider giving it a star 🌟, it would help us a lot! + Report Issues + Changelog + Report Issue + Ignore + Program Crash Detected x_x + Click the button below to send an error report + Select and Import Files + Import Folder + Save File + \ No newline at end of file diff --git a/SubRenamer/Assets/Lang/zh-Hans.axaml b/SubRenamer/Assets/Lang/zh-Hans.axaml new file mode 100644 index 0000000..3a76d75 --- /dev/null +++ b/SubRenamer/Assets/Lang/zh-Hans.axaml @@ -0,0 +1,126 @@ + + SubRenamer + 导入文件 + 导入文件夹 + 编辑 + 删除 + 重新匹配 + 清空列表 + 规则 + 设置 + 预览 + 一键改名 + 设置... + 退出 SubRenamer + 文件 + 导入文件... + 导入文件夹... + 重新匹配 + 清空列表 + 规则 + 设置 + 已置顶 + 置顶 + (有更新) + 编辑 + 删除 + 丢弃视频 + 丢弃字幕 + 全选 + 在文件夹中找到视频文件 + 在文件夹中找到字幕文件 + 复制改名命令至剪切板 + 匹配 + 视频 + 字幕 + 状态 + 原始文件名 + 修改后 + 已修改 + 待修改 + 失败 + 无需修改 + 已匹配 + 缺视频 + 缺字幕 + 未匹配 + 已修改 + 字幕筛选 + 检测到字幕似乎存在多种语言,请选择: + 导入文件 + 全部保留 + 编辑 + 匹配 + 视频文件 + 字幕文件 + 手动匹配 + 视频 文件名规则 + 选择视频文件 + 任意选择一个文件,然后手动将左侧集数修改为 $$ + 无匹配 + 字幕 文件名规则 + 选择字幕文件 + 通配符 * 星号可表示省略任意文字,举个栗子: +对于文件名:“[Steins;Gate][11][1080P] 时空边界的教理.sc.ass” +可以修改为:“[Steins;Gate][$$]*.sc.ass” + 保存 + 取消 + 错误 + 正则表达式匹配 + 视频文件 正则表达式 + 例如:Breaking\.Bad\.S03E(\d+).*\.mp4 + 字幕文件 正则表达式 + 例如:Breaking\.Bad\.S03E(\d+).*\.ass + 匹配测试 + 视频 + 字幕 + 输入内容或打开文件... + 无匹配 + 了解更多 + 测试工具 + 保存 + 取消 + 无匹配 + 错误 + 匹配规则 + 自动匹配 + 通过比较两个文件名的差异来匹配 + 手动匹配 + 手动输入文件名中集数前后的文字来匹配 + 正则匹配 + 手动输入用于匹配文件名集数的正则表达式 + 编辑 + 设置 + 语言 + 备份原始字幕 + 若字幕和视频文件处于相同目录内,则备份到 SubBackup + 语言筛选对话框 + 检测到导入的字幕文件包含多种语言时,弹出语言筛选对话框 + 保留字幕语言后缀 + 例如 "subtitle.sc.srt" 保留字幕语言后缀 ".sc" + 追加字幕后缀 + 在字幕文件扩展名前添加自定义后缀 + 输入后缀,例如:zh-Hans + 文件识别格式扩充 + 扩充用于识别字幕和视频文件格式的后缀名(以英文逗号分隔) + 字幕格式扩充 + 例如:srt + 视频格式扩充 + 例如:mkv + 程序升级检查 + |´・ω・)ノ 嗨!这是开源程序 + 你可以在 + GitHub + 找到源代码,请考虑点个 Star 🌟这会对我们很有帮助! + 反馈问题 + 更新日志 + 反馈问题 + 忽略 + 检测到程序崩溃 x_x + 点击下方按钮反馈错误报告 + 选择并导入文件 + 导入文件夹 + 保存文件 + \ No newline at end of file diff --git a/SubRenamer/Config.cs b/SubRenamer/Config.cs index 17c2085..844ba00 100644 --- a/SubRenamer/Config.cs +++ b/SubRenamer/Config.cs @@ -10,6 +10,7 @@ namespace SubRenamer; public partial class Config { public ThemeMode ThemeMode { get; set; } = ThemeMode.Default; + public string Language { get; set; } = ""; public bool Backup { get; set; } = true; public bool UpdateCheck { get; set; } = true; public bool KeepLangExt { get; set; } = false; diff --git a/SubRenamer/Helper/I18NHelper.cs b/SubRenamer/Helper/I18NHelper.cs new file mode 100644 index 0000000..45bd439 --- /dev/null +++ b/SubRenamer/Helper/I18NHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Platform; + +namespace SubRenamer.Helper; + +public static class I18NHelper +{ + public static readonly string DefaultLanguage = "zh-Hans"; + public static string[] Languages { get; } = ["en-US:English", "zh-Hans:简体中文"]; + public static string[] LanguageNames { get; } = Languages.Select(x => x.Split(":")[0]).ToArray(); + public static string[] LanguageTitles { get; } = Languages.Select(x => x.Split(":")[1]).ToArray(); + + public static void Translate( + this Application? app, + string targetLanguage) + { + if (!LanguageNames.Contains(targetLanguage)) + { + Console.WriteLine($"[I18NHelper.Translate] language \"{targetLanguage}\" not support"); + return; + } + + var translations = App.Current?.Resources.MergedDictionaries.OfType() + .FirstOrDefault(x => x.Source?.OriginalString?.Contains("/Lang/") ?? false); + if (translations != null) + App.Current?.Resources.MergedDictionaries.Remove(translations); + + var uri = new Uri($"avares://SubRenamer/Assets/Lang/{targetLanguage}.axaml"); + App.Current?.Resources.MergedDictionaries.Add(new ResourceInclude(uri) + { + Source = uri + }); + } +} \ No newline at end of file diff --git a/SubRenamer/Helper/IssueReporter.cs b/SubRenamer/Helper/IssueReporter.cs index c32726a..82bd149 100644 --- a/SubRenamer/Helper/IssueReporter.cs +++ b/SubRenamer/Helper/IssueReporter.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Web; +using Avalonia; using Avalonia.Controls; using Avalonia.Threading; using MsBox.Avalonia; @@ -23,16 +24,17 @@ public static void CheckCrashAndShowDialog() var crashLog = await File.ReadAllTextAsync(App.CrashLogFile); var title = crashLog.Split('\n').FirstOrDefault() ?? ""; - + + var reportBtnName = Application.Current.GetResource("App.Strings.IssueReporterReport"); var box = MessageBoxManager.GetMessageBoxCustom(new MessageBoxCustomParams { ButtonDefinitions = new List { - new ButtonDefinition { Name = "反馈问题", IsDefault = true }, - new ButtonDefinition { Name = "忽略", IsCancel = true } + new ButtonDefinition { Name = reportBtnName, IsDefault = true }, + new ButtonDefinition { Name = Application.Current.GetResource("App.Strings.IssueReporterIgnore"), IsCancel = true } }, - ContentTitle = "检测到程序崩溃 x_x", - ContentMessage = $"{title}\n\n点击下方按钮反馈错误报告", + ContentTitle = Application.Current.GetResource("App.Strings.IssueReporterCapital"), + ContentMessage = $"{title}\n\n{Application.Current.GetResource("App.Strings.IssueReporterMessage")}", Icon = MsBox.Avalonia.Enums.Icon.Warning, WindowStartupLocation = WindowStartupLocation.CenterScreen, CanResize = false, @@ -42,7 +44,7 @@ public static void CheckCrashAndShowDialog() Topmost = true }); var result = await box.ShowAsync(); - if (result == "反馈问题") + if (result == reportBtnName) { CreateGitHubIssue(title, $"{crashLog}\n\nWheel: {SystemInfo.GetOSArchPair()} Version: v{Config.AppVersion}"); } diff --git a/SubRenamer/Helper/MatchItemHelper.cs b/SubRenamer/Helper/MatchItemHelper.cs index 1c22a2d..fd6743a 100644 --- a/SubRenamer/Helper/MatchItemHelper.cs +++ b/SubRenamer/Helper/MatchItemHelper.cs @@ -6,9 +6,9 @@ public static class MatchItemHelper { public static void UpdateMatchItemStatus(MatchItem item) { - if (item.Key != "" && item.Subtitle != "" && item.Video != "") item.Status = "已匹配"; - else if (item.Key != "" && item.Subtitle != "") item.Status = "缺视频"; - else if (item.Key != "" && item.Video != "") item.Status = "缺字幕"; - else if (item.Key == "") item.Status = "未匹配"; + if (item.Key != "" && item.Subtitle != "" && item.Video != "") item.Status = MatchItemStatus.Matched; + else if (item.Key != "" && item.Subtitle != "") item.Status = MatchItemStatus.NoVideo; + else if (item.Key != "" && item.Video != "") item.Status = MatchItemStatus.NoSubtitle; + else if (item.Key == "") item.Status = MatchItemStatus.NoMatch; } } \ No newline at end of file diff --git a/SubRenamer/Helper/ResourceHelper.cs b/SubRenamer/Helper/ResourceHelper.cs new file mode 100644 index 0000000..83546ef --- /dev/null +++ b/SubRenamer/Helper/ResourceHelper.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Styling; + +namespace SubRenamer.Helper; + +public static class ResourceHelper +{ + /// + /// Gets the first resource matching the given key within the application resources dictionary. + /// + /// The type of resource to return. + /// The application instance. + /// The resource key. + /// The located resource or default(T). + public static T? GetResource( + this Application? app, + object key) + { + if (Application.Current?.Resources.TryGetResource(key, null, out var resource) ?? false) + { + if (resource is T value) + { + return value; + } + } + + return default(T); + } +} \ No newline at end of file diff --git a/SubRenamer/Model/MatchItem.cs b/SubRenamer/Model/MatchItem.cs index b4c3291..597e239 100644 --- a/SubRenamer/Model/MatchItem.cs +++ b/SubRenamer/Model/MatchItem.cs @@ -1,33 +1,60 @@ using System.IO; +using Avalonia; using CommunityToolkit.Mvvm.ComponentModel; +using SubRenamer.Helper; namespace SubRenamer.Model; -public partial class MatchItem(string key = "", string video = "", string subtitle = "", string status = "") : ObservableObject +public partial class MatchItem(string key = "", string video = "", string subtitle = "") : ObservableObject { /** * Match Key */ [ObservableProperty] private string _key = key; - + /** * Video Absolute Path */ [ObservableProperty] private string _video = video; + [ObservableProperty] private string _videoName = Path.GetFileName(video); - + partial void OnVideoChanged(string value) => VideoName = Path.GetFileName(value); - + /** * Subtitle Absolute Path */ [ObservableProperty] private string _subtitle = subtitle; + [ObservableProperty] private string _subtitleName = Path.GetFileName(subtitle); - + partial void OnSubtitleChanged(string value) => SubtitleName = Path.GetFileName(value); /** * Current Status */ - [ObservableProperty] private string _status = status; + [ObservableProperty] private MatchItemStatus? _status; + + /** + * Current Status Text + */ + [ObservableProperty] private string _statusText = ""; + + partial void OnStatusChanged(MatchItemStatus? value) + { + StatusText = GetStatusText(); + } + + public string GetStatusText() + { + return Status switch + { + MatchItemStatus.Matched => Application.Current.GetResource("App.Strings.MatchItemStatusMatched") ?? "Matched", + MatchItemStatus.NoVideo => Application.Current.GetResource("App.Strings.MatchItemStatusNoVideo") ?? "NoVideo", + MatchItemStatus.NoSubtitle => Application.Current.GetResource("App.Strings.MatchItemStatusNoSubtitle") ?? "NoSubtitle", + MatchItemStatus.NoMatch => Application.Current.GetResource("App.Strings.MatchItemStatusNoMatch") ?? "NoMatch", + MatchItemStatus.Altered => Application.Current.GetResource("App.Strings.MatchItemStatusAltered") ?? "Altered", + _ => "" + }; + } } \ No newline at end of file diff --git a/SubRenamer/Model/MatchItemStatus.cs b/SubRenamer/Model/MatchItemStatus.cs new file mode 100644 index 0000000..b5b31ee --- /dev/null +++ b/SubRenamer/Model/MatchItemStatus.cs @@ -0,0 +1,10 @@ +namespace SubRenamer.Model; + +public enum MatchItemStatus +{ + Matched, + NoVideo, + NoSubtitle, + NoMatch, + Altered, +} \ No newline at end of file diff --git a/SubRenamer/Model/RenameTask.cs b/SubRenamer/Model/RenameTask.cs index a43cbd9..883f3f6 100644 --- a/SubRenamer/Model/RenameTask.cs +++ b/SubRenamer/Model/RenameTask.cs @@ -1,30 +1,61 @@ using System.IO; +using Avalonia; using CommunityToolkit.Mvvm.ComponentModel; +using SubRenamer.Helper; namespace SubRenamer.Model; -public partial class RenameTask(string origin = "", string alter = "", string status = "") : ObservableObject +public partial class RenameTask(string origin = "", string alter = "") : ObservableObject { /** * Original Full Path */ [ObservableProperty] private string _origin = origin; + [ObservableProperty] private string _originName = Path.GetFileName(origin); - + partial void OnOriginChanged(string value) => OriginName = Path.GetFileName(value); /** * Alter to Full Path */ [ObservableProperty] private string _alter = alter; + [ObservableProperty] private string _alterName = Path.GetFileName(alter); - + partial void OnAlterChanged(string value) => AlterName = Path.GetFileName(value); - + /** * Current Status */ - [ObservableProperty] private string _status = status; + [ObservableProperty] private RenameTaskStatus? _status; + + /** + * Current Status Text + */ + [ObservableProperty] private string _statusText = ""; + + partial void OnStatusChanged(RenameTaskStatus? value) + { + StatusText = GetStatusText(); + } + + public string GetStatusText() + { + return Status switch + { + RenameTaskStatus.Altered => Application.Current.GetResource("App.Strings.RenameTaskStatusAltered") ?? "Altered", + RenameTaskStatus.Ready => Application.Current.GetResource("App.Strings.RenameTaskStatusReady") ?? "Ready", + RenameTaskStatus.Failed => $"{Application.Current.GetResource("App.Strings.RenameTaskStatusFailed") ?? "Failed"} - {ErrorMessage}", + RenameTaskStatus.NoNeed => Application.Current.GetResource("App.Strings.RenameTaskStatusNoNeed") ?? "NoNeed", + _ => "" + }; + } + + /** + * Error Message + */ + [ObservableProperty] private string _errorMessage = ""; /** * Associated MatchItem diff --git a/SubRenamer/Model/RenameTaskStatus.cs b/SubRenamer/Model/RenameTaskStatus.cs new file mode 100644 index 0000000..de770a0 --- /dev/null +++ b/SubRenamer/Model/RenameTaskStatus.cs @@ -0,0 +1,9 @@ +namespace SubRenamer.Model; + +public enum RenameTaskStatus +{ + Altered, + Ready, + NoNeed, + Failed, +} \ No newline at end of file diff --git a/SubRenamer/Services/DialogService.cs b/SubRenamer/Services/DialogService.cs index e41ee5d..5767aa1 100644 --- a/SubRenamer/Services/DialogService.cs +++ b/SubRenamer/Services/DialogService.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; +using SubRenamer.Helper; using SubRenamer.Model; using SubRenamer.ViewModels; using SubRenamer.Views; @@ -50,7 +52,9 @@ public async Task OpenItemEdit(MatchItem item, ObservableCollection c public async Task OpenConflict(List options) { - var store = new ConflictViewModel([..options, "全部保留"]); + var keepAllText = Application.Current.GetResource("App.Strings.ConflictKeepAll"); + + var store = new ConflictViewModel([..options, keepAllText]); var dialog = new ConflictWindow { DataContext = store @@ -63,7 +67,7 @@ public async Task OpenItemEdit(MatchItem item, ObservableCollection c await dialog.ShowDialog(_target); if (cancel) throw new UserCancelDialogException(); var selected = store.GetResult(); - return selected == "全部保留" ? null : selected; + return selected == keepAllText ? null : selected; } public async Task OpenRegexModeSetting() diff --git a/SubRenamer/Services/FilesService.cs b/SubRenamer/Services/FilesService.cs index 05d20dd..cf81d4f 100644 --- a/SubRenamer/Services/FilesService.cs +++ b/SubRenamer/Services/FilesService.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; @@ -8,6 +6,7 @@ using SubRenamer.Helper; using static SubRenamer.Common.Constants; using SubRenamer.Model; +using Avalonia; namespace SubRenamer.Services; @@ -29,7 +28,7 @@ public async Task> OpenFilesAsync() { var files = await _target.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions() { - Title = "选择并导入文件", + Title = Application.Current.GetResource("App.Strings.OpenFileDialogTitle"), AllowMultiple = true, FileTypeFilter = new []{ VideosAndSubtitles }, }); @@ -41,7 +40,7 @@ public async Task> OpenFolderAsync() { var folders = await _target.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions() { - Title = "导入文件夹", + Title = Application.Current.GetResource("App.Strings.OpenFolderDialogTitle"), AllowMultiple = true, }); @@ -52,7 +51,7 @@ public async Task> OpenFolderAsync() { var files = await _target.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions() { - Title = "选择并打开文件", + Title = Application.Current.GetResource("App.Strings.OpenFileDialogTitle"), AllowMultiple = false }); @@ -64,7 +63,7 @@ public async Task> OpenFolderAsync() { return await _target.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions() { - Title = "Save Text File" + Title = Application.Current.GetResource("App.Strings.SaveFileDialogTitle") }); } } \ No newline at end of file diff --git a/SubRenamer/Services/ImportService.cs b/SubRenamer/Services/ImportService.cs index 27c9a6e..ae236dc 100644 --- a/SubRenamer/Services/ImportService.cs +++ b/SubRenamer/Services/ImportService.cs @@ -44,8 +44,8 @@ public async Task ImportMultipleFiles( // Import to list dataSource.Clear(); - videos.ForEach(x => dataSource.Add(new MatchItem("", x, "", ""))); - subtitles.ForEach(x => dataSource.Add(new MatchItem("", "", x, ""))); + videos.ForEach(x => dataSource.Add(new MatchItem("", x, ""))); + subtitles.ForEach(x => dataSource.Add(new MatchItem("", "", x))); } private static async Task> SolveSubtitleConflict( diff --git a/SubRenamer/Services/RenameService.cs b/SubRenamer/Services/RenameService.cs index 66f44a1..0bc181d 100644 --- a/SubRenamer/Services/RenameService.cs +++ b/SubRenamer/Services/RenameService.cs @@ -18,7 +18,9 @@ public void UpdateRenameTaskList(IReadOnlyList matchList, Collection< { destList.Clear(); - // 检查是否有重复的 Key (存在视频字幕一对多的情况),若有则保留语言后缀 + // Check for duplicate keys + // (there are cases where video subtitles have a one-to-many relationship), + // if found, retain the language suffix. // @see https://github.com/qwqcode/SubRenamer/pull/54 // @file https://github.com/qwqcode/SubRenamer/blob/main/SubRenamer.Tests/MatcherTests/MergeSameKeysItemsTests.cs var hasDuplicateKey = matchList.GroupBy(x => x.Key).Any(g => g.Count() > 1); @@ -30,31 +32,36 @@ public void UpdateRenameTaskList(IReadOnlyList matchList, Collection< { if (string.IsNullOrEmpty(item.Subtitle) || string.IsNullOrEmpty(item.Video)) continue; - // 字幕文件语言后缀 + // Subtitle file language suffix var subSuffix = ""; if (keepLangExt) { - // 从原始字幕文件名中提取语言后缀 + // Extract language suffix from original subtitle file name var subSplit = Path.GetFileNameWithoutExtension(item.Subtitle).Split('.'); if (subSplit.Length > 1) subSuffix = "." + subSplit[^1]; } if (hasCustomLangExt) { - // 自定义追加的后缀 + // Custom appended suffix subSuffix += "." + customLangExt.TrimStart('.'); } - // 拼接新的字幕文件路径 + // Splice new subtitle file path var videoFolder = Path.GetDirectoryName(item.Video) ?? ""; var subFilename = Path.GetFileNameWithoutExtension(item.Video) + subSuffix + Path.GetExtension(item.Subtitle); var altered = Path.Combine(videoFolder, subFilename); - // 是否无需修改 + // No need to alter var noNeedAlter = item.Subtitle == altered; - - // 添加到重命名任务列表中 - destList.Add(new RenameTask(item.Subtitle, altered, !noNeedAlter ? (item.Status == "已修改" ? "已修改" : "待修改") : "无需修改") + + // Add to rename task list + var status = !noNeedAlter + ? (item.Status != MatchItemStatus.Altered ? RenameTaskStatus.Ready : RenameTaskStatus.Altered) + : RenameTaskStatus.NoNeed; + + destList.Add(new RenameTask(item.Subtitle, altered) { - MatchItem = item + MatchItem = item, + Status = status }); } } @@ -69,7 +76,7 @@ public void ExecuteRename(IReadOnlyList taskList) foreach (var task in taskList) { - string[] skipStatus = ["已修改", "已跳过", "无需修改"]; + RenameTaskStatus?[] skipStatus = [RenameTaskStatus.Altered, RenameTaskStatus.NoNeed]; if (skipStatus.Contains(task.Status)) continue; try @@ -93,13 +100,13 @@ public void ExecuteRename(IReadOnlyList taskList) FileHelper.CopyFile(task.Origin, task.Alter); } - task.Status = "已修改"; - if (task.MatchItem != null) task.MatchItem.Status = "已修改"; + task.Status = RenameTaskStatus.Altered; + if (task.MatchItem != null) task.MatchItem.Status = MatchItemStatus.Altered; } catch (Exception e) { - task.Status = $"失败:{e.Message}"; - // task.Error = e.Message; + task.ErrorMessage = e.Message; + task.Status = RenameTaskStatus.Failed; } } } diff --git a/SubRenamer/SubRenamer.csproj b/SubRenamer/SubRenamer.csproj index 2ce5281..fcbd811 100644 --- a/SubRenamer/SubRenamer.csproj +++ b/SubRenamer/SubRenamer.csproj @@ -66,15 +66,4 @@ - - - - - - - - ManualModeSetting.axaml - Code - - diff --git a/SubRenamer/ViewModels/MainViewModel.cs b/SubRenamer/ViewModels/MainViewModel.cs index ece0fcc..14ee870 100644 --- a/SubRenamer/ViewModels/MainViewModel.cs +++ b/SubRenamer/ViewModels/MainViewModel.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Avalonia; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -213,9 +214,9 @@ private void RevealFileInFolder(string type) => public void SyncCurrentStatusText() => CurrMatchModeText = Config.Get().MatchMode switch { - MatchMode.Diff => "自动匹配", - MatchMode.Manual => "手动匹配", - MatchMode.Regex => "正则匹配", + MatchMode.Diff => Application.Current.GetResource("App.Strings.RulesAutoMatch") ?? "Diff", + MatchMode.Manual => Application.Current.GetResource("App.Strings.RulesManualMatch") ?? "Manual", + MatchMode.Regex => Application.Current.GetResource("App.Strings.RulesRegexMatch") ?? "Regex", _ => "" }; diff --git a/SubRenamer/ViewModels/ManualRuleViewModel.cs b/SubRenamer/ViewModels/ManualRuleViewModel.cs index ae8f776..3365b39 100644 --- a/SubRenamer/ViewModels/ManualRuleViewModel.cs +++ b/SubRenamer/ViewModels/ManualRuleViewModel.cs @@ -1,11 +1,13 @@ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using SubRenamer.Common; +using SubRenamer.Helper; using SubRenamer.Model; namespace SubRenamer.ViewModels; @@ -64,13 +66,14 @@ private string MatchByInputRegex(string pattern, string testCase) try { var match = Regex.Match(testCase, pattern); - if (!match.Success || match.Groups.Count == 0) return "未匹配"; + if (!match.Success || match.Groups.Count == 0) + return Application.Current.GetResource("App.Strings.ManualRuleNoMatch") ?? "No Match"; return match.Groups[1].Value; } catch (Exception e) { ErrorMessage = e.Message; - return "错误"; + return Application.Current.GetResource("App.Strings.ManualRuleMatchErr") ?? "Error"; } } diff --git a/SubRenamer/ViewModels/RegexRuleViewModel.cs b/SubRenamer/ViewModels/RegexRuleViewModel.cs index a0bbb7f..60f500e 100644 --- a/SubRenamer/ViewModels/RegexRuleViewModel.cs +++ b/SubRenamer/ViewModels/RegexRuleViewModel.cs @@ -57,13 +57,14 @@ private string MatchByInputRegex(string pattern, string testCase) try { var match = Regex.Match(testCase, pattern); - if (!match.Success || match.Groups.Count == 0) return "未匹配"; + if (!match.Success || match.Groups.Count == 0) + return Application.Current.GetResource("App.Strings.RegexRuleNoMatch") ?? "No Match"; return match.Groups[1].Value; } catch (Exception e) { ErrorMessage = e.Message; - return "错误"; + return Application.Current.GetResource("App.Strings.RegexRuleMatchErr") ?? "Error"; } } diff --git a/SubRenamer/ViewModels/SettingsViewModel.cs b/SubRenamer/ViewModels/SettingsViewModel.cs index cef2e81..ac64f3b 100644 --- a/SubRenamer/ViewModels/SettingsViewModel.cs +++ b/SubRenamer/ViewModels/SettingsViewModel.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; +using System.Linq; +using Avalonia; +using Avalonia.Markup.Xaml.Styling; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; -using SubRenamer.Services; -using Microsoft.Extensions.DependencyInjection; using SubRenamer.Helper; -using SubRenamer.Model; +using DynamicData; namespace SubRenamer.ViewModels; @@ -126,4 +121,29 @@ public bool FileClsExtAppendEnabled [RelayCommand] private void OpenLink(string url) => BrowserHelper.OpenBrowserAsync(url); + + #region Language + public static string[] LanguageNames { get; } = I18NHelper.LanguageNames; + public static string[] LanguageTitles { get; } = I18NHelper.LanguageTitles; + + private string _language = Config.Get().Language; + + [ObservableProperty] private int _languageIndex = LanguageNames.IndexOf(Config.Get().Language); + + public string Language + { + get => _language; + set + { + Config.Get().Language = value; + Application.Current.Translate(value); + SetProperty(ref _language, value); + } + } + + partial void OnLanguageIndexChanged(int value) + { + Language = I18NHelper.Languages[value].Split(":")[0]; + } + #endregion } \ No newline at end of file diff --git a/SubRenamer/Views/ConflictWindow.axaml b/SubRenamer/Views/ConflictWindow.axaml index 1a0e341..fbe112a 100644 --- a/SubRenamer/Views/ConflictWindow.axaml +++ b/SubRenamer/Views/ConflictWindow.axaml @@ -5,7 +5,7 @@ xmlns:viewModels="clr-namespace:SubRenamer.ViewModels" mc:Ignorable="d" x:Class="SubRenamer.Views.ConflictWindow" - Title="字幕筛选" + Title="{DynamicResource App.Strings.ConflictTitle}" WindowStartupLocation="CenterOwner" Width="400" SizeToContent="Height"> @@ -36,7 +36,7 @@ - + - - 任意选择一个文件,然后手动将左侧集数修改为 $$ - + + + @@ -34,15 +34,15 @@ - + - - 任意选择一个文件,然后手动将左侧集数修改为 $$ - + + + @@ -50,14 +50,10 @@ - - 通配符 * 星号可表示省略任意文字,举个栗子: - 对于文件名:“[Steins;Gate][11][1080P] 时空边界的教理.sc.ass” - 可以修改为:“[Steins;Gate][$$]*.sc.ass” - + - - + - - + @@ -71,12 +71,12 @@ - + - + - + - + - + diff --git a/SubRenamer/Views/SettingsWindow.axaml b/SubRenamer/Views/SettingsWindow.axaml index ef8cdfe..3c9e325 100644 --- a/SubRenamer/Views/SettingsWindow.axaml +++ b/SubRenamer/Views/SettingsWindow.axaml @@ -9,7 +9,7 @@ SizeToContent="Height" x:Class="SubRenamer.Views.SettingsWindow" x:DataType="vm:SettingsViewModel" - Title="设置"> + Title="{DynamicResource App.Strings.SettingsTitle}"> @@ -34,39 +34,57 @@ - 备份原始字幕 - + + + + + + + + + + + + + + + - 语言筛选对话框 - - 保留字幕语言后缀 - - 追加字幕后缀 + + + + + - - + + - 文件识别格式扩充 - + + - 字幕格式扩充 - + + - 视频格式扩充 - + + - 程序升级检查 + - |´・ω・)ノ 嗨!这是开源程序 - 你可以在